Skip to content

fix(eval): sheet-name resolution is case-insensitive (Excel parity) - #4

Open
rmdort wants to merge 1 commit into
structured-refsfrom
rmdort/case-insensitive-sheet-names
Open

fix(eval): sheet-name resolution is case-insensitive (Excel parity)#4
rmdort wants to merge 1 commit into
structured-refsfrom
rmdort/case-insensitive-sheet-names

Conversation

@rmdort

@rmdort rmdort commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary

Excel resolves sheet names case-insensitively (=data!A1 reaches a sheet named Data), but SheetRegistry was an exact-match map: a case-mismatched reference errored at ingest (→ #REF! downstream) or silently minted a phantom sheet on the plan.rs/id_for paths. The engine's own consumers already assume case-insensitive semantics (rnc's reverse-dependency index and rename-rewrite both fold case), so a case-variant orphan could loop forever: matched case-insensitively for reinstall, rejected case-sensitively on install.

Found while scoping the unknown-sheet-#REF! fix for the Athena rnc stability battery (rowsncolumns/spreadsheet#375 is the sibling change).

Change

SheetRegistry gains a case-folded secondary index (Unicode to_lowercase, matching normalize_name_key's folding for named ranges/tables), probed only after an exact-spelling miss:

  • get_id/id_for resolve case-variants to the existing sheet instead of erroring or minting phantoms; the ref then interns as SheetKey::Id, so reconstruction yields the display spelling and every downstream display-name lookup (arrow store, canonical hashing) is correct with no further edits
  • Exact-first keeps any pre-existing pair of sheets differing only by case resolving each to itself, byte-identical to today
  • remove/rename re-point a shared folded key at a surviving case-sibling; case-only renames (DataDATA) keep resolving throughout
  • rename's collision check stays exact-spelling — rejecting case-collisions Excel-style is a follow-up gated on callers surfacing the error (rnc currently discards it)

Tests

  • New SheetRegistry unit tests: exact priority, phantom-free id_for, remove/rename index sync, case-only rename, Unicode folding
  • New engine-level test: test_sheet_reference_resolution_is_case_insensitive (sibling of the unknown-sheet test)
  • Full formualizer-eval suite: 2417 pass; the 3 extended_coverage IMEXP/IMSIN/IMCOS failures are pre-existing last-ULP float-formatting platform diffs (identical on the base commit)

Excel resolves sheet names case-insensitively — =data!A1 reaches a sheet
named Data — but SheetRegistry was an exact-match HashMap, so a
case-mismatched reference either errored at ingest (Sheet not found ->
surfaced as #REF! downstream) or, on the plan/id_for paths, silently minted
a phantom sheet with the variant spelling. The engine's own consumers
already assume case-insensitive semantics (rnc's reverse-dependency index
and rename-rewrite both fold case), so a case-variant orphan could loop
forever: matched case-insensitively for reinstall, rejected case-sensitively
on install.

SheetRegistry gains a case-folded secondary index (Unicode to_lowercase,
matching normalize_name_key's folding for named ranges/tables) probed only
after an exact-spelling miss:
- get_id/id_for resolve case-variants to the existing sheet instead of
  erroring/minting phantoms; because the ref then interns as SheetKey::Id,
  reconstruction yields the display spelling and every downstream
  display-name lookup (arrow store, canonical hashing) is correct with no
  further changes
- exact-first keeps any pre-existing pair of sheets differing only by case
  resolving each to itself, byte-identical to before
- remove/rename keep the folded index in sync by re-pointing a shared folded
  key at a surviving case-sibling; case-only renames (Data -> DATA) keep
  resolving throughout
- rename's collision check stays exact-spelling — rejecting case-collisions
  is a follow-up gated on callers surfacing the error

Tests: SheetRegistry unit tests (exact priority, phantom-free id_for,
remove/rename index sync, case-only rename, Unicode folding) + an engine-
level resolution test. Full formualizer-eval suite: 2417 pass; the 3
extended_coverage IMEXP/IMSIN/IMCOS failures are pre-existing last-ULP
float formatting diffs on this platform (fail identically on the base
commit).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant