Merge pull request #297 from vangel76/feat/trimrange
Functions: TRIMRANGE (#296) # Conflicts: # README.md # ROADMAP.md # crates/xlsx/src/tables.rs # docs/function-parity.md # docs/gaps.md # docs/target-app-parity.md
This commit is contained in:
commit
90dcc2b329
|
|
@ -136,6 +136,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
|||
| 2026-10-11 | Odd-period bond functions ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD: Financial 55 / 55 (#275) |
|
||||
| 2026-10-11 | PERCENTOF and EUROCONVERT worksheet functions (#279) |
|
||||
| 2026-10-11 | BAHTTEXT: amounts in Thai words with Baht and Satang (#291) |
|
||||
| 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 |
|
||||
|
|
@ -150,6 +151,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
|||
| 2026-10-11 | minor | Worksheet functions 505 / 523 (ODD* bond functions); progress log row |
|
||||
| 2026-10-11 | minor | Worksheet functions 503 / 523 (PERCENTOF, EUROCONVERT); progress log row |
|
||||
| 2026-10-11 | minor | Worksheet functions 502 / 523 (BAHTTEXT); progress log row |
|
||||
| 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 |
|
||||
|
|
|
|||
|
|
@ -677,6 +677,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 {
|
||||
|
|
@ -1468,6 +1469,7 @@ fn is_special(n: &str) -> bool {
|
|||
| "INDIRECT"
|
||||
| "INDEX"
|
||||
| "CELL"
|
||||
| "TRIMRANGE"
|
||||
| "SUBTOTAL"
|
||||
| "AGGREGATE"
|
||||
| "TEXT"
|
||||
|
|
@ -1505,6 +1507,7 @@ pub const SPECIAL_FUNCTIONS: &[&str] = &[
|
|||
"INDIRECT",
|
||||
"INDEX",
|
||||
"CELL",
|
||||
"TRIMRANGE",
|
||||
"SUBTOTAL",
|
||||
"AGGREGATE",
|
||||
"TEXT",
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -486,3 +486,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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Ev, CellError> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -185,6 +185,7 @@ pub fn function_list() -> Vec<Json> {
|
|||
("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 {
|
||||
|
|
|
|||
|
|
@ -232,6 +232,7 @@ const FUTURE_FUNCTIONS: &[&str] = &[
|
|||
"TEXTSPLIT",
|
||||
"TOCOL",
|
||||
"TOROW",
|
||||
"TRIMRANGE",
|
||||
"UNICHAR",
|
||||
"UNICODE",
|
||||
"UNIQUE",
|
||||
|
|
@ -324,6 +325,7 @@ mod tests {
|
|||
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("PERCENTOF").as_deref(), Some("_xlfn.PERCENTOF"));
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ Excel's worksheet functions, category by category, against GridCraft's function
|
|||
PY (Microsoft 365 functions not on that page when fetched).
|
||||
- **Ours (505):** every function spec in `crates/functions/src/*.rs` (`f!("NAME", …)`) plus the
|
||||
- **Ours (503):** 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
|
||||
- **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,
|
||||
|
|
@ -59,6 +60,8 @@ PIVOTBY, REGEXTEST…) are all present.
|
|||
| GETPIVOTDATA | yes | 2–3 h | Needs the pivot engine's result cache; also the "Generate GetPivotData" option |
|
||||
| TRIMRANGE, BAHTTEXT | yes | 1–2 h | TRIMRANGE also implies the `.:` trim-ref operators |
|
||||
| TRIMRANGE, PERCENTOF | 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 |
|
||||
| CUBE* (7), WEBSERVICE, FILTERXML | partly | 4–8 h | FILTERXML can work offline; CUBE needs an OLAP/data-model source |
|
||||
| TRANSLATE, DETECTLANGUAGE, STOCKHISTORY, COPILOT, PY | no (cloud) | — | Microsoft cloud services; open equivalents only by owner decision |
|
||||
|
|
@ -101,4 +104,5 @@ workbooks; results checked in as data, not Excel files) and fixing what it finds
|
|||
| 2026-10-11 | minor | ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD added: Financial 55 / 55; 505 / 523 by name, 496 working |
|
||||
| 2026-10-11 | minor | PERCENTOF and EUROCONVERT added: 503 / 523 by name, 494 working |
|
||||
| 2026-10-11 | minor | BAHTTEXT added: 502 / 523 by name, 493 working |
|
||||
| 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 |
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha-
|
|||
- 18 missing, 9 stubs; no Excel-computed oracle corpus; #137, #138. **Estimate:** 16–26 h.
|
||||
- 20 missing, 9 stubs; no Excel-computed oracle corpus; #137, #138. **Estimate:** 17–29 h.
|
||||
- 21 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)
|
||||
|
|
@ -154,6 +155,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha-
|
|||
| 2026-10-11 | minor | §15: ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD added (18 functions missing) |
|
||||
| 2026-10-11 | minor | §15: PERCENTOF and EUROCONVERT added (20 functions missing) |
|
||||
| 2026-10-11 | minor | §15: BAHTTEXT added (21 functions missing) |
|
||||
| 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 |
|
||||
|
|
|
|||
|
|
@ -335,6 +335,7 @@ The inventory carried over from the 2026-10-07 ROADMAP.md, updated for what land
|
|||
| 2026-10-11 | minor | Worksheet functions 505 / 523 (96.6%; 496 working): ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD |
|
||||
| 2026-10-11 | minor | Worksheet functions 503 / 523 (96.2%; 494 working): PERCENTOF, EUROCONVERT |
|
||||
| 2026-10-11 | minor | Worksheet functions 502 / 523 (96.0%; 493 working): BAHTTEXT |
|
||||
| 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 |
|
||||
|
|
|
|||
Loading…
Reference in New Issue