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:
Harrison Booth
2026-10-15 05:19:32 -05:00
co-authored by Claude Opus 5.5
parent e6b6b34970
commit 6db94d154a
4 changed files with 87 additions and 3 deletions
+3 -1
View File
@@ -2,7 +2,7 @@
**Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away **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 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. 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 | | 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 | 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: 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) | | 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 | | 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 | 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 | minor | Progress log: multi-area row/column delete (#758) |
| 2026-10-12 | trivial | Progress log: XLSX chart sheets kept on save (#727) | | 2026-10-12 | trivial | Progress log: XLSX chart sheets kept on save (#727) |
+7 -1
View File
@@ -1251,7 +1251,13 @@ impl<'h> Evaluator<'h> {
let area = usize::try_from((area_n as u64).saturating_sub(1)).ok(); 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 Some(a) = area.and_then(|i| areas.get(i)).copied() else { return err(CellError::Ref) };
let r = a.range; 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) }; 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 { if row as u64 > r.height() as u64 || col as u64 > r.width() as u64 {
return err(CellError::Ref); return err(CellError::Ref);
+75
View File
@@ -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] #[test]
fn errors_and_cycles() { fn errors_and_cycles() {
let mut t = T::new(); let mut t = T::new();
+2 -1
View File
@@ -1,6 +1,6 @@
# Function parity: worksheet functions and formulas # 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 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). [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 | | 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-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 | 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 | | 2026-10-11 | minor | Iterative calculation: circular references iterate up to Maximum Iterations / Maximum Change when enabled |