Skip to content

Fix silent drop of named LAMBDA / non-range defined names on round-trip#59

Closed
senoff wants to merge 1 commit into
protobi:masterfrom
senoff:fix/lambda-silent-drop
Closed

Fix silent drop of named LAMBDA / non-range defined names on round-trip#59
senoff wants to merge 1 commit into
protobi:masterfrom
senoff:fix/lambda-silent-drop

Conversation

@senoff
Copy link
Copy Markdown

@senoff senoff commented May 2, 2026

Problem

Modern Excel (2024+) lets users define reusable workbook-level formulas using LAMBDA:

Name Manager → New → Name: MyDouble, Refers To: =LAMBDA(x, x*2)

Cells can then call =MyDouble(5) directly. These named LAMBDAs live as <definedName> entries in xl/workbook.xml:

<definedName name="MyDouble">LAMBDA(x,x*2)</definedName>

ExcelJS currently silently drops every <definedName> whose value is not a valid range address. After one read/write cycle, all named LAMBDAs — and any other formula-type defined names — are gone with no error or warning. The workbook is quietly broken.

Root cause

DefinedNamesXform.extractRanges() splits the raw text on commas and passes each token through colCache.decodeEx(). For LAMBDA(x,x*2) the split produces ['LAMBDA(x', 'x*2)']. LAMBDA(x throws (correctly rejected), but x*2) does not throw — decodeEx is permissive enough to return a row-only object for it. The result is a ranges array with garbage, non-empty, so the formula text is never preserved and is lost on write.

Fix

Two targeted changes:

lib/xlsx/xform/book/defined-name-xform.js

  • extractRanges(): add a fast-path at the top — if the text contains (, it cannot be a range address list (parentheses are never valid in range addresses), so return [] immediately without attempting to split.
  • parseClose(): when extractRanges returns empty and the trimmed text is non-empty, store the raw text in a formula field on the model object.
  • render(): if the model has a formula field, write it verbatim; otherwise join ranges as before.

lib/doc/workbook.js

  • Add _formulaDefinedNames = [] alongside the existing _definedNames (CellMatrix-backed).
  • model getter: concatenate both arrays so formula-type names are included in the output fed to WorkbookXform.render().
  • model setter: split incoming definedNames — entries with a formula field go to _formulaDefinedNames, entries without go to the existing _definedNames path unchanged.

Normal range-based defined names, print areas, and print titles are completely unaffected.

Also includes the .prettierrc trailingComma: "all""es5" config correction (same fix as pending PR #57) to keep lint-staged hooks passing.

Tests

Unit (spec/unit/xlsx/xform/book/defined-name-xform.spec.js): two new cases — Named LAMBDA expression and Named LAMBDA with multiple parameters — covering render, renderIn, and parse. The existing String with something that looks like a range parse expectation is updated to include the now-correctly-preserved formula field.

Integration (spec/integration/pr/lambda-defined-name.spec.js): builds a synthetic workbook via JSZip with two LAMBDA defined names (MyDouble = LAMBDA(x,x*2), MyAdd = LAMBDA(x,y,x+y)) and one normal range name (NormalRange = Sheet1!$A$1:$B$2), writes through ExcelJS, reads back, and asserts all three survive verbatim. Three assertions: LAMBDA names present, range name present, exact LAMBDA body text unchanged.

Test counts after patch: 890 unit (was 884, +6), 203 integration (was 200, +3), 1 e2e — all passing.

DefinedNamesXform.extractRanges() split definedName values on commas and
passed each token through colCache.decodeEx(). Tokens that looked like valid
addresses (e.g. fragments of LAMBDA(x,x*2)) were silently kept; the rest
were silently dropped.  Any definedName whose value is not a pure range list
(LAMBDA, LET, OFFSET wrappers, etc.) was therefore irreversibly lost on the
first ExcelJS write.

Fix: add a fast-path in extractRanges() — if the text contains '(' it cannot
be a range address list and is returned as empty immediately.  parseClose()
then stores the raw text in a `formula` field instead.  render() writes that
field verbatim.  workbook.js keeps formula-type defined names in a separate
_formulaDefinedNames array (not in the CellMatrix-backed DefinedNames class)
and round-trips them through model get/set.

Also carry the .prettierrc trailingComma "all" → "es5" fix (same as PR protobi#57).

Tests: add unit cases for Named LAMBDA expression and Named LAMBDA with
multiple parameters (render + renderIn + parse), and an integration test
that builds a synthetic workbook via JSZip, injects two LAMBDA definedNames
plus one normal range name, writes/re-reads through ExcelJS, and asserts
all three survive verbatim.
@senoff
Copy link
Copy Markdown
Author

senoff commented May 3, 2026

Local test run on this branch — npm install --legacy-peer-deps && npm run test:unit && npm run test:integration:

Tier This PR Baseline (master @ 4c9e73c) Δ
test:unit 890 passing, 1 pending, 0 failing 884 passing, 1 pending, 0 failing +6 unit, +3 int (this PR adds tests)
test:integration 203 passing, 0 failing 200 passing, 0 failing +6 unit, +3 int (this PR adds tests)

No regressions vs baseline. Skipped test:end-to-end + test:jasmine (browser/grunt tiers) — same regression-catching surface for unit + integration on a feature branch like this one.

Posted because the tests.yml workflow at this repo hasn't auto-fired on this PR (GitHub's first-time-contributor approval gate). Just want to make the merge feel less risky if/when you get to it. Happy to wait for the official CI run if you'd prefer to flip the approval gate; this is purely supplementary.

@senoff
Copy link
Copy Markdown
Author

senoff commented May 7, 2026

Regenerated per AGENTS.md guidance — see new PR #71. Closing this one in favor of the regenerated version.

@senoff senoff closed this May 7, 2026
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