mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 02:36:48 -06:00
Calc: INDEX with one index on a 2-D reference is #REF!, as in Excel
INDEX(A1:C3,2) returned the whole second row. Excel gives #REF! when a
reference has several rows and columns and the column argument is
omitted, so the error also carries into ranges built from it
(A1:INDEX(A1:C3,2)), position arrays (INDEX(A1:C3,{1;2})) and outer
functions. An empty column argument (INDEX(A1:C3,2,)) still gives the
row, and arrays are unchanged: INDEX({1,2;3,4},2) is the row.
Observed in Excel that a column array with one index,
INDEX({10;20;30},2), is a 1x1 array (TYPE 64), which is what calc
already returned; the new test pins it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e6b6b34970
commit
6db94d154a
+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-12 · **Change:** minor (rows auto-fit oversized fonts on open and whole-row/column formatting) · **Target:** Microsoft Excel (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-15 · **Change:** minor (INDEX with one index on a 2-D reference is #REF! as in Excel) · **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.
|
||||
@@ -132,6 +132,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-15 | 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-12 | Rows auto-fit oversized fonts: opening a file and whole-row/column formatting grow non-custom rows to their tallest font (#833) |
|
||||
| 2026-10-12 | Number formats: parsing a format code is linear in its length (long runs of `[`, `,` or `/` no longer take quadratic time), and an unclosed `[` is a literal when splitting sections as well as when tokenizing (#810) |
|
||||
| 2026-10-12 | Number formats: a fraction denominator with 65,536 zeros no longer crashes; format codes are capped at 255 characters like Excel (longer codes display as General, `TEXT` gives `#VALUE!`) (#807) |
|
||||
@@ -193,6 +194,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-15 | minor | Progress log: INDEX with one index on a 2-D reference is #REF! |
|
||||
| 2026-10-12 | minor | Rows auto-fit oversized fonts on open and whole-row/column formatting (#833); progress log row |
|
||||
| 2026-10-12 | minor | Progress log: multi-area row/column delete (#758) |
|
||||
| 2026-10-12 | trivial | Progress log: XLSX chart sheets kept on save (#727) |
|
||||
|
||||
@@ -1251,7 +1251,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);
|
||||
|
||||
@@ -172,6 +172,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-11 · **Last updated:** 2026-10-12 · **Change:** minor (USDOLLAR added as a compatibility function; MIRR, COUNTIF and INDIRECT edge cases match Excel 365) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-15 · **Change:** minor (INDEX with one index on a reference of several rows and columns is #REF! as in Excel) · **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).
|
||||
@@ -108,6 +108,7 @@ workbooks; results checked in as data, not Excel files) and fixing what it finds
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-15 | 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-12 | minor | USDOLLAR added as a Compatibility function (not on Microsoft's list, so the counts don't change); MIRR with no positive flow is -1, COUNTIF's bare `"<="` / `">="` skip empty strings, INDIRECT with `a1` = FALSE gives `#REF!` for A1-style text (#738) |
|
||||
| 2026-10-11 | minor | TEXT / number formats fixed against Excel-cached results: fraction literals and zero fractions, `0` denominators zero padded, `?` pads the thousands separator, booleans use the text section (226 mismatches → 0 in 352 TEXT cells) |
|
||||
| 2026-10-11 | minor | Iterative calculation: circular references iterate up to Maximum Iterations / Maximum Change when enabled |
|
||||
|
||||
Reference in New Issue
Block a user