mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
Merge PR #2392
This commit is contained in:
+3
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-22 · **Change:** trivial (progress log: formula hardening, #2163/#2162; previously minor: Page Setup paper sizes: JIS B4/B5/B6, the Japanese postcards and envelopes, and A6; long flat operator chains up to Excel's formula limit) · **Target:** Microsoft Excel (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-23 · **Change:** minor (INDEX with one index on a 2-D reference is #REF! as in Excel; earlier: Page Setup paper sizes: JIS B4/B5/B6, the Japanese postcards and envelopes, and A6; long flat operator chains up to Excel's formula limit) · **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.
|
||||
@@ -135,6 +135,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-23 | Calc: `INDEX(A1:C3,2)` (one index on a reference of several rows and columns) is `#REF!` as in Excel, instead of the whole row; `INDEX(A1:C3,2,)` still gives the row |
|
||||
| 2026-10-22 | Formula hardening: digits too many for a finite number are text, so `"1 <400 digits>/<400 digits>"` no longer coerces to NaN; a NaN column index in VLOOKUP, HLOOKUP and the D-functions is `#VALUE!` instead of an index underflow; REGEXREPLACE stops building a result as soon as it can only be too long (a worst case went from 1 GB to 7 MB) (#2163, #2162) |
|
||||
| 2026-10-21 | Page Setup paper sizes match Japanese Excel: `B4`/`B5`/`B6` are the JIS sizes (ISO B4/B5/B6 stay reachable by name), so a Japanese B5 worksheet prints at 182×257 mm; Japanese Postcard (100×148 mm), Double Postcard (148×200 mm), Envelope #3/#4 (120×235/90×205 mm), Kaku #2/#3 (240×332/216×277 mm) and A6 are added, both sizes reach the PDF page (`file.exportPdf`/`file.print` MediaBox) and the size survives an XLSX round-trip; the Page Layout Size menu lists them. `B4`/`B5` used to be the ISO sizes, which is changed here |
|
||||
| 2026-10-20 | Japanese interface catalog covers every key the Brazilian Portuguese catalog translates: the 398-label shared core plus 545 dialog, message, Backstage and menu strings (943 total, ~43% of ~2,200). A test parses `pt.rs`'s source, so the two catalogs stay in step |
|
||||
@@ -221,6 +222,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-23 | minor | Progress log: INDEX with one index on a 2-D reference is #REF! |
|
||||
| 2026-10-22 | trivial | Progress log: formula hardening (#2163, #2162) |
|
||||
| 2026-10-21 | minor | Page Setup paper sizes: JIS B4/B5/B6 (ISO by name), Japanese postcards and envelopes, A6; PDF page size follows, XLSX round-trips |
|
||||
| 2026-10-20 | minor | Dutch (nl) listed under other shipped languages; progress log row |
|
||||
|
||||
@@ -1301,7 +1301,13 @@ impl<'h> Evaluator<'h> {
|
||||
let area = usize::try_from((area_n as u64).saturating_sub(1)).ok();
|
||||
let Some(a) = area.and_then(|i| areas.get(i)).copied() else { return err(CellError::Ref) };
|
||||
let r = a.range;
|
||||
// A single row or column with one index picks along it.
|
||||
// A single row or column with one index picks along it. An area with several rows
|
||||
// and columns needs both: `INDEX(A1:C3,2)` is #REF! in Excel (an empty third
|
||||
// argument, `INDEX(A1:C3,2,)`, still means the whole row). Arrays differ:
|
||||
// `INDEX({1,2;3,4},2)` is the row.
|
||||
if two_args && r.height() > 1 && r.width() > 1 {
|
||||
return err(CellError::Ref);
|
||||
}
|
||||
let (row, col) = if two_args && r.height() == 1 { (1, row) } else { (row, col) };
|
||||
if row as u64 > r.height() as u64 || col as u64 > r.width() as u64 {
|
||||
return err(CellError::Ref);
|
||||
|
||||
@@ -191,6 +191,81 @@ fn index_area_number_overflow() {
|
||||
}
|
||||
}
|
||||
|
||||
/// INDEX with one index, as observed in Excel: a 2-D reference needs both indexes (#REF!), a
|
||||
/// column array gives a 1×1 array (TYPE 64), a row array or 1-D reference gives one value.
|
||||
#[test]
|
||||
fn index_with_one_index() {
|
||||
let mut t = T::new();
|
||||
for (i, v) in [10, 20, 30].iter().enumerate() {
|
||||
t.set(&format!("A{}", i + 1), &v.to_string());
|
||||
}
|
||||
let mut k = 1;
|
||||
for r in 5..=7 {
|
||||
for col in ["A", "B", "C"] {
|
||||
t.set(&format!("{col}{r}"), &k.to_string());
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
for (row, (formula, expected)) in [
|
||||
// Column arrays keep Excel's 1×1 array; row arrays and explicit columns give a value.
|
||||
("=INDEX({10;20;30},2)", 20.0),
|
||||
("=TYPE(INDEX({10;20;30},2))", 64.0),
|
||||
("=TYPE(INDEX(SEQUENCE(3),2))", 64.0),
|
||||
("=TYPE(INDEX({10;20;30},2,1))", 1.0),
|
||||
("=TYPE(INDEX({10,20,30},2))", 1.0),
|
||||
("=TYPE(INDEX(INDEX({10;20;30},2),1))", 1.0),
|
||||
("=SUM(INDEX({10;20;30},2))", 20.0),
|
||||
// 1-D references with one index are single-cell references.
|
||||
("=TYPE(INDEX(A1:A3,2))", 1.0),
|
||||
("=SUM(INDEX(A1:A3,1):INDEX(A1:A3,3))", 60.0),
|
||||
("=SUM(INDEX((A1:A3,A5:C7),2))", 20.0),
|
||||
// A 2-D array with one index is the row.
|
||||
("=SUM(INDEX({1,2;3,4},2))", 7.0),
|
||||
("=SUM(INDEX({1,2;3,4},0))", 10.0),
|
||||
// A 2-D reference with an empty (not omitted) column argument is the row.
|
||||
("=SUM(INDEX(A5:C7,2,))", 15.0),
|
||||
("=SUM(INDEX(A5:C7,2,,1))", 15.0),
|
||||
("=COLUMNS(INDEX((A1:A3,A5:C7),2,,2))", 3.0),
|
||||
("=INDEX(A5:C7,2,2)", 5.0),
|
||||
// A 2-D reference with one index is #REF!, and the error carries through.
|
||||
("=TYPE(INDEX(A5:C7,2))", 16.0),
|
||||
("=ISREF(INDEX(A5:C7,2))", 0.0),
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let at = format!("J{}", row * 4 + 1);
|
||||
t.set(&at, formula);
|
||||
let got = match t.get(&at) {
|
||||
Value::Bool(b) => f64::from(u8::from(b)),
|
||||
v => v.as_f64().unwrap_or_else(|| panic!("{formula} = {v:?}")),
|
||||
};
|
||||
assert_eq!(got, *expected, "{formula}");
|
||||
}
|
||||
for formula in [
|
||||
"=INDEX(A5:C7,2)",
|
||||
"=INDEX(A5:C7,0)",
|
||||
"=INDEX(A5:C7,1.9)",
|
||||
"=INDEX(A5:C7,)",
|
||||
"=INDEX(A5:B6,1)",
|
||||
"=INDEX(A5:C7,2)+1",
|
||||
"=SUM(INDEX(A5:C7,{1;2}))",
|
||||
"=ROWS(INDEX(INDEX(A5:C7,2),3))",
|
||||
"=SUM(A5:INDEX(A5:C7,2))",
|
||||
"=SUM(INDEX(A5:C7,2):C7)",
|
||||
] {
|
||||
t.set("L1", formula);
|
||||
assert_eq!(t.get("L1"), Value::Error(CellError::Ref), "{formula}");
|
||||
}
|
||||
t.set("L1", "=INDEX(A5:C7,{1;2})");
|
||||
assert_eq!([t.get("L1"), t.get("L2")], [Value::Error(CellError::Ref), Value::Error(CellError::Ref)]);
|
||||
t.set("L1", "=IFERROR(INDEX(A5:C7,2),\"e\")");
|
||||
assert_eq!(t.get("L1"), Value::from("e"));
|
||||
// A bad position is still #VALUE!.
|
||||
t.set("L1", "=INDEX(A5:C7,\"x\")");
|
||||
assert_eq!(t.get("L1"), Value::Error(CellError::Value));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_and_cycles() {
|
||||
let mut t = T::new();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Function parity: worksheet functions and formulas
|
||||
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-21 · **Change:** minor (SUBTOTAL and AGGREGATE cap the cells they visit, #2175) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-23 · **Change:** minor (INDEX with one index on a 2-D reference is #REF! as in Excel; earlier: SUBTOTAL and AGGREGATE cap the cells they visit, #2175) · **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).
|
||||
@@ -122,6 +122,7 @@ workbooks; results checked in as data, not Excel files) and fixing what it finds
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-23 | minor | INDEX with only a row index on a reference of several rows and columns (`INDEX(A1:C3,2)`, `INDEX(A1:C3,0)`) is `#REF!`, as observed in Excel, so it also fails as a range endpoint and inside other functions; an empty column argument (`INDEX(A1:C3,2,)`) still gives the row. Checked that a column array with one index stays a 1×1 array (`TYPE` 64), which is what Excel returns |
|
||||
| 2026-10-21 | minor | SUBTOTAL and AGGREGATE charge every argument against the evaluator's 16-million-cell limit before scanning; a value in XFD1048576 no longer makes `SUBTOTAL(9,A:XFD)` read the whole sheet (#2175) |
|
||||
| 2026-10-19 | minor | LET and LAMBDA names compare case-insensitively in any script, as in Excel: `=LET(dług,100,DŁUG*2)` is 200 (was #NAME?); also LET-bound lambdas called or passed by name (`żar(2)`, `MAP(…,Λ)`), `ISOMITTED`, and formula-bar alias translation (one shared `gridcraft_formula::same_name`, which matches `ß` with `ẞ` and keeps dotless `ı` apart from `i`) |
|
||||
| 2026-10-19 | minor | Long flat operator chains (#916): 4,096 operands (Excel's 8,192-character limit) no longer overflow the stack when entered, filled, adjusted or saved; chains past 64 operands are no longer saved as dynamic-array formulas; Spanish entry no longer refuses chains past 256 operands |
|
||||
|
||||
Reference in New Issue
Block a user