From 52921512c71aa25a118b0b77196f493655252467 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sun, 11 Oct 2026 12:11:25 +0200 Subject: [PATCH] Functions: TRIMRANGE (#296) Evaluator special form returning the range (or array) without its blank outer rows and columns; trim codes 0-3 as documented; written as _xlfn.TRIMRANGE. The trim-reference operators are not parsed yet. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- ROADMAP.md | 8 ++- crates/calc/src/eval.rs | 3 + crates/calc/src/lib.rs | 1 + crates/calc/src/tests.rs | 38 +++++++++++ crates/calc/src/trimrange.rs | 108 ++++++++++++++++++++++++++++++ crates/engine/src/cmd/formulas.rs | 1 + crates/xlsx/src/tables.rs | 2 + docs/function-parity.md | 16 +++-- docs/gaps.md | 5 +- docs/target-app-parity.md | 11 +-- 11 files changed, 177 insertions(+), 18 deletions(-) create mode 100644 crates/calc/src/trimrange.rs diff --git a/README.md b/README.md index 770b3fe..9f1795f 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ egui frontend is a separate crate, so the UI can be swapped without touching the GridCraft is in **alpha**: the core workflow works end to end, but depth, fidelity and file compatibility are still rough. It covers 89% of the Excel ribbon commands in our catalog -([`docs/parity-checklist.md`](docs/parity-checklist.md)) and 501 of Excel's 523 worksheet functions +([`docs/parity-checklist.md`](docs/parity-checklist.md)) and 502 of Excel's 523 worksheet functions ([`docs/function-parity.md`](docs/function-parity.md)); weighted by what Excel users rely on, we estimate it is about 50% ready for real work. Next up: XLSX fidelity on real-world files, very large workbooks and multi-threaded recalc, chart depth, text in every script, and `.xls` import. Stage, diff --git a/ROADMAP.md b/ROADMAP.md index 1189fd7..7b262c5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (full re-measure against Excel for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Excel (Microsoft 365) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (worksheet functions 502 / 523) · **Target:** Microsoft Excel (Microsoft 365) GridCraft aims at full Microsoft Excel parity, and at being better in places: faster, open (XLSX is the native format), fully scriptable by agents (MCP, control channel, CLI), and on the web. @@ -21,7 +21,7 @@ This page is the summary. The detail is in: | Number | Value | Kind | |---|---|---| -| **Feature breadth** (weighted) | **~75%** | estimated, anchored on two measurements: ribbon catalog 258 / 290 (89.0%), worksheet functions 501 / 523 (95.8%; 492 working, 94.1%) | +| **Feature breadth** (weighted) | **~75%** | estimated, anchored on two measurements: ribbon catalog 258 / 290 (89.0%), worksheet functions 502 / 523 (96.0%; 493 working, 94.3%) | | **Ready for real work** | **~50%** | estimated from written weights (51.6); [target-app-parity.md](docs/target-app-parity.md) | | **Mainstream practitioner** | **~50%** | estimated: typical analyst/finance user, weekly features, × 0.90 interaction × 0.92 stability × 0.88 file exchange; [method](docs/target-app-parity.md#mainstream-practitioner) | | **Essentials user** | **~63%** | estimated: core features only, × 0.92 launch/stability × 0.95 clarity × 0.90 opening sent files; [method](docs/target-app-parity.md#essentials-user) | @@ -74,7 +74,7 @@ collaboration, mobile, AI). | Area | Breadth | Ready | Hours | Doc | |---|---|---|---|---| | Formula language and calculation | ~92% | ~72% | 20–30 | [target-app-parity.md](docs/target-app-parity.md#feature-areas) | -| Worksheet functions | 95.8% measured | ~82% | 18–30 | [function-parity.md](docs/function-parity.md) | +| Worksheet functions | 96.0% measured | ~82% | 18–30 | [function-parity.md](docs/function-parity.md) | | Editing, navigation, clipboard, fill | ~90% | ~70% | 15–25 | [ui-parity.md](docs/ui-parity.md) | | Cell formatting, styles, conditional formatting | ~95% | ~80% | 8–14 | [target-app-parity.md](docs/target-app-parity.md#feature-areas) | | Sort, filter, tables, validation, outline | ~82% | ~70% | 10–16 | [target-app-parity.md](docs/target-app-parity.md#feature-areas) | @@ -131,6 +131,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | What landed | |---|---| +| 2026-10-11 | TRIMRANGE: ranges and arrays without blank outer rows and columns (#296) | | 2026-10-10 | Ribbon translated into Japanese, Chinese, Korean, Brazilian Portuguese and Russian with system-language default (#31, #104, #82); CJK/Thai/Hebrew font fallback (#38); clear messages for encrypted and `.xls` files (#17); smooth line charts (#62). Progress docs restructured to the craftrules standard; full re-measure (this page). Initial ribbon key tips (#43), in-place text box editing (#84), copy as HTML table (#36). ODS and XLSB data import (#100, #101); recalc linear in the number of range formulas (#66); RISC-V Linux build; Flatpak repository on Pages; web cell-commit trap fixed | | 2026-10-09 | Community fixes: spill range operator `A1#`, sheet-scoped names, protection refuses locked-cell edits, Insert/Delete Cells adjust references, `DATE(1900,2,29)`, SUMIF sizing, `ROUND` at 15 digits, `_xlpm.` LAMBDA parameters in XLSX, vertical scrollbar, system appearance, border previews; v0.4.0 | | 2026-10-08 | v0.2.0 and v0.3.0 releases; Flatpak, AppImage zsync, branded DMG; desktop log file; DirectX 12 default on Windows | @@ -142,6 +143,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Worksheet functions 502 / 523 (TRIMRANGE); progress log row | | 2026-10-10 | minor | Readiness-by-audience table with hours to ~95% per audience; full number unchanged (additive weighted sum) | | 2026-10-10 | minor | Added Mainstream practitioner (~50%) and Essentials user (~63%); languages updated for the 10-10 merges (ribbon in ja, zh, ko, pt-BR, ru); localization 5%→10%; full number re-checked (51.6, unchanged) | | 2026-10-10 | minor | Checked against the alpha gate (six core workflows pass): stays alpha | diff --git a/crates/calc/src/eval.rs b/crates/calc/src/eval.rs index eff659d..996dec5 100644 --- a/crates/calc/src/eval.rs +++ b/crates/calc/src/eval.rs @@ -664,6 +664,7 @@ impl<'h> Evaluator<'h> { "INDIRECT" => self.indirect(args), "INDEX" => self.index(args), "CELL" => self.cell_info(args), + "TRIMRANGE" => self.trimrange(args), "SUBTOTAL" | "AGGREGATE" => self.subtotal(name, args), "TEXT" => { if args.len() != 2 { @@ -1455,6 +1456,7 @@ fn is_special(n: &str) -> bool { | "INDIRECT" | "INDEX" | "CELL" + | "TRIMRANGE" | "SUBTOTAL" | "AGGREGATE" | "TEXT" @@ -1492,6 +1494,7 @@ pub const SPECIAL_FUNCTIONS: &[&str] = &[ "INDIRECT", "INDEX", "CELL", + "TRIMRANGE", "SUBTOTAL", "AGGREGATE", "TEXT", diff --git a/crates/calc/src/lib.rs b/crates/calc/src/lib.rs index 6668b44..27c9e19 100644 --- a/crates/calc/src/lib.rs +++ b/crates/calc/src/lib.rs @@ -5,6 +5,7 @@ pub mod eval; pub mod recalc; +mod trimrange; pub use eval::{Area, SPECIAL_FUNCTIONS, is_known_function}; pub use recalc::{Calc, Key, evaluate, now_serial}; diff --git a/crates/calc/src/tests.rs b/crates/calc/src/tests.rs index d1e8576..d4ea999 100644 --- a/crates/calc/src/tests.rs +++ b/crates/calc/src/tests.rs @@ -467,3 +467,41 @@ fn sumif_resized_sum_range_on_another_sheet_and_through_a_name() { assert_eq!(t.num("C1"), 10.0); assert_eq!(t.num("C2"), 5.0); } + +#[test] +fn trimrange() { + let mut t = T::new(); + t.set("C3", "1"); + t.set("D5", "2"); + let cases = [ + ("J1", "=ROWS(TRIMRANGE(A1:F10))", 3.0), + ("J2", "=COLUMNS(TRIMRANGE(A1:F10))", 2.0), + ("J3", "=ROWS(TRIMRANGE(A1:F10,2))", 5.0), + ("J4", "=ROWS(TRIMRANGE(A1:F10,1,0))", 8.0), + ("J5", "=COLUMNS(TRIMRANGE(A1:F10,1,0))", 6.0), + ("J6", "=ROWS(TRIMRANGE(A1:F10,0,0))", 10.0), + ("J7", "=SUM(TRIMRANGE(C:C))", 1.0), + ("J8", "=ROWS(TRIMRANGE(C:C))", 1.0), + ("J9", "=ROWS(TRIMRANGE(C:C,2))", 3.0), + ("J10", "=MIN(ROW(TRIMRANGE(A1:F10)))", 3.0), + ("J11", "=COLUMNS(TRIMRANGE({1,2;3,4}))", 2.0), + ]; + for (at, f, _) in cases { + t.set(at, f); + } + for (at, f, want) in cases { + assert_eq!(t.num(at), want, "{f}"); + } + // The result spills as the trimmed block and follows edits. + t.set("L1", "=TRIMRANGE(A1:F10)"); + assert_eq!((t.num("L1"), t.num("M3")), (1.0, 2.0)); + t.set("E6", "3"); + assert_eq!((t.num("J1"), t.num("J2")), (4.0, 3.0)); + // Errors: a bad trim code, nothing but blanks, several areas. + t.set("K1", "=TRIMRANGE(A1:F10,4)"); + assert_eq!(t.get("K1"), Value::Error(CellError::Value)); + t.set("K2", "=TRIMRANGE(Q1:Q9)"); + assert_eq!(t.get("K2"), Value::Error(CellError::Calc)); + t.set("K3", "=TRIMRANGE((A1:B2,C3:D4))"); + assert_eq!(t.get("K3"), Value::Error(CellError::Value)); +} diff --git a/crates/calc/src/trimrange.rs b/crates/calc/src/trimrange.rs new file mode 100644 index 0000000..4810277 --- /dev/null +++ b/crates/calc/src/trimrange.rs @@ -0,0 +1,108 @@ +//! TRIMRANGE: a range or array without its blank outer rows and columns (Microsoft's +//! documented semantics: trim_rows / trim_cols 0 none, 1 leading, 2 trailing, 3 both). + +use gridcraft_core::{Array, CellError, CellRef, RangeRef, Value}; +use gridcraft_formula::Expr; + +use crate::eval::{Area, Ev, Evaluator, MAX_CELLS}; + +/// (leading, trailing) for a trim_rows / trim_cols code. +fn mode(code: f64) -> Result<(bool, bool), CellError> { + match code.trunc() { + 0.0 => Ok((false, false)), + 1.0 => Ok((true, false)), + 2.0 => Ok((false, true)), + 3.0 => Ok((true, true)), + _ => Err(CellError::Value), + } +} + +/// Rows and columns (first, last) holding a non-blank value in a row-major grid. +fn bounds(values: &[Value], width: usize) -> Option<((usize, usize), (usize, usize))> { + let mut b: Option<((usize, usize), (usize, usize))> = None; + for (i, v) in values.iter().enumerate() { + if matches!(v, Value::Empty) { + continue; + } + let (r, c) = (i / width.max(1), i % width.max(1)); + b = Some(match b { + None => ((r, r), (c, c)), + Some(((r0, r1), (c0, c1))) => ((r0.min(r), r1.max(r)), (c0.min(c), c1.max(c))), + }); + } + b +} + +/// The kept span of `0..len` given the non-blank span and the trim flags. +fn keep(len: usize, nonblank: (usize, usize), (lead, trail): (bool, bool)) -> (usize, usize) { + (if lead { nonblank.0 } else { 0 }, if trail { nonblank.1 } else { len.saturating_sub(1) }) +} + +impl Evaluator<'_> { + pub(crate) fn trimrange(&mut self, args: &[Expr]) -> Ev { + match self.trimrange_inner(args) { + Ok(ev) => ev, + Err(e) => Ev::V(Value::Error(e)), + } + } + + fn trim_mode(&mut self, args: &[Expr], i: usize) -> Result<(bool, bool), CellError> { + match args.get(i) { + None | Some(Expr::Missing) => Ok((true, true)), + Some(e) => match self.value(e).scalar() { + Value::Error(e) => Err(e), + v => mode(v.to_number()?), + }, + } + } + + fn trimrange_inner(&mut self, args: &[Expr]) -> Result { + let first = match args { + [first] | [first, _] | [first, _, _] => first, + _ => return Err(CellError::Value), + }; + let rows = self.trim_mode(args, 1)?; + let cols = self.trim_mode(args, 2)?; + let none = rows == (false, false) && cols == (false, false); + match self.eval(first) { + Ev::R(areas) => { + let [a] = areas.as_slice() else { return Err(CellError::Value) }; + if none { + return Ok(Ev::R(vec![*a])); + } + // Cells past the used range are blank: only the used part is read. + let part = self.trim(a); + let (h, w) = (part.height() as usize, part.width() as usize); + if (h as u64) * (w as u64) > MAX_CELLS { + return Err(CellError::Num); + } + let values = self.host.range_values(a.sheet, part); + let ((r0, r1), (c0, c1)) = bounds(&values, w).ok_or(CellError::Calc)?; + let (h_all, w_all) = (a.range.height() as usize, a.range.width() as usize); + let (kr0, kr1) = keep(h_all, (r0, r1), rows); + let (kc0, kc1) = keep(w_all, (c0, c1), cols); + let at = |r: usize, c: usize| CellRef::new(a.range.start.row.saturating_add(r as u32), a.range.start.col.saturating_add(c as u32)); + Ok(Ev::R(vec![Area { sheet: a.sheet, range: RangeRef::new(at(kr0, kc0), at(kr1, kc1)) }])) + } + Ev::V(Value::Array(arr)) => { + if none { + return Ok(Ev::V(Value::Array(arr))); + } + let ((r0, r1), (c0, c1)) = bounds(&arr.data, arr.cols).ok_or(CellError::Calc)?; + let (kr0, kr1) = keep(arr.rows, (r0, r1), rows); + let (kc0, kc1) = keep(arr.cols, (c0, c1), cols); + let mut data = Vec::with_capacity((kr1 + 1 - kr0) * (kc1 + 1 - kc0)); + for r in kr0..=kr1 { + for c in kc0..=kc1 { + data.push(arr.get(r, c).cloned().unwrap_or(Value::Empty)); + } + } + Array::new(kr1 + 1 - kr0, kc1 + 1 - kc0, data).map(|a| Ev::V(Value::from(a))).ok_or(CellError::Value) + } + Ev::V(Value::Error(e)) => Err(e), + Ev::V(Value::Empty) if !none => Err(CellError::Calc), + Ev::V(v) => Ok(Ev::V(v)), + Ev::L(_) => Err(CellError::Value), + } + } +} diff --git a/crates/engine/src/cmd/formulas.rs b/crates/engine/src/cmd/formulas.rs index f3576e4..6992579 100644 --- a/crates/engine/src/cmd/formulas.rs +++ b/crates/engine/src/cmd/formulas.rs @@ -185,6 +185,7 @@ pub fn function_list() -> Vec { ("SHEET", "Information", "SHEET([value])", "Sheet number."), ("SHEETS", "Information", "SHEETS([reference])", "Number of sheets."), ("AREAS", "Lookup", "AREAS(reference)", "Number of areas in a reference."), + ("TRIMRANGE", "Lookup", "TRIMRANGE(range, [trim_rows], [trim_cols])", "A range or array without its blank outer rows and columns."), ("ISOMITTED", "Information", "ISOMITTED(argument)", "TRUE when a LAMBDA argument was left out."), ]; for (n, c, sig, d) in specials { diff --git a/crates/xlsx/src/tables.rs b/crates/xlsx/src/tables.rs index 61f0b7c..aac7857 100644 --- a/crates/xlsx/src/tables.rs +++ b/crates/xlsx/src/tables.rs @@ -231,6 +231,7 @@ const FUTURE_FUNCTIONS: &[&str] = &[ "TEXTSPLIT", "TOCOL", "TOROW", + "TRIMRANGE", "UNICHAR", "UNICODE", "UNIQUE", @@ -322,6 +323,7 @@ mod tests { fn functions() { assert_eq!(file_function_name("xlookup").as_deref(), Some("_xlfn.XLOOKUP")); assert_eq!(file_function_name("FILTER").as_deref(), Some("_xlfn._xlws.FILTER")); + assert_eq!(file_function_name("TRIMRANGE").as_deref(), Some("_xlfn.TRIMRANGE")); assert_eq!(file_function_name("SUM"), None); assert_eq!(builtin_format(14), Some("m/d/yyyy")); assert_eq!(builtin_id("0.00%"), Some(10)); diff --git a/docs/function-parity.md b/docs/function-parity.md index 8ff9d49..359c001 100644 --- a/docs/function-parity.md +++ b/docs/function-parity.md @@ -1,6 +1,6 @@ # Function parity: worksheet functions and formulas -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (created; measured against Microsoft's published Excel function list) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (TRIMRANGE added) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 Excel's worksheet functions, category by category, against GridCraft's function library. Part of [target-app-parity.md](target-app-parity.md); gaps feed [gaps.md](gaps.md). @@ -12,7 +12,7 @@ Excel's worksheet functions, category by category, against GridCraft's function them), plus 14 text functions the page lists under combined entries (FIND/FINDB, LEFT/LEFTB, LEN/LENB, MID/MIDB, REPLACE/REPLACEB, RIGHT/RIGHTB, SEARCH/SEARCHB), BETA.INV, and COPILOT and PY (Microsoft 365 functions not on that page when fetched). -- **Ours (501):** every function spec in `crates/functions/src/*.rs` (`f!("NAME", …)`) plus the +- **Ours (502):** every function spec in `crates/functions/src/*.rs` (`f!("NAME", …)`) plus the evaluator's special forms in `crates/calc/src/eval.rs` (`SPECIAL_FUNCTIONS`: IF, IFS, CHOOSE, OFFSET, INDIRECT, INDEX, SUBTOTAL, AGGREGATE, TEXT, LET, LAMBDA, MAP, REDUCE, SCAN, BYROW, BYCOL, MAKEARRAY…), excluding the test-only `ADD2`. Every one is an Excel name. @@ -23,7 +23,7 @@ Excel's worksheet functions, category by category, against GridCraft's function ## Coverage -**Measured: 501 / 523 by name (95.8%); 492 / 523 working (94.1%).** +**Measured: 502 / 523 by name (96.0%); 493 / 523 working (94.3%).** | Category | Excel | Ours | Working | % working | Missing | |---|---|---|---|---|---| @@ -32,7 +32,7 @@ Excel's worksheet functions, category by category, against GridCraft's function | Financial | 55 | 51 | 51 | 93% | ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD | | Engineering | 54 | 54 | 54 | 100% | — | | Text | 50 | 47 | 47 | 94% | BAHTTEXT, DETECTLANGUAGE, TRANSLATE | -| Lookup and reference | 40 | 36 | 36 | 90% | GETPIVOTDATA, IMAGE, RTD, TRIMRANGE | +| Lookup and reference | 40 | 37 | 37 | 93% | GETPIVOTDATA, IMAGE, RTD | | Compatibility | 40 | 40 | 40 | 100% | — | | Date and time | 25 | 25 | 25 | 100% | — | | Information | 22 | 21 | 21 | 95% | STOCKHISTORY | @@ -42,10 +42,10 @@ Excel's worksheet functions, category by category, against GridCraft's function | Web | 3 | 3 | 1 | 33% | (WEBSERVICE, FILTERXML are stubs; ENCODEURL works) | | Add-in and Automation | 3 | 0 | 0 | 0% | CALL, EUROCONVERT, REGISTER.ID | | AI and Python | 2 | 0 | 0 | 0% | COPILOT, PY | -| **Total** | **523** | **501** | **492** | **94.1%** | 22 missing, 9 stubs | +| **Total** | **523** | **502** | **493** | **94.3%** | 21 missing, 9 stubs | By the Excel version that introduced them (working / total): before 2010 342 / 360, 2010 -56 / 56, 2013 49 / 51, 2016 1 / 5, 2019 6 / 6, 2021 11 / 11, 2024 22 / 23, Microsoft 365 5 / 11. +56 / 56, 2013 49 / 51, 2016 1 / 5, 2019 6 / 6, 2021 11 / 11, 2024 22 / 23, Microsoft 365 6 / 11. Dynamic-array and LAMBDA-era functions (FILTER, SORT, UNIQUE, XLOOKUP, VSTACK, TAKE, GROUPBY, PIVOTBY, REGEXTEST…) are all present. @@ -56,7 +56,8 @@ PIVOTBY, REGEXTEST…) are all present. | FORECAST.ETS family (4) | yes | 4–6 h | AAA exponential smoothing with seasonality detection; needed for Forecast Sheet too | | ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD | yes | 2–4 h | Odd first/last period bond maths | | GETPIVOTDATA | yes | 2–3 h | Needs the pivot engine's result cache; also the "Generate GetPivotData" option | -| TRIMRANGE, PERCENTOF, BAHTTEXT | yes | 1–2 h | TRIMRANGE also implies the `.:` trim-ref operators | +| PERCENTOF, BAHTTEXT | yes | 1–2 h | | +| Trim references (`A1.:.E10`, `A1:.E10`, `A1.:E10`) | yes | 1–2 h | TRIMRANGE works; the operators need lexer support (`.` is a name character) | | IMAGE | yes | 3–5 h | Pictures in cells (see PR #105); fetching URLs needs a privacy decision | | EUROCONVERT | yes | 0.5 h | Fixed conversion table | | CUBE* (7), WEBSERVICE, FILTERXML | partly | 4–8 h | FILTERXML can work offline; CUBE needs an OLAP/data-model source | @@ -97,4 +98,5 @@ workbooks; results checked in as data, not Excel files) and fixing what it finds | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | TRIMRANGE added (the trim-reference operators not yet): 502 / 523 by name, 493 working | | 2026-10-10 | major | Created: 501 / 523 by name, 492 working, per category, against Microsoft's published list | diff --git a/docs/gaps.md b/docs/gaps.md index aa752b8..e5ae6be 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (created from the full re-measure against Excel for Mac 16.113.4 and the issue tracker) · **Target:** Microsoft Excel (Microsoft 365) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (§15: TRIMRANGE landed) · **Target:** Microsoft Excel (Microsoft 365) Every known shortfall against Excel, one entry each, ranked by user impact (what blocks someone using GridCraft for real work first). This is the work list: pick from the top, check the open @@ -106,7 +106,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha- #143). **Estimate:** 12–20 h. ### 15. Remaining functions and an oracle suite (F) -- 22 missing, 9 stubs; no Excel-computed oracle corpus; #137, #138. **Estimate:** 18–30 h. +- 21 missing, 9 stubs; trim-reference operators (`A1:.E10`) not parsed; no Excel-computed oracle corpus; #137, #138. **Estimate:** 18–30 h. [function-parity.md](function-parity.md) ### 16. Collaboration and review (F) @@ -149,6 +149,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha- | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | §15: TRIMRANGE added (21 functions missing; trim-reference operators still missing) | | 2026-10-10 | minor | Updated for the 10-10 merges (#17, #31, #38, #62, #82, #104, #116) and the new chart resize bug (#190) | | 2026-10-10 | minor | Noted that no gap blocks the alpha gate | | 2026-10-10 | major | Created: 24 ranked gaps with evidence, from the full re-measure and the 60 open issues | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2e5ed01..eadf128 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Target-app parity: GridCraft vs Microsoft Excel -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first full assessment; re-measure against Excel for Mac 16.113.4, replacing the estimates that lived in ROADMAP.md) · **Target:** Microsoft Excel (Microsoft 365) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (worksheet functions 502 / 523: TRIMRANGE) · **Target:** Microsoft Excel (Microsoft 365) This is the authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it, [`gaps.md`](gaps.md) lists every shortfall, and the area checklists hold the detail: @@ -14,7 +14,7 @@ generated ribbon checklist is [`parity-checklist.md`](parity-checklist.md). |---|---|---| | Feature breadth (weighted by use) | **~75%** | estimated from the area table below; anchored on two measurements | | Ribbon and menu catalog | **258 / 290 (89.0%)** | measured: `cargo xtask parity` (`crates/engine/src/catalog.rs` ∩ registered command ids) | -| Worksheet functions, by name | **501 / 523 (95.8%)** | measured: registered functions vs Microsoft's published function list (method below) | +| Worksheet functions, by name | **502 / 523 (96.0%)** | measured: registered functions vs Microsoft's published function list (method below) | | Worksheet functions that do real work | **492 / 523 (94.1%)** | measured: excludes 9 stubs (7 CUBE functions, WEBSERVICE, FILTERXML) | | **Ready for real work** | **~50%** | estimated; written weights below (51.6 computed) | | **Mainstream practitioner** | **~50%** | estimated; [method](#mainstream-practitioner) (69.5 depth × 0.90 × 0.92 × 0.88 = 50.6) | @@ -53,7 +53,7 @@ long `.xls` track, so about half parallelizes (≈30–50 wall-clock hours with LEFT/LEFTB, LEN/LENB, MID/MIDB, REPLACE/REPLACEB, RIGHT/RIGHTB, SEARCH/SEARCHB, BETA.INV) and COPILOT and PY: **523 functions**. Ours: every `f!(…)` spec in `crates/functions/src/*.rs` plus `SPECIAL_FUNCTIONS` in `crates/calc/src/eval.rs` (evaluator special forms), minus the test-only - `ADD2`: **501**, all of them Excel names. + `ADD2`: **502**, all of them Excel names. - **Ribbon catalog:** `crates/engine/src/catalog.rs` (290 unique ids across 15 tabs) against the 313 command ids registered in `crates/engine/src/cmd/*.rs`, recomputed from source by script and identical to the committed `parity-checklist.md`. The catalog is our own transcription of @@ -80,7 +80,7 @@ performance 8, stability 8, localization 4, hardware 2, platforms 2, ecosystem 2 | Area | Weight | Breadth | Ready | Hours | Evidence | |---|---|---|---|---|---| | Formula language and calculation | 18 | ~92% | ~72% | 20–30 | A1/R1C1 parser, reference adjustment, dependency-graph recalc, dynamic arrays and `#SPILL!`, `A1#`, LET/LAMBDA and helpers, structured references, names, 3-D references. Open correctness bugs: 15-digit equality (#57), stale spills after structural edits and cleared blockers (#133, #139), 3-D span endpoint deletion (#140), sheet rename and qualified names (#134). Single-threaded | -| Worksheet functions | 12 | 95.8% (measured) | ~82% | 18–30 | 501 / 523 names, 492 working. Missing: FORECAST.ETS family, ODDF*/ODDL*, GETPIVOTDATA, TRIMRANGE, PERCENTOF, BAHTTEXT, IMAGE plus cloud functions. No conformance suite against Excel-computed oracles; edge-case bugs keep surfacing (#137, #138). [function-parity.md](function-parity.md) | +| Worksheet functions | 12 | 96.0% (measured) | ~82% | 18–30 | 502 / 523 names, 493 working. Missing: FORECAST.ETS family, ODDF*/ODDL*, GETPIVOTDATA, trim-reference operators, PERCENTOF, BAHTTEXT, IMAGE plus cloud functions. No conformance suite against Excel-computed oracles; edge-case bugs keep surfacing (#137, #138). [function-parity.md](function-parity.md) | | Editing, navigation, clipboard, fill | 12 | ~90% | ~70% | 15–25 | Point mode, F4, AutoComplete, Paste Special, AutoFill, Flash Fill, Find/Replace, Go To Special. Bugs: rectangular drag-select (#28), monthly fill (#132), cut across sheets (#128), Insert Copied Cells missing (#21), key tips only on part of the Home tab (#43; #33, #49). [ui-parity.md](ui-parity.md) | | Cell formatting, styles, conditional formatting | 10 | ~95% | ~80% | 8–14 | Full number-format language, borders, 47 cell styles, 60 table styles, all CF rule kinds including data bars, colour scales and icon sets. Gaps: font weights beyond regular/bold (#168), system fonts list (#4), themes | | Sort, filter, tables, validation, outline | 8 | ~82% | ~70% | 10–16 | Multi-level sort, AutoFilter, Advanced Filter, tables with totals, validation, outline, subtotals, consolidate. Bugs: subtotal header (#131), validation edits (#129), table style panic caught (#130); no slicers on tables | @@ -277,7 +277,7 @@ The inventory carried over from the 2026-10-07 ROADMAP.md, updated for what land - Engine: A1/R1C1 formula parser with reference adjustment, dependency-graph recalculation with dynamic arrays (spill + `#SPILL!`), cycle detection, LET/LAMBDA/MAP/REDUCE/SCAN/BYROW/BYCOL/ MAKEARRAY, structured table references, defined names, INDIRECT/OFFSET/INDEX references, - 501 worksheet functions (478 at M0) (math, statistics and distributions, text and regex, lookup incl. + 502 worksheet functions (478 at M0) (math, statistics and distributions, text and regex, lookup incl. XLOOKUP/FILTER/SORT/UNIQUE/VSTACK, dates, financial incl. bonds, engineering incl. complex numbers and Bessel, database, information). - Number formats: Excel's full format-code language (sections, conditions, colours, dates and @@ -332,6 +332,7 @@ The inventory carried over from the 2026-10-07 ROADMAP.md, updated for what land | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Worksheet functions 502 / 523 (96.0%; 493 working): TRIMRANGE | | 2026-10-10 | minor | Readiness-by-audience table: hours to ~95% for each of the three numbers (full 650–1,050, mainstream 200–310, essentials 60–100). Full number confirmed as the additive weighted sum over the written dimension weights (no change) | | 2026-10-10 | minor | Added Mainstream practitioner (~50%) and Essentials user (~63%) with written weights and discounts, and user-evidence counts; re-checked the full number after the 10-10 merges (charts 38→30, localization 5→10; still 51.6, ~50%) | | 2026-10-10 | minor | Stage checked against the new alpha gate (six core workflows, all pass): stays alpha |