@cj-tech-master/excelts 9.2.1 → 9.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -2
- package/README_zh.md +29 -6
- package/dist/browser/index.browser.d.ts +1 -1
- package/dist/browser/index.browser.js +4 -0
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.js +4 -0
- package/dist/browser/modules/excel/cell.d.ts +17 -3
- package/dist/browser/modules/excel/cell.js +170 -22
- package/dist/browser/modules/excel/defined-names.d.ts +96 -1
- package/dist/browser/modules/excel/defined-names.js +411 -21
- package/dist/browser/modules/excel/image.d.ts +11 -0
- package/dist/browser/modules/excel/image.js +24 -1
- package/dist/browser/modules/excel/stream/workbook-reader.browser.d.ts +9 -3
- package/dist/browser/modules/excel/stream/workbook-reader.browser.js +14 -0
- package/dist/browser/modules/excel/stream/workbook-reader.d.ts +2 -1
- package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +39 -5
- package/dist/browser/modules/excel/stream/workbook-writer.browser.js +48 -1
- package/dist/browser/modules/excel/stream/workbook-writer.d.ts +3 -2
- package/dist/browser/modules/excel/stream/worksheet-reader.js +17 -1
- package/dist/browser/modules/excel/stream/worksheet-writer.d.ts +39 -6
- package/dist/browser/modules/excel/stream/worksheet-writer.js +45 -5
- package/dist/browser/modules/excel/table.js +15 -2
- package/dist/browser/modules/excel/types.d.ts +133 -2
- package/dist/browser/modules/excel/utils/col-cache.d.ts +1 -0
- package/dist/browser/modules/excel/utils/col-cache.js +15 -0
- package/dist/browser/modules/excel/utils/drawing-utils.d.ts +3 -3
- package/dist/browser/modules/excel/utils/drawing-utils.js +4 -0
- package/dist/browser/modules/excel/utils/external-link-formula.d.ts +76 -0
- package/dist/browser/modules/excel/utils/external-link-formula.js +208 -0
- package/dist/browser/modules/excel/utils/iterate-stream.d.ts +9 -3
- package/dist/browser/modules/excel/utils/iterate-stream.js +3 -1
- package/dist/browser/modules/excel/utils/ooxml-paths.d.ts +19 -0
- package/dist/browser/modules/excel/utils/ooxml-paths.js +37 -2
- package/dist/browser/modules/excel/utils/shared-strings.d.ts +8 -3
- package/dist/browser/modules/excel/utils/shared-strings.js +21 -2
- package/dist/browser/modules/excel/utils/workbook-protection.d.ts +30 -0
- package/dist/browser/modules/excel/utils/workbook-protection.js +30 -0
- package/dist/browser/modules/excel/workbook.browser.d.ts +257 -6
- package/dist/browser/modules/excel/workbook.browser.js +318 -34
- package/dist/browser/modules/excel/workbook.d.ts +1 -1
- package/dist/browser/modules/excel/worksheet.d.ts +3 -1
- package/dist/browser/modules/excel/worksheet.js +21 -2
- package/dist/browser/modules/excel/xlsx/rel-type.d.ts +15 -0
- package/dist/browser/modules/excel/xlsx/rel-type.js +16 -1
- package/dist/browser/modules/excel/xlsx/xform/book/defined-name-xform.d.ts +6 -5
- package/dist/browser/modules/excel/xlsx/xform/book/defined-name-xform.js +21 -86
- package/dist/browser/modules/excel/xlsx/xform/book/external-link-xform.d.ts +84 -0
- package/dist/browser/modules/excel/xlsx/xform/book/external-link-xform.js +330 -0
- package/dist/browser/modules/excel/xlsx/xform/book/external-reference-xform.d.ts +17 -0
- package/dist/browser/modules/excel/xlsx/xform/book/external-reference-xform.js +24 -0
- package/dist/browser/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.d.ts +3 -0
- package/dist/browser/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.js +11 -2
- package/dist/browser/modules/excel/xlsx/xform/book/workbook-protection-xform.d.ts +20 -0
- package/dist/browser/modules/excel/xlsx/xform/book/workbook-protection-xform.js +66 -0
- package/dist/browser/modules/excel/xlsx/xform/book/workbook-xform.js +38 -5
- package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +19 -1
- package/dist/browser/modules/excel/xlsx/xform/core/metadata-xform.d.ts +56 -0
- package/dist/browser/modules/excel/xlsx/xform/core/metadata-xform.js +158 -0
- package/dist/browser/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.d.ts +26 -0
- package/dist/browser/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +105 -0
- package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +3 -0
- package/dist/browser/modules/excel/xlsx/xform/drawing/drawing-xform.js +10 -2
- package/dist/browser/modules/excel/xlsx/xform/sheet/cell-xform.d.ts +1 -1
- package/dist/browser/modules/excel/xlsx/xform/sheet/cell-xform.js +166 -8
- package/dist/browser/modules/excel/xlsx/xform/sheet/data-validations-xform.js +1 -1
- package/dist/browser/modules/excel/xlsx/xform/sheet/ignored-errors-xform.d.ts +21 -0
- package/dist/browser/modules/excel/xlsx/xform/sheet/ignored-errors-xform.js +80 -0
- package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +9 -4
- package/dist/browser/modules/excel/xlsx/xform/style/border-xform.js +4 -1
- package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +172 -13
- package/dist/browser/modules/excel/xlsx/xlsx.browser.js +410 -20
- package/dist/browser/modules/excel/xlsx/xlsx.d.ts +7 -4
- package/dist/browser/modules/excel/xlsx/xlsx.js +4 -5
- package/dist/browser/modules/formula/compile/address-utils.d.ts +62 -0
- package/dist/browser/modules/formula/compile/address-utils.js +83 -0
- package/dist/browser/modules/formula/compile/binder.d.ts +42 -0
- package/dist/browser/modules/formula/compile/binder.js +487 -0
- package/dist/browser/modules/formula/compile/bound-ast.d.ts +230 -0
- package/dist/browser/modules/formula/compile/bound-ast.js +80 -0
- package/dist/browser/modules/formula/compile/compiled-formula.d.ts +137 -0
- package/dist/browser/modules/formula/compile/compiled-formula.js +383 -0
- package/dist/browser/modules/formula/compile/dependency-analysis.d.ts +93 -0
- package/dist/browser/modules/formula/compile/dependency-analysis.js +432 -0
- package/dist/browser/modules/formula/compile/structured-ref-utils.d.ts +93 -0
- package/dist/browser/modules/formula/compile/structured-ref-utils.js +136 -0
- package/dist/browser/modules/formula/default-syntax-probe.d.ts +79 -0
- package/dist/browser/modules/formula/default-syntax-probe.js +83 -0
- package/dist/browser/modules/formula/functions/_date-context.d.ts +4 -0
- package/dist/browser/modules/formula/functions/_date-context.js +29 -0
- package/dist/browser/modules/formula/functions/_shared.d.ts +121 -0
- package/dist/browser/modules/formula/functions/_shared.js +381 -0
- package/dist/browser/modules/formula/functions/conditional.d.ts +27 -0
- package/dist/browser/modules/formula/functions/conditional.js +343 -0
- package/dist/browser/modules/formula/functions/database.d.ts +37 -0
- package/dist/browser/modules/formula/functions/database.js +274 -0
- package/dist/browser/modules/formula/functions/date.d.ts +61 -0
- package/dist/browser/modules/formula/functions/date.js +855 -0
- package/dist/browser/modules/formula/functions/dynamic-array.d.ts +23 -0
- package/dist/browser/modules/formula/functions/dynamic-array.js +860 -0
- package/dist/browser/modules/formula/functions/engineering.d.ts +57 -0
- package/dist/browser/modules/formula/functions/engineering.js +1128 -0
- package/dist/browser/modules/formula/functions/financial.d.ts +202 -0
- package/dist/browser/modules/formula/functions/financial.js +2296 -0
- package/dist/browser/modules/formula/functions/lookup.d.ts +18 -0
- package/dist/browser/modules/formula/functions/lookup.js +886 -0
- package/dist/browser/modules/formula/functions/math.d.ts +114 -0
- package/dist/browser/modules/formula/functions/math.js +1406 -0
- package/dist/browser/modules/formula/functions/statistical.d.ts +193 -0
- package/dist/browser/modules/formula/functions/statistical.js +3390 -0
- package/dist/browser/modules/formula/functions/text.d.ts +86 -0
- package/dist/browser/modules/formula/functions/text.js +1845 -0
- package/dist/browser/modules/formula/host-registry.d.ts +53 -0
- package/dist/browser/modules/formula/host-registry.js +69 -0
- package/dist/browser/modules/formula/index.d.ts +39 -0
- package/dist/browser/modules/formula/index.js +49 -0
- package/dist/browser/modules/formula/install.d.ts +62 -0
- package/dist/browser/modules/formula/install.js +88 -0
- package/dist/browser/modules/formula/integration/apply-writeback-plan.d.ts +26 -0
- package/dist/browser/modules/formula/integration/apply-writeback-plan.js +210 -0
- package/dist/browser/modules/formula/integration/calculate-formulas-impl.d.ts +30 -0
- package/dist/browser/modules/formula/integration/calculate-formulas-impl.js +616 -0
- package/dist/browser/modules/formula/integration/calculate-formulas.d.ts +67 -0
- package/dist/browser/modules/formula/integration/calculate-formulas.js +68 -0
- package/dist/browser/modules/formula/integration/formula-instance.d.ts +64 -0
- package/dist/browser/modules/formula/integration/formula-instance.js +79 -0
- package/dist/browser/modules/formula/integration/workbook-adapter.d.ts +26 -0
- package/dist/browser/modules/formula/integration/workbook-adapter.js +324 -0
- package/dist/browser/modules/formula/integration/workbook-snapshot.d.ts +267 -0
- package/dist/browser/modules/formula/integration/workbook-snapshot.js +77 -0
- package/dist/browser/modules/formula/materialize/build-writeback-plan.d.ts +34 -0
- package/dist/browser/modules/formula/materialize/build-writeback-plan.js +473 -0
- package/dist/browser/modules/formula/materialize/spill-engine.d.ts +9 -0
- package/dist/browser/modules/formula/materialize/spill-engine.js +38 -0
- package/dist/browser/modules/formula/materialize/types.d.ts +179 -0
- package/dist/browser/modules/formula/materialize/types.js +29 -0
- package/dist/browser/modules/formula/materialize/writeback-plan.d.ts +167 -0
- package/dist/browser/modules/formula/materialize/writeback-plan.js +27 -0
- package/dist/browser/modules/formula/runtime/evaluator.d.ts +151 -0
- package/dist/browser/modules/formula/runtime/evaluator.js +2291 -0
- package/dist/browser/modules/formula/runtime/function-registry.d.ts +47 -0
- package/dist/browser/modules/formula/runtime/function-registry.js +840 -0
- package/dist/browser/modules/formula/runtime/values.d.ts +211 -0
- package/dist/browser/modules/formula/runtime/values.js +385 -0
- package/dist/browser/modules/formula/syntax/ast.d.ts +129 -0
- package/dist/browser/modules/formula/syntax/ast.js +28 -0
- package/dist/browser/modules/formula/syntax/parser.d.ts +18 -0
- package/dist/browser/modules/formula/syntax/parser.js +439 -0
- package/dist/browser/modules/formula/syntax/token-types.d.ts +153 -0
- package/dist/browser/modules/formula/syntax/token-types.js +59 -0
- package/dist/browser/modules/formula/syntax/tokenizer.d.ts +10 -0
- package/dist/browser/modules/formula/syntax/tokenizer.js +1074 -0
- package/dist/browser/modules/pdf/excel-bridge.js +9 -0
- package/dist/cjs/index.js +4 -0
- package/dist/cjs/modules/excel/cell.js +170 -22
- package/dist/cjs/modules/excel/defined-names.js +411 -21
- package/dist/cjs/modules/excel/image.js +24 -1
- package/dist/cjs/modules/excel/stream/workbook-reader.browser.js +14 -0
- package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +48 -1
- package/dist/cjs/modules/excel/stream/worksheet-reader.js +17 -1
- package/dist/cjs/modules/excel/stream/worksheet-writer.js +45 -5
- package/dist/cjs/modules/excel/table.js +15 -2
- package/dist/cjs/modules/excel/utils/col-cache.js +15 -0
- package/dist/cjs/modules/excel/utils/drawing-utils.js +4 -0
- package/dist/cjs/modules/excel/utils/external-link-formula.js +212 -0
- package/dist/cjs/modules/excel/utils/iterate-stream.js +3 -1
- package/dist/cjs/modules/excel/utils/ooxml-paths.js +42 -2
- package/dist/cjs/modules/excel/utils/shared-strings.js +21 -2
- package/dist/cjs/modules/excel/utils/workbook-protection.js +33 -0
- package/dist/cjs/modules/excel/workbook.browser.js +318 -34
- package/dist/cjs/modules/excel/worksheet.js +20 -1
- package/dist/cjs/modules/excel/xlsx/rel-type.js +16 -1
- package/dist/cjs/modules/excel/xlsx/xform/book/defined-name-xform.js +21 -86
- package/dist/cjs/modules/excel/xlsx/xform/book/external-link-xform.js +333 -0
- package/dist/cjs/modules/excel/xlsx/xform/book/external-reference-xform.js +27 -0
- package/dist/cjs/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.js +11 -2
- package/dist/cjs/modules/excel/xlsx/xform/book/workbook-protection-xform.js +69 -0
- package/dist/cjs/modules/excel/xlsx/xform/book/workbook-xform.js +38 -5
- package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +18 -0
- package/dist/cjs/modules/excel/xlsx/xform/core/metadata-xform.js +161 -0
- package/dist/cjs/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +108 -0
- package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +3 -0
- package/dist/cjs/modules/excel/xlsx/xform/drawing/drawing-xform.js +10 -2
- package/dist/cjs/modules/excel/xlsx/xform/sheet/cell-xform.js +166 -8
- package/dist/cjs/modules/excel/xlsx/xform/sheet/data-validations-xform.js +1 -1
- package/dist/cjs/modules/excel/xlsx/xform/sheet/ignored-errors-xform.js +83 -0
- package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +9 -4
- package/dist/cjs/modules/excel/xlsx/xform/style/border-xform.js +4 -1
- package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +408 -18
- package/dist/cjs/modules/excel/xlsx/xlsx.js +4 -5
- package/dist/cjs/modules/formula/compile/address-utils.js +89 -0
- package/dist/cjs/modules/formula/compile/binder.js +489 -0
- package/dist/cjs/modules/formula/compile/bound-ast.js +68 -0
- package/dist/cjs/modules/formula/compile/compiled-formula.js +387 -0
- package/dist/cjs/modules/formula/compile/dependency-analysis.js +437 -0
- package/dist/cjs/modules/formula/compile/structured-ref-utils.js +141 -0
- package/dist/cjs/modules/formula/default-syntax-probe.js +87 -0
- package/dist/cjs/modules/formula/functions/_date-context.js +33 -0
- package/dist/cjs/modules/formula/functions/_shared.js +396 -0
- package/dist/cjs/modules/formula/functions/conditional.js +354 -0
- package/dist/cjs/modules/formula/functions/database.js +288 -0
- package/dist/cjs/modules/formula/functions/date.js +883 -0
- package/dist/cjs/modules/formula/functions/dynamic-array.js +881 -0
- package/dist/cjs/modules/formula/functions/engineering.js +1183 -0
- package/dist/cjs/modules/formula/functions/financial.js +2348 -0
- package/dist/cjs/modules/formula/functions/lookup.js +902 -0
- package/dist/cjs/modules/formula/functions/math.js +1487 -0
- package/dist/cjs/modules/formula/functions/statistical.js +3488 -0
- package/dist/cjs/modules/formula/functions/text.js +1889 -0
- package/dist/cjs/modules/formula/host-registry.js +75 -0
- package/dist/cjs/modules/formula/index.js +58 -0
- package/dist/cjs/modules/formula/install.js +93 -0
- package/dist/cjs/modules/formula/integration/apply-writeback-plan.js +213 -0
- package/dist/cjs/modules/formula/integration/calculate-formulas-impl.js +619 -0
- package/dist/cjs/modules/formula/integration/calculate-formulas.js +71 -0
- package/dist/cjs/modules/formula/integration/formula-instance.js +82 -0
- package/dist/cjs/modules/formula/integration/workbook-adapter.js +327 -0
- package/dist/cjs/modules/formula/integration/workbook-snapshot.js +84 -0
- package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +475 -0
- package/dist/cjs/modules/formula/materialize/spill-engine.js +42 -0
- package/dist/cjs/modules/formula/materialize/types.js +32 -0
- package/dist/cjs/modules/formula/materialize/writeback-plan.js +28 -0
- package/dist/cjs/modules/formula/runtime/evaluator.js +2298 -0
- package/dist/cjs/modules/formula/runtime/function-registry.js +846 -0
- package/dist/cjs/modules/formula/runtime/values.js +385 -0
- package/dist/cjs/modules/formula/syntax/ast.js +8 -0
- package/dist/cjs/modules/formula/syntax/parser.js +440 -0
- package/dist/cjs/modules/formula/syntax/token-types.js +32 -0
- package/dist/cjs/modules/formula/syntax/tokenizer.js +1076 -0
- package/dist/cjs/modules/pdf/excel-bridge.js +9 -0
- package/dist/esm/index.browser.js +4 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/modules/excel/cell.js +170 -22
- package/dist/esm/modules/excel/defined-names.js +411 -21
- package/dist/esm/modules/excel/image.js +24 -1
- package/dist/esm/modules/excel/stream/workbook-reader.browser.js +14 -0
- package/dist/esm/modules/excel/stream/workbook-writer.browser.js +48 -1
- package/dist/esm/modules/excel/stream/worksheet-reader.js +17 -1
- package/dist/esm/modules/excel/stream/worksheet-writer.js +45 -5
- package/dist/esm/modules/excel/table.js +15 -2
- package/dist/esm/modules/excel/utils/col-cache.js +15 -0
- package/dist/esm/modules/excel/utils/drawing-utils.js +4 -0
- package/dist/esm/modules/excel/utils/external-link-formula.js +208 -0
- package/dist/esm/modules/excel/utils/iterate-stream.js +3 -1
- package/dist/esm/modules/excel/utils/ooxml-paths.js +37 -2
- package/dist/esm/modules/excel/utils/shared-strings.js +21 -2
- package/dist/esm/modules/excel/utils/workbook-protection.js +30 -0
- package/dist/esm/modules/excel/workbook.browser.js +318 -34
- package/dist/esm/modules/excel/worksheet.js +21 -2
- package/dist/esm/modules/excel/xlsx/rel-type.js +16 -1
- package/dist/esm/modules/excel/xlsx/xform/book/defined-name-xform.js +21 -86
- package/dist/esm/modules/excel/xlsx/xform/book/external-link-xform.js +330 -0
- package/dist/esm/modules/excel/xlsx/xform/book/external-reference-xform.js +24 -0
- package/dist/esm/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.js +11 -2
- package/dist/esm/modules/excel/xlsx/xform/book/workbook-protection-xform.js +66 -0
- package/dist/esm/modules/excel/xlsx/xform/book/workbook-xform.js +38 -5
- package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +19 -1
- package/dist/esm/modules/excel/xlsx/xform/core/metadata-xform.js +158 -0
- package/dist/esm/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +105 -0
- package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +3 -0
- package/dist/esm/modules/excel/xlsx/xform/drawing/drawing-xform.js +10 -2
- package/dist/esm/modules/excel/xlsx/xform/sheet/cell-xform.js +166 -8
- package/dist/esm/modules/excel/xlsx/xform/sheet/data-validations-xform.js +1 -1
- package/dist/esm/modules/excel/xlsx/xform/sheet/ignored-errors-xform.js +80 -0
- package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +9 -4
- package/dist/esm/modules/excel/xlsx/xform/style/border-xform.js +4 -1
- package/dist/esm/modules/excel/xlsx/xlsx.browser.js +410 -20
- package/dist/esm/modules/excel/xlsx/xlsx.js +4 -5
- package/dist/esm/modules/formula/compile/address-utils.js +83 -0
- package/dist/esm/modules/formula/compile/binder.js +487 -0
- package/dist/esm/modules/formula/compile/bound-ast.js +80 -0
- package/dist/esm/modules/formula/compile/compiled-formula.js +383 -0
- package/dist/esm/modules/formula/compile/dependency-analysis.js +432 -0
- package/dist/esm/modules/formula/compile/structured-ref-utils.js +136 -0
- package/dist/esm/modules/formula/default-syntax-probe.js +83 -0
- package/dist/esm/modules/formula/functions/_date-context.js +29 -0
- package/dist/esm/modules/formula/functions/_shared.js +381 -0
- package/dist/esm/modules/formula/functions/conditional.js +343 -0
- package/dist/esm/modules/formula/functions/database.js +274 -0
- package/dist/esm/modules/formula/functions/date.js +855 -0
- package/dist/esm/modules/formula/functions/dynamic-array.js +860 -0
- package/dist/esm/modules/formula/functions/engineering.js +1128 -0
- package/dist/esm/modules/formula/functions/financial.js +2296 -0
- package/dist/esm/modules/formula/functions/lookup.js +886 -0
- package/dist/esm/modules/formula/functions/math.js +1406 -0
- package/dist/esm/modules/formula/functions/statistical.js +3390 -0
- package/dist/esm/modules/formula/functions/text.js +1845 -0
- package/dist/esm/modules/formula/host-registry.js +69 -0
- package/dist/esm/modules/formula/index.js +49 -0
- package/dist/esm/modules/formula/install.js +88 -0
- package/dist/esm/modules/formula/integration/apply-writeback-plan.js +210 -0
- package/dist/esm/modules/formula/integration/calculate-formulas-impl.js +616 -0
- package/dist/esm/modules/formula/integration/calculate-formulas.js +68 -0
- package/dist/esm/modules/formula/integration/formula-instance.js +79 -0
- package/dist/esm/modules/formula/integration/workbook-adapter.js +324 -0
- package/dist/esm/modules/formula/integration/workbook-snapshot.js +77 -0
- package/dist/esm/modules/formula/materialize/build-writeback-plan.js +473 -0
- package/dist/esm/modules/formula/materialize/spill-engine.js +38 -0
- package/dist/esm/modules/formula/materialize/types.js +29 -0
- package/dist/esm/modules/formula/materialize/writeback-plan.js +27 -0
- package/dist/esm/modules/formula/runtime/evaluator.js +2291 -0
- package/dist/esm/modules/formula/runtime/function-registry.js +840 -0
- package/dist/esm/modules/formula/runtime/values.js +385 -0
- package/dist/esm/modules/formula/syntax/ast.js +28 -0
- package/dist/esm/modules/formula/syntax/parser.js +439 -0
- package/dist/esm/modules/formula/syntax/token-types.js +59 -0
- package/dist/esm/modules/formula/syntax/tokenizer.js +1074 -0
- package/dist/esm/modules/pdf/excel-bridge.js +9 -0
- package/dist/iife/excelts.iife.js +2302 -373
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +34 -34
- package/dist/types/index.browser.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/modules/excel/cell.d.ts +17 -3
- package/dist/types/modules/excel/defined-names.d.ts +96 -1
- package/dist/types/modules/excel/image.d.ts +11 -0
- package/dist/types/modules/excel/stream/workbook-reader.browser.d.ts +9 -3
- package/dist/types/modules/excel/stream/workbook-reader.d.ts +2 -1
- package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +39 -5
- package/dist/types/modules/excel/stream/workbook-writer.d.ts +3 -2
- package/dist/types/modules/excel/stream/worksheet-writer.d.ts +39 -6
- package/dist/types/modules/excel/types.d.ts +133 -2
- package/dist/types/modules/excel/utils/col-cache.d.ts +1 -0
- package/dist/types/modules/excel/utils/drawing-utils.d.ts +3 -3
- package/dist/types/modules/excel/utils/external-link-formula.d.ts +76 -0
- package/dist/types/modules/excel/utils/iterate-stream.d.ts +9 -3
- package/dist/types/modules/excel/utils/ooxml-paths.d.ts +19 -0
- package/dist/types/modules/excel/utils/shared-strings.d.ts +8 -3
- package/dist/types/modules/excel/utils/workbook-protection.d.ts +30 -0
- package/dist/types/modules/excel/workbook.browser.d.ts +257 -6
- package/dist/types/modules/excel/workbook.d.ts +1 -1
- package/dist/types/modules/excel/worksheet.d.ts +3 -1
- package/dist/types/modules/excel/xlsx/rel-type.d.ts +15 -0
- package/dist/types/modules/excel/xlsx/xform/book/defined-name-xform.d.ts +6 -5
- package/dist/types/modules/excel/xlsx/xform/book/external-link-xform.d.ts +84 -0
- package/dist/types/modules/excel/xlsx/xform/book/external-reference-xform.d.ts +17 -0
- package/dist/types/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.d.ts +3 -0
- package/dist/types/modules/excel/xlsx/xform/book/workbook-protection-xform.d.ts +20 -0
- package/dist/types/modules/excel/xlsx/xform/core/metadata-xform.d.ts +56 -0
- package/dist/types/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.d.ts +26 -0
- package/dist/types/modules/excel/xlsx/xform/sheet/cell-xform.d.ts +1 -1
- package/dist/types/modules/excel/xlsx/xform/sheet/ignored-errors-xform.d.ts +21 -0
- package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +172 -13
- package/dist/types/modules/excel/xlsx/xlsx.d.ts +7 -4
- package/dist/types/modules/formula/compile/address-utils.d.ts +62 -0
- package/dist/types/modules/formula/compile/binder.d.ts +42 -0
- package/dist/types/modules/formula/compile/bound-ast.d.ts +230 -0
- package/dist/types/modules/formula/compile/compiled-formula.d.ts +137 -0
- package/dist/types/modules/formula/compile/dependency-analysis.d.ts +93 -0
- package/dist/types/modules/formula/compile/structured-ref-utils.d.ts +93 -0
- package/dist/types/modules/formula/default-syntax-probe.d.ts +79 -0
- package/dist/types/modules/formula/functions/_date-context.d.ts +4 -0
- package/dist/types/modules/formula/functions/_shared.d.ts +121 -0
- package/dist/types/modules/formula/functions/conditional.d.ts +27 -0
- package/dist/types/modules/formula/functions/database.d.ts +37 -0
- package/dist/types/modules/formula/functions/date.d.ts +61 -0
- package/dist/types/modules/formula/functions/dynamic-array.d.ts +23 -0
- package/dist/types/modules/formula/functions/engineering.d.ts +57 -0
- package/dist/types/modules/formula/functions/financial.d.ts +202 -0
- package/dist/types/modules/formula/functions/lookup.d.ts +18 -0
- package/dist/types/modules/formula/functions/math.d.ts +114 -0
- package/dist/types/modules/formula/functions/statistical.d.ts +193 -0
- package/dist/types/modules/formula/functions/text.d.ts +86 -0
- package/dist/types/modules/formula/host-registry.d.ts +53 -0
- package/dist/types/modules/formula/index.d.ts +39 -0
- package/dist/types/modules/formula/install.d.ts +62 -0
- package/dist/types/modules/formula/integration/apply-writeback-plan.d.ts +26 -0
- package/dist/types/modules/formula/integration/calculate-formulas-impl.d.ts +30 -0
- package/dist/types/modules/formula/integration/calculate-formulas.d.ts +67 -0
- package/dist/types/modules/formula/integration/formula-instance.d.ts +64 -0
- package/dist/types/modules/formula/integration/workbook-adapter.d.ts +26 -0
- package/dist/types/modules/formula/integration/workbook-snapshot.d.ts +267 -0
- package/dist/types/modules/formula/materialize/build-writeback-plan.d.ts +34 -0
- package/dist/types/modules/formula/materialize/spill-engine.d.ts +9 -0
- package/dist/types/modules/formula/materialize/types.d.ts +179 -0
- package/dist/types/modules/formula/materialize/writeback-plan.d.ts +167 -0
- package/dist/types/modules/formula/runtime/evaluator.d.ts +151 -0
- package/dist/types/modules/formula/runtime/function-registry.d.ts +47 -0
- package/dist/types/modules/formula/runtime/values.d.ts +211 -0
- package/dist/types/modules/formula/syntax/ast.d.ts +129 -0
- package/dist/types/modules/formula/syntax/parser.d.ts +18 -0
- package/dist/types/modules/formula/syntax/token-types.d.ts +153 -0
- package/dist/types/modules/formula/syntax/tokenizer.d.ts +10 -0
- package/package.json +28 -28
|
@@ -0,0 +1,2291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluator — Execute BoundExpr using the RuntimeValue system.
|
|
3
|
+
*
|
|
4
|
+
* The evaluator operates on BoundExpr (from the compile phase),
|
|
5
|
+
* WorkbookSnapshot (from the snapshot phase), and RuntimeValue
|
|
6
|
+
* (the value system).
|
|
7
|
+
*/
|
|
8
|
+
import { parseDefinedNameRange } from "../compile/address-utils.js";
|
|
9
|
+
import { bind } from "../compile/binder.js";
|
|
10
|
+
import { BoundExprKind } from "../compile/bound-ast.js";
|
|
11
|
+
import { resolveStructuredRefRows, buildTableGeometry, resolveStructuredRefColumns } from "../compile/structured-ref-utils.js";
|
|
12
|
+
import { snapshotCellKey, formulaCellKey, resolveDefinedName as resolveDefinedNameFromSnapshot } from "../integration/workbook-snapshot.js";
|
|
13
|
+
import { parse } from "../syntax/parser.js";
|
|
14
|
+
import { stripFunctionPrefix } from "../syntax/token-types.js";
|
|
15
|
+
import { tokenize } from "../syntax/tokenizer.js";
|
|
16
|
+
import { lookupFunction } from "./function-registry.js";
|
|
17
|
+
import { RVKind, BLANK, ERRORS, rvNumber, rvString, rvBoolean, rvError, rvArray, rvRef, rvCellRef, rvLambda, isError, isScalar, isLambda, toNumberRV, toStringRV, toBooleanRV, topLeft, scalarEquals, compareScalarsSameKind, fromSnapshotValue } from "./values.js";
|
|
18
|
+
/**
|
|
19
|
+
* Per-calculation mutable state.
|
|
20
|
+
*/
|
|
21
|
+
export class EvalSession {
|
|
22
|
+
constructor() {
|
|
23
|
+
/** Cells currently on the evaluation call stack (circular-ref detection). */
|
|
24
|
+
this.evaluating = new Set();
|
|
25
|
+
/**
|
|
26
|
+
* Unified formula result cache.
|
|
27
|
+
* Each entry holds both the scalar form (for dependents) and the raw form
|
|
28
|
+
* (for materialize). This replaces the previous separate cache/rawCache
|
|
29
|
+
* pattern with a single, self-documenting structure.
|
|
30
|
+
*/
|
|
31
|
+
this.resultCache = new Map();
|
|
32
|
+
/** Cache for runtime name resolution (defined names that need parsing). */
|
|
33
|
+
this.nameCache = new Map();
|
|
34
|
+
/** Fallback values for circular references during iterative calculation. */
|
|
35
|
+
this.circularFallback = new Map();
|
|
36
|
+
/**
|
|
37
|
+
* Live spill map: cell key → (masterKey, row-offset, col-offset).
|
|
38
|
+
*
|
|
39
|
+
* Populated as soon as a dynamic-array formula is evaluated and yields
|
|
40
|
+
* an array result. Downstream formulas that read a cell inside the
|
|
41
|
+
* spill region look the master's cached array up via this map and
|
|
42
|
+
* return the correct element — even before materialize has written
|
|
43
|
+
* the ghost cells to the snapshot.
|
|
44
|
+
*
|
|
45
|
+
* This is the fix for "first-pass `=SUM(A1:A5)` over a `=SEQUENCE(5)`
|
|
46
|
+
* spill" — without the live map, `getCellValue("S", 2, 1)` returned
|
|
47
|
+
* BLANK and SUM only counted the master cell.
|
|
48
|
+
*/
|
|
49
|
+
this.liveSpills = new Map();
|
|
50
|
+
/**
|
|
51
|
+
* Runtime dependency recorder — tracks cell accesses made during evaluation.
|
|
52
|
+
*
|
|
53
|
+
* When a formula with `hasDynamicRefs` (INDIRECT/OFFSET) is being evaluated,
|
|
54
|
+
* every `getCellValue` / `buildRangeArray` call records the accessed cell/range
|
|
55
|
+
* key here. After evaluation, these dynamic edges can be merged with the
|
|
56
|
+
* compiled static dependency set to produce a complete dependency graph.
|
|
57
|
+
*
|
|
58
|
+
* Key: formula cell key being evaluated → Set of accessed cell keys.
|
|
59
|
+
* Only populated for formulas that have `hasDynamicRefs === true`.
|
|
60
|
+
*/
|
|
61
|
+
this.dynamicDeps = new Map();
|
|
62
|
+
/**
|
|
63
|
+
* The formula cell key currently being recorded (null if recording is off).
|
|
64
|
+
* Set before evaluating a formula with dynamic refs, cleared after.
|
|
65
|
+
*/
|
|
66
|
+
this.recordingKey = null;
|
|
67
|
+
/**
|
|
68
|
+
* Current LAMBDA invocation depth. Guards against unbounded recursion
|
|
69
|
+
* (e.g. `LAMBDA(x, x(x))(LAMBDA(x, x(x)))`) that would otherwise overflow
|
|
70
|
+
* the JS call stack. Excel documents a recursion limit of ~256.
|
|
71
|
+
*/
|
|
72
|
+
this.lambdaDepth = 0;
|
|
73
|
+
/**
|
|
74
|
+
* AST cache for INDIRECT re-parsing. INDIRECT receives a runtime string
|
|
75
|
+
* describing a reference; re-parsing it per invocation would be wasted
|
|
76
|
+
* work, so we memoise the `bound` expression keyed on the reference
|
|
77
|
+
* text. This belongs to the session (per-calculation lifetime) rather
|
|
78
|
+
* than the snapshot because the bindings depend on the evaluation
|
|
79
|
+
* context.
|
|
80
|
+
*/
|
|
81
|
+
this.indirectAstCache = new Map();
|
|
82
|
+
}
|
|
83
|
+
makeKey(sheet, row, col) {
|
|
84
|
+
return formulaCellKey(sheet, row, col);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Record a cell access for the currently-recording formula.
|
|
88
|
+
*/
|
|
89
|
+
recordAccess(sheet, row, col) {
|
|
90
|
+
if (this.recordingKey === null) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
let deps = this.dynamicDeps.get(this.recordingKey);
|
|
94
|
+
if (!deps) {
|
|
95
|
+
deps = new Set();
|
|
96
|
+
this.dynamicDeps.set(this.recordingKey, deps);
|
|
97
|
+
}
|
|
98
|
+
deps.add(formulaCellKey(sheet, row, col));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// Main Evaluate Function
|
|
103
|
+
// ============================================================================
|
|
104
|
+
/**
|
|
105
|
+
* Exhaustiveness helper — TypeScript narrows `never` here to prove that
|
|
106
|
+
* every discriminated-union variant was handled. At runtime this should be
|
|
107
|
+
* unreachable; if a new variant is added without a case, compilation fails.
|
|
108
|
+
*/
|
|
109
|
+
function assertNever(x) {
|
|
110
|
+
throw new Error(`unexpected variant: ${JSON.stringify(x)}`);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Evaluate a BoundExpr to produce a RuntimeValue.
|
|
114
|
+
*/
|
|
115
|
+
export function evaluate(expr, ctx, session) {
|
|
116
|
+
switch (expr.kind) {
|
|
117
|
+
case BoundExprKind.Literal:
|
|
118
|
+
return evaluateLiteral(expr);
|
|
119
|
+
case BoundExprKind.CellRef:
|
|
120
|
+
return evaluateCellRef(expr, ctx, session);
|
|
121
|
+
case BoundExprKind.AreaRef:
|
|
122
|
+
return evaluateAreaRef(expr, ctx, session);
|
|
123
|
+
case BoundExprKind.ColRangeRef:
|
|
124
|
+
return evaluateColRange(expr, ctx, session);
|
|
125
|
+
case BoundExprKind.RowRangeRef:
|
|
126
|
+
return evaluateRowRange(expr, ctx, session);
|
|
127
|
+
case BoundExprKind.Ref3D:
|
|
128
|
+
return evaluateRef3D(expr, ctx, session);
|
|
129
|
+
case BoundExprKind.BinaryOp:
|
|
130
|
+
return evaluateBinaryOp(expr.op, expr.left, expr.right, ctx, session);
|
|
131
|
+
case BoundExprKind.UnaryOp:
|
|
132
|
+
return evaluateUnaryOp(expr.op, expr.operand, ctx, session);
|
|
133
|
+
case BoundExprKind.Percent:
|
|
134
|
+
return evaluatePercent(expr.operand, ctx, session);
|
|
135
|
+
case BoundExprKind.Call:
|
|
136
|
+
return evaluateCall(expr, ctx, session);
|
|
137
|
+
case BoundExprKind.SpecialCall:
|
|
138
|
+
return evaluateSpecialCall(expr, ctx, session);
|
|
139
|
+
case BoundExprKind.Array:
|
|
140
|
+
return evaluateArrayLiteral(expr, ctx, session);
|
|
141
|
+
case BoundExprKind.NameExpr:
|
|
142
|
+
return evaluateNameExpr(expr, ctx, session);
|
|
143
|
+
case BoundExprKind.Lambda:
|
|
144
|
+
return evaluateLambdaExpr(expr, ctx);
|
|
145
|
+
case BoundExprKind.StructuredRef:
|
|
146
|
+
return evaluateStructuredRef(expr, ctx, session);
|
|
147
|
+
default:
|
|
148
|
+
return assertNever(expr);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// Literal
|
|
153
|
+
// ============================================================================
|
|
154
|
+
function evaluateLiteral(expr) {
|
|
155
|
+
if (expr.errorCode) {
|
|
156
|
+
return rvError(expr.errorCode);
|
|
157
|
+
}
|
|
158
|
+
if (expr.value === null) {
|
|
159
|
+
return BLANK;
|
|
160
|
+
}
|
|
161
|
+
if (typeof expr.value === "number") {
|
|
162
|
+
return rvNumber(expr.value);
|
|
163
|
+
}
|
|
164
|
+
if (typeof expr.value === "string") {
|
|
165
|
+
return rvString(expr.value);
|
|
166
|
+
}
|
|
167
|
+
if (typeof expr.value === "boolean") {
|
|
168
|
+
return rvBoolean(expr.value);
|
|
169
|
+
}
|
|
170
|
+
return BLANK;
|
|
171
|
+
}
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// Cell Reference
|
|
174
|
+
// ============================================================================
|
|
175
|
+
function evaluateCellRef(expr, ctx, session) {
|
|
176
|
+
return rvCellRef(expr.sheet, expr.row, expr.col);
|
|
177
|
+
}
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// Area Reference → ReferenceValue
|
|
180
|
+
// ============================================================================
|
|
181
|
+
function evaluateAreaRef(expr, ctx, session) {
|
|
182
|
+
return rvRef(expr.sheet, expr.top, expr.left, expr.bottom, expr.right);
|
|
183
|
+
}
|
|
184
|
+
function evaluateColRange(expr, ctx, session) {
|
|
185
|
+
const ws = ctx.snapshot.worksheetsByName.get(expr.sheet.toLowerCase());
|
|
186
|
+
if (!ws || !ws.dimensions) {
|
|
187
|
+
return rvArray([]);
|
|
188
|
+
}
|
|
189
|
+
return rvRef(expr.sheet, ws.dimensions.top, expr.leftCol, ws.dimensions.bottom, expr.rightCol);
|
|
190
|
+
}
|
|
191
|
+
function evaluateRowRange(expr, ctx, session) {
|
|
192
|
+
const ws = ctx.snapshot.worksheetsByName.get(expr.sheet.toLowerCase());
|
|
193
|
+
if (!ws || !ws.dimensions) {
|
|
194
|
+
return rvArray([]);
|
|
195
|
+
}
|
|
196
|
+
return rvRef(expr.sheet, expr.topRow, ws.dimensions.left, expr.bottomRow, ws.dimensions.right);
|
|
197
|
+
}
|
|
198
|
+
function evaluateRef3D(expr, ctx, session) {
|
|
199
|
+
const areas = [];
|
|
200
|
+
for (const sheet of expr.sheets) {
|
|
201
|
+
if (expr.inner.kind === BoundExprKind.CellRef) {
|
|
202
|
+
areas.push({
|
|
203
|
+
sheet,
|
|
204
|
+
top: expr.inner.row,
|
|
205
|
+
left: expr.inner.col,
|
|
206
|
+
bottom: expr.inner.row,
|
|
207
|
+
right: expr.inner.col
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
// For AreaRef: if the inner range is a whole-column or whole-row
|
|
212
|
+
// reference (top = 1 & bottom = 1048576, or left = 1 & right =
|
|
213
|
+
// 16384), clamp it against each sheet's actual used dimensions.
|
|
214
|
+
// Without this clamp, a 3D reference like `Sheet1:Sheet3!A:A`
|
|
215
|
+
// would allocate 3 × 1M rows of BLANK values and spend seconds
|
|
216
|
+
// (or OOM) before SUM even starts. The non-3D path already does
|
|
217
|
+
// this clamp in `evaluateColRange`; parity with that is what we
|
|
218
|
+
// want here.
|
|
219
|
+
let top = expr.inner.top;
|
|
220
|
+
let left = expr.inner.left;
|
|
221
|
+
let bottom = expr.inner.bottom;
|
|
222
|
+
let right = expr.inner.right;
|
|
223
|
+
const isWholeCol = top === 1 && bottom === 1048576;
|
|
224
|
+
const isWholeRow = left === 1 && right === 16384;
|
|
225
|
+
if (isWholeCol || isWholeRow) {
|
|
226
|
+
const ws = ctx.snapshot.worksheetsByName.get(sheet.toLowerCase());
|
|
227
|
+
const dims = ws?.dimensions;
|
|
228
|
+
if (dims) {
|
|
229
|
+
if (isWholeCol) {
|
|
230
|
+
top = dims.top;
|
|
231
|
+
bottom = dims.bottom;
|
|
232
|
+
}
|
|
233
|
+
if (isWholeRow) {
|
|
234
|
+
left = dims.left;
|
|
235
|
+
right = dims.right;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
areas.push({ sheet, top, left, bottom, right });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { kind: RVKind.Reference, areas };
|
|
243
|
+
}
|
|
244
|
+
// ============================================================================
|
|
245
|
+
// Build Range Array from Snapshot
|
|
246
|
+
// ============================================================================
|
|
247
|
+
function buildRangeArray(ctx, session, sheet, top, left, bottom, right) {
|
|
248
|
+
// Hoist the worksheet lookup once for the entire range — avoids
|
|
249
|
+
// N redundant toLowerCase()/Map.get() calls inside the hot loop.
|
|
250
|
+
const ws = ctx.snapshot.worksheetsByName.get(sheet.toLowerCase());
|
|
251
|
+
const cells = ws?.cells;
|
|
252
|
+
const wsHiddenRows = ws?.hiddenRows;
|
|
253
|
+
const compiledFormulas = ctx.compiledFormulas;
|
|
254
|
+
const resultCache = session.resultCache;
|
|
255
|
+
// Hoist the recording guard — when not recording, skip recordAccess
|
|
256
|
+
// entirely in the loop instead of paying the function-call overhead.
|
|
257
|
+
const recording = session.recordingKey !== null;
|
|
258
|
+
const rows = [];
|
|
259
|
+
// Lazily-allocated masks: only materialized once we encounter a
|
|
260
|
+
// SUBTOTAL/AGGREGATE cell or a hidden row inside the range. For the
|
|
261
|
+
// common case (plain data range, no hidden rows) we never touch these
|
|
262
|
+
// and emit an ArrayValue without extra metadata.
|
|
263
|
+
let subtotalMask;
|
|
264
|
+
let hiddenRowMask;
|
|
265
|
+
const height = bottom - top + 1;
|
|
266
|
+
const width = right - left + 1;
|
|
267
|
+
for (let r = top; r <= bottom; r++) {
|
|
268
|
+
const row = [];
|
|
269
|
+
const ri = r - top;
|
|
270
|
+
// Record row visibility — SUBTOTAL 1xx / AGGREGATE opt 5/7 use it.
|
|
271
|
+
if (wsHiddenRows?.has(r)) {
|
|
272
|
+
if (!hiddenRowMask) {
|
|
273
|
+
hiddenRowMask = new Array(height).fill(false);
|
|
274
|
+
}
|
|
275
|
+
hiddenRowMask[ri] = true;
|
|
276
|
+
}
|
|
277
|
+
for (let c = left; c <= right; c++) {
|
|
278
|
+
if (recording) {
|
|
279
|
+
session.recordAccess(sheet, r, c);
|
|
280
|
+
}
|
|
281
|
+
// Missing worksheet: matches getCellValue's BLANK fallback.
|
|
282
|
+
if (!cells) {
|
|
283
|
+
row.push(BLANK);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const cell = cells.get(snapshotCellKey(r, c));
|
|
287
|
+
if (!cell) {
|
|
288
|
+
// No snapshot cell yet — might still be inside a live spill
|
|
289
|
+
// (e.g. reading A2..A5 while A1 = SEQUENCE(5) is still being
|
|
290
|
+
// materialized). See `readLiveSpill` for the lookup path.
|
|
291
|
+
const live = readLiveSpill(sheet, r, c, session);
|
|
292
|
+
row.push(live ? topLeft(live) : BLANK);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (cell.formulaKind !== "none" && cell.formula) {
|
|
296
|
+
const fKey = formulaCellKey(sheet, r, c);
|
|
297
|
+
const compiled = compiledFormulas.get(fKey);
|
|
298
|
+
// Mark SUBTOTAL/AGGREGATE output cells so an outer SUBTOTAL /
|
|
299
|
+
// AGGREGATE over this range knows to skip them (Excel's
|
|
300
|
+
// no-double-count semantics).
|
|
301
|
+
if (compiled?.isSubtotalOutput) {
|
|
302
|
+
if (!subtotalMask) {
|
|
303
|
+
subtotalMask = new Array(height);
|
|
304
|
+
for (let i = 0; i < height; i++) {
|
|
305
|
+
subtotalMask[i] = new Array(width).fill(false);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
subtotalMask[ri][c - left] = true;
|
|
309
|
+
}
|
|
310
|
+
const cached = resultCache.get(fKey);
|
|
311
|
+
if (cached !== undefined) {
|
|
312
|
+
row.push(topLeft(cached.scalar));
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (compiled) {
|
|
316
|
+
row.push(topLeft(evaluateFormula(compiled, ctx, session)));
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Non-formula snapshot cell — but it might still be a ghost slot
|
|
321
|
+
// that a fresh dynamic-array spill is about to overwrite. Prefer
|
|
322
|
+
// the live value when a master is registered so this-pass SUM /
|
|
323
|
+
// LOOKUP / etc. see the new spill immediately.
|
|
324
|
+
const live = readLiveSpill(sheet, r, c, session);
|
|
325
|
+
if (live) {
|
|
326
|
+
row.push(topLeft(live));
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
row.push(topLeft(fromSnapshotValue(cell.value)));
|
|
330
|
+
}
|
|
331
|
+
rows.push(row);
|
|
332
|
+
}
|
|
333
|
+
return rvArray(rows, top, left, subtotalMask, hiddenRowMask);
|
|
334
|
+
}
|
|
335
|
+
// ============================================================================
|
|
336
|
+
// Dereference: Reference → concrete value
|
|
337
|
+
// ============================================================================
|
|
338
|
+
/**
|
|
339
|
+
* Resolve a `ReferenceValue` to its concrete value (scalar or array).
|
|
340
|
+
* - Single-cell references (from CellRef nodes) resolve to a scalar.
|
|
341
|
+
* - Area references (even 1x1 from A1:A1) produce an array.
|
|
342
|
+
* - Multi-area references (from 3D refs) flatten all areas into one array.
|
|
343
|
+
* Non-reference values are returned unchanged.
|
|
344
|
+
*/
|
|
345
|
+
function dereferenceValue(v, ctx, session) {
|
|
346
|
+
if (v.kind !== RVKind.Reference) {
|
|
347
|
+
return v;
|
|
348
|
+
}
|
|
349
|
+
if (v.areas.length === 0) {
|
|
350
|
+
return BLANK;
|
|
351
|
+
}
|
|
352
|
+
// Single-cell references (from CellRef nodes) resolve to scalar
|
|
353
|
+
if (v.singleCell) {
|
|
354
|
+
const area = v.areas[0];
|
|
355
|
+
return getCellValue(area.sheet, area.top, area.left, ctx, session);
|
|
356
|
+
}
|
|
357
|
+
// Single area — build range array
|
|
358
|
+
if (v.areas.length === 1) {
|
|
359
|
+
const area = v.areas[0];
|
|
360
|
+
return buildRangeArray(ctx, session, area.sheet, area.top, area.left, area.bottom, area.right);
|
|
361
|
+
}
|
|
362
|
+
// Multi-area (3D reference) — flatten all areas into one array
|
|
363
|
+
const allRows = [];
|
|
364
|
+
let mergedSubtotal;
|
|
365
|
+
let mergedHidden;
|
|
366
|
+
for (const area of v.areas) {
|
|
367
|
+
const arr = buildRangeArray(ctx, session, area.sheet, area.top, area.left, area.bottom, area.right);
|
|
368
|
+
const startRow = allRows.length;
|
|
369
|
+
for (const row of arr.rows) {
|
|
370
|
+
allRows.push([...row]);
|
|
371
|
+
}
|
|
372
|
+
// Merge masks from this area into the flattened output. Only
|
|
373
|
+
// allocate the merged masks when the first masked area shows up —
|
|
374
|
+
// most multi-area refs have no masks and should pay no overhead.
|
|
375
|
+
if (arr.subtotalMask || arr.hiddenRowMask) {
|
|
376
|
+
const height = arr.height;
|
|
377
|
+
if (arr.subtotalMask) {
|
|
378
|
+
if (!mergedSubtotal) {
|
|
379
|
+
mergedSubtotal = [];
|
|
380
|
+
for (let i = 0; i < startRow; i++) {
|
|
381
|
+
// Widths may differ across areas; mask rows use each area's
|
|
382
|
+
// own width so an outer SUBTOTAL reads the right positions.
|
|
383
|
+
mergedSubtotal.push([]);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
for (let r = 0; r < height; r++) {
|
|
387
|
+
mergedSubtotal.push([...arr.subtotalMask[r]]);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else if (mergedSubtotal) {
|
|
391
|
+
// Pad with empty rows so indices stay aligned.
|
|
392
|
+
for (let r = 0; r < height; r++) {
|
|
393
|
+
mergedSubtotal.push([]);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (arr.hiddenRowMask) {
|
|
397
|
+
if (!mergedHidden) {
|
|
398
|
+
mergedHidden = new Array(startRow).fill(false);
|
|
399
|
+
}
|
|
400
|
+
for (let r = 0; r < height; r++) {
|
|
401
|
+
mergedHidden.push(arr.hiddenRowMask[r] ?? false);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else if (mergedHidden) {
|
|
405
|
+
for (let r = 0; r < height; r++) {
|
|
406
|
+
mergedHidden.push(false);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else if (mergedSubtotal || mergedHidden) {
|
|
411
|
+
// An earlier area had masks; pad this area's rows with non-masked
|
|
412
|
+
// entries to keep row indices aligned.
|
|
413
|
+
const height = arr.height;
|
|
414
|
+
if (mergedSubtotal) {
|
|
415
|
+
for (let r = 0; r < height; r++) {
|
|
416
|
+
mergedSubtotal.push([]);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (mergedHidden) {
|
|
420
|
+
for (let r = 0; r < height; r++) {
|
|
421
|
+
mergedHidden.push(false);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return rvArray(allRows, undefined, undefined, mergedSubtotal, mergedHidden);
|
|
427
|
+
}
|
|
428
|
+
// ============================================================================
|
|
429
|
+
// Get Cell Value from Snapshot
|
|
430
|
+
// ============================================================================
|
|
431
|
+
function getCellValue(sheetName, row, col, ctx, session) {
|
|
432
|
+
// Record this access for runtime dependency tracking. Inline guard
|
|
433
|
+
// avoids the function call overhead in the common case where no
|
|
434
|
+
// recording is active (formulas without dynamic refs).
|
|
435
|
+
if (session.recordingKey !== null) {
|
|
436
|
+
session.recordAccess(sheetName, row, col);
|
|
437
|
+
}
|
|
438
|
+
const ws = ctx.snapshot.worksheetsByName.get(sheetName.toLowerCase());
|
|
439
|
+
if (!ws) {
|
|
440
|
+
return BLANK;
|
|
441
|
+
}
|
|
442
|
+
const cellKey = snapshotCellKey(row, col);
|
|
443
|
+
const cell = ws.cells.get(cellKey);
|
|
444
|
+
if (!cell) {
|
|
445
|
+
// The cell isn't in the snapshot, but we might still have a live
|
|
446
|
+
// spill value for it — look up the master formula and extract the
|
|
447
|
+
// right array element. Matters when a downstream formula like
|
|
448
|
+
// `SUM(A1:A5)` runs before materialize writes the ghost cells for
|
|
449
|
+
// `A1 = SEQUENCE(5)` into the snapshot.
|
|
450
|
+
return readLiveSpill(sheetName, row, col, session) ?? BLANK;
|
|
451
|
+
}
|
|
452
|
+
// If this cell has a formula, evaluate it
|
|
453
|
+
if (cell.formulaKind !== "none" && cell.formula) {
|
|
454
|
+
const fKey = formulaCellKey(sheetName, row, col);
|
|
455
|
+
// Check cache — return scalar form for dependency resolution
|
|
456
|
+
const cached = session.resultCache.get(fKey);
|
|
457
|
+
if (cached !== undefined) {
|
|
458
|
+
return cached.scalar;
|
|
459
|
+
}
|
|
460
|
+
// Get compiled formula
|
|
461
|
+
const compiled = ctx.compiledFormulas.get(fKey);
|
|
462
|
+
if (compiled) {
|
|
463
|
+
return evaluateFormula(compiled, ctx, session);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// Non-formula cell — but it might also be a ghost for a live spill
|
|
467
|
+
// (e.g. a value that exists in the snapshot from a previous calc
|
|
468
|
+
// cycle but is about to be overwritten by a fresh spill). Prefer the
|
|
469
|
+
// live value when a master is registered.
|
|
470
|
+
const spill = readLiveSpill(sheetName, row, col, session);
|
|
471
|
+
if (spill) {
|
|
472
|
+
return spill;
|
|
473
|
+
}
|
|
474
|
+
return fromSnapshotValue(cell.value);
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Retrieve the spill-target value at (sheetName, row, col) from an
|
|
478
|
+
* already-evaluated dynamic-array master. Returns `undefined` when no
|
|
479
|
+
* master is registered for that cell — callers fall back to the
|
|
480
|
+
* snapshot value or BLANK.
|
|
481
|
+
*/
|
|
482
|
+
function readLiveSpill(sheetName, row, col, session) {
|
|
483
|
+
const key = session.makeKey(sheetName, row, col);
|
|
484
|
+
const spill = session.liveSpills.get(key);
|
|
485
|
+
if (!spill) {
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
const master = session.resultCache.get(spill.masterKey);
|
|
489
|
+
if (!master || master.raw.kind !== RVKind.Array) {
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
const arr = master.raw;
|
|
493
|
+
if (spill.rowOffset >= arr.height || spill.colOffset >= arr.width) {
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
return arr.rows[spill.rowOffset][spill.colOffset];
|
|
497
|
+
}
|
|
498
|
+
// ============================================================================
|
|
499
|
+
// Evaluate a Compiled Formula
|
|
500
|
+
// ============================================================================
|
|
501
|
+
/**
|
|
502
|
+
* Shared implementation for evaluateFormula and evaluateFormulaRaw.
|
|
503
|
+
*
|
|
504
|
+
* Handles key computation, cache lookup, circular reference detection,
|
|
505
|
+
* expression evaluation, and result caching.
|
|
506
|
+
*/
|
|
507
|
+
function evaluateFormulaInner(compiled, ctx, session) {
|
|
508
|
+
const inst = compiled.instance;
|
|
509
|
+
const key = session.makeKey(inst.sheetName, inst.row, inst.col);
|
|
510
|
+
// Check cache
|
|
511
|
+
const cached = session.resultCache.get(key);
|
|
512
|
+
if (cached !== undefined) {
|
|
513
|
+
return cached;
|
|
514
|
+
}
|
|
515
|
+
// Circular reference detection. Under iterative calculation the driver
|
|
516
|
+
// (calculate-formulas-impl.ts) seeds `circularFallback` with the previous
|
|
517
|
+
// iteration's result so the re-entrant lookup receives a stable value.
|
|
518
|
+
// Outside of iterative mode the map is empty — we return 0 as the fallback,
|
|
519
|
+
// matching Excel's "iterate with 0 seed" convention. This keeps simple
|
|
520
|
+
// cycles like A1=A1+1 producing a number instead of an error, which is the
|
|
521
|
+
// established behaviour for this engine (tests depend on this). For strict
|
|
522
|
+
// circular-reference error reporting, enable iterative calculation and
|
|
523
|
+
// observe convergence failure, or configure a custom fallback value.
|
|
524
|
+
if (session.evaluating.has(key)) {
|
|
525
|
+
const fallback = session.circularFallback.get(key);
|
|
526
|
+
const val = fallback !== undefined ? fallback : rvNumber(0);
|
|
527
|
+
return { scalar: val, raw: val };
|
|
528
|
+
}
|
|
529
|
+
session.evaluating.add(key);
|
|
530
|
+
const prevAddress = ctx.currentAddress;
|
|
531
|
+
const prevSheet = ctx.currentSheet;
|
|
532
|
+
const prevRecording = session.recordingKey;
|
|
533
|
+
ctx.currentAddress = { sheet: inst.sheetName, row: inst.row, col: inst.col };
|
|
534
|
+
ctx.currentSheet = inst.sheetName;
|
|
535
|
+
// Enable runtime dependency recording for formulas with dynamic refs
|
|
536
|
+
if (compiled.hasDynamicRefs) {
|
|
537
|
+
session.recordingKey = key;
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
const result = evaluate(compiled.bound, ctx, session);
|
|
541
|
+
const intersected = implicitIntersect(result, ctx);
|
|
542
|
+
const scalar = dereferenceValue(intersected, ctx, session);
|
|
543
|
+
const raw = dereferenceValue(result, ctx, session);
|
|
544
|
+
const entry = { scalar, raw };
|
|
545
|
+
session.resultCache.set(key, entry);
|
|
546
|
+
// Register the spill region if this is a dynamic-array formula
|
|
547
|
+
// whose result is a multi-cell array. Downstream formulas that
|
|
548
|
+
// read into the spill range (e.g. `=SUM(A1:A5)` over a
|
|
549
|
+
// `=SEQUENCE(5)` master) can now pick up the ghost-cell values
|
|
550
|
+
// before materialize writes them back to the snapshot.
|
|
551
|
+
const isDyn = compiled.instance.isDynamicArray || compiled.isDynamicArrayFunction;
|
|
552
|
+
if (isDyn && raw.kind === RVKind.Array && (raw.height > 1 || raw.width > 1)) {
|
|
553
|
+
for (let r = 0; r < raw.height; r++) {
|
|
554
|
+
for (let c = 0; c < raw.width; c++) {
|
|
555
|
+
if (r === 0 && c === 0) {
|
|
556
|
+
continue; // master cell already points to its own cache entry
|
|
557
|
+
}
|
|
558
|
+
const targetKey = session.makeKey(inst.sheetName, inst.row + r, inst.col + c);
|
|
559
|
+
session.liveSpills.set(targetKey, {
|
|
560
|
+
masterKey: key,
|
|
561
|
+
rowOffset: r,
|
|
562
|
+
colOffset: c
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return entry;
|
|
568
|
+
}
|
|
569
|
+
catch (err) {
|
|
570
|
+
// Cache a #CALC! sentinel so a re-entrant lookup for the same cell does
|
|
571
|
+
// not trigger repeated (exponentially growing) re-evaluation under
|
|
572
|
+
// iterative calc or dependent recomputation. The exception is re-thrown
|
|
573
|
+
// so the outer caller can still log / translate it into a sheet error.
|
|
574
|
+
const fallback = { scalar: ERRORS.CALC, raw: ERRORS.CALC };
|
|
575
|
+
session.resultCache.set(key, fallback);
|
|
576
|
+
throw err;
|
|
577
|
+
}
|
|
578
|
+
finally {
|
|
579
|
+
session.evaluating.delete(key);
|
|
580
|
+
ctx.currentAddress = prevAddress;
|
|
581
|
+
ctx.currentSheet = prevSheet;
|
|
582
|
+
session.recordingKey = prevRecording;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Evaluate a compiled formula and return its **scalar** result.
|
|
587
|
+
*
|
|
588
|
+
* This is the standard evaluation path for regular (non-array) formulas.
|
|
589
|
+
* The result is:
|
|
590
|
+
* 1. Evaluated from the bound expression tree
|
|
591
|
+
* 2. Implicit-intersected to a single value/reference
|
|
592
|
+
* 3. Dereferenced if it's a reference
|
|
593
|
+
* 4. Cached for subsequent lookups by dependent formulas
|
|
594
|
+
*
|
|
595
|
+
* Use `evaluateFormulaRaw` instead when the full array result is needed
|
|
596
|
+
* (dynamic array formulas, CSE formulas).
|
|
597
|
+
*/
|
|
598
|
+
export function evaluateFormula(compiled, ctx, session) {
|
|
599
|
+
return evaluateFormulaInner(compiled, ctx, session).scalar;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Evaluate a compiled formula and return the **raw** (possibly array) result.
|
|
603
|
+
*
|
|
604
|
+
* This is the evaluation path for dynamic array and CSE formulas where
|
|
605
|
+
* the full array shape must be preserved for spill/distribution.
|
|
606
|
+
*
|
|
607
|
+
* Semantics:
|
|
608
|
+
* - Both scalar and raw forms are stored in `session.resultCache` as a
|
|
609
|
+
* `CachedResult{scalar, raw}`. Dependent scalar formulas see the scalar
|
|
610
|
+
* form; the materialize layer retrieves the raw form.
|
|
611
|
+
* - The return value is the full dereferenced result — may be an ArrayValue
|
|
612
|
+
* with height > 1 or width > 1.
|
|
613
|
+
*/
|
|
614
|
+
export function evaluateFormulaRaw(compiled, ctx, session) {
|
|
615
|
+
return evaluateFormulaInner(compiled, ctx, session).raw;
|
|
616
|
+
}
|
|
617
|
+
// ============================================================================
|
|
618
|
+
// Binary Operations
|
|
619
|
+
// ============================================================================
|
|
620
|
+
function evaluateBinaryOp(op, leftExpr, rightExpr, ctx, session) {
|
|
621
|
+
// Intersection operator (whitespace between two refs). Must be handled
|
|
622
|
+
// BEFORE dereferencing so we can inspect the reference areas.
|
|
623
|
+
if (op === " ") {
|
|
624
|
+
return evaluateIntersection(leftExpr, rightExpr, ctx, session);
|
|
625
|
+
}
|
|
626
|
+
// Range operator `:` — union of two references into the bounding
|
|
627
|
+
// rectangle. Needed when one side is a function call (e.g.
|
|
628
|
+
// `B11:INDIRECT("B" & ROW()-1)`). Both sides must be references or
|
|
629
|
+
// coerce to references; otherwise Excel returns #REF!.
|
|
630
|
+
if (op === ":") {
|
|
631
|
+
return evaluateRangeUnion(leftExpr, rightExpr, ctx, session);
|
|
632
|
+
}
|
|
633
|
+
const left = dereferenceValue(evaluate(leftExpr, ctx, session), ctx, session);
|
|
634
|
+
const right = dereferenceValue(evaluate(rightExpr, ctx, session), ctx, session);
|
|
635
|
+
const lIsArr = left.kind === RVKind.Array;
|
|
636
|
+
const rIsArr = right.kind === RVKind.Array;
|
|
637
|
+
if (lIsArr || rIsArr) {
|
|
638
|
+
return broadcastBinaryOp(op, left, right);
|
|
639
|
+
}
|
|
640
|
+
return applyScalarBinaryOp(op, topLeft(left), topLeft(right));
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Excel's intersection operator — a whitespace character separating two
|
|
644
|
+
* references (e.g. `A1:A10 B1:B10`).
|
|
645
|
+
*
|
|
646
|
+
* Semantics:
|
|
647
|
+
* - Both operands must evaluate to single-area references. Otherwise the
|
|
648
|
+
* result is `#VALUE!` (matches Excel's behaviour when non-refs or
|
|
649
|
+
* multi-area refs are intersected).
|
|
650
|
+
* - Intersection is the rectangle overlap of the two areas on the same
|
|
651
|
+
* sheet.
|
|
652
|
+
* - If the areas do not overlap (or are on different sheets) the result
|
|
653
|
+
* is `#NULL!`, Excel's canonical "empty intersection" error.
|
|
654
|
+
*/
|
|
655
|
+
function evaluateIntersection(leftExpr, rightExpr, ctx, session) {
|
|
656
|
+
const left = evaluate(leftExpr, ctx, session);
|
|
657
|
+
const right = evaluate(rightExpr, ctx, session);
|
|
658
|
+
if (isError(left)) {
|
|
659
|
+
return left;
|
|
660
|
+
}
|
|
661
|
+
if (isError(right)) {
|
|
662
|
+
return right;
|
|
663
|
+
}
|
|
664
|
+
if (left.kind !== RVKind.Reference || right.kind !== RVKind.Reference) {
|
|
665
|
+
return ERRORS.VALUE;
|
|
666
|
+
}
|
|
667
|
+
if (left.areas.length !== 1 || right.areas.length !== 1) {
|
|
668
|
+
return ERRORS.VALUE;
|
|
669
|
+
}
|
|
670
|
+
const la = left.areas[0];
|
|
671
|
+
const ra = right.areas[0];
|
|
672
|
+
if (la.sheet.toLowerCase() !== ra.sheet.toLowerCase()) {
|
|
673
|
+
return ERRORS.NULL;
|
|
674
|
+
}
|
|
675
|
+
const top = Math.max(la.top, ra.top);
|
|
676
|
+
const left_ = Math.max(la.left, ra.left);
|
|
677
|
+
const bottom = Math.min(la.bottom, ra.bottom);
|
|
678
|
+
const right_ = Math.min(la.right, ra.right);
|
|
679
|
+
if (top > bottom || left_ > right_) {
|
|
680
|
+
return ERRORS.NULL;
|
|
681
|
+
}
|
|
682
|
+
return rvRef(la.sheet, top, left_, bottom, right_);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Range operator `:` applied at runtime. Normally `A1:B2` is merged by
|
|
686
|
+
* the tokenizer, but patterns like `A1:INDIRECT("B5")` leave the colon
|
|
687
|
+
* as a standalone operator. Both operands must evaluate to references
|
|
688
|
+
* (single-cell or area); the result is the bounding rectangle of the
|
|
689
|
+
* two reference ranges on the same sheet.
|
|
690
|
+
*
|
|
691
|
+
* Semantics:
|
|
692
|
+
* - Both sides must be references. Literal numbers / strings → #VALUE!.
|
|
693
|
+
* - References must live on the same sheet → else #REF!.
|
|
694
|
+
* - Multi-area references on either side → #REF! (Excel behavior).
|
|
695
|
+
*/
|
|
696
|
+
function evaluateRangeUnion(leftExpr, rightExpr, ctx, session) {
|
|
697
|
+
const left = evaluate(leftExpr, ctx, session);
|
|
698
|
+
const right = evaluate(rightExpr, ctx, session);
|
|
699
|
+
if (isError(left)) {
|
|
700
|
+
return left;
|
|
701
|
+
}
|
|
702
|
+
if (isError(right)) {
|
|
703
|
+
return right;
|
|
704
|
+
}
|
|
705
|
+
if (left.kind !== RVKind.Reference || right.kind !== RVKind.Reference) {
|
|
706
|
+
return ERRORS.VALUE;
|
|
707
|
+
}
|
|
708
|
+
if (left.areas.length !== 1 || right.areas.length !== 1) {
|
|
709
|
+
return ERRORS.REF;
|
|
710
|
+
}
|
|
711
|
+
const la = left.areas[0];
|
|
712
|
+
const ra = right.areas[0];
|
|
713
|
+
if (la.sheet.toLowerCase() !== ra.sheet.toLowerCase()) {
|
|
714
|
+
return ERRORS.REF;
|
|
715
|
+
}
|
|
716
|
+
// Bounding rectangle that spans both areas — this is the union /
|
|
717
|
+
// range-op semantics (Excel), distinct from intersection which uses
|
|
718
|
+
// min/max in the opposite direction.
|
|
719
|
+
const top = Math.min(la.top, ra.top);
|
|
720
|
+
const left_ = Math.min(la.left, ra.left);
|
|
721
|
+
const bottom = Math.max(la.bottom, ra.bottom);
|
|
722
|
+
const right_ = Math.max(la.right, ra.right);
|
|
723
|
+
return rvRef(la.sheet, top, left_, bottom, right_);
|
|
724
|
+
}
|
|
725
|
+
function applyScalarBinaryOp(op, left, right) {
|
|
726
|
+
if (isError(left)) {
|
|
727
|
+
return left;
|
|
728
|
+
}
|
|
729
|
+
if (isError(right)) {
|
|
730
|
+
return right;
|
|
731
|
+
}
|
|
732
|
+
// Concatenation
|
|
733
|
+
if (op === "&") {
|
|
734
|
+
const lStr = toStringRV(left);
|
|
735
|
+
const rStr = toStringRV(right);
|
|
736
|
+
return rvString(lStr + rStr);
|
|
737
|
+
}
|
|
738
|
+
// Comparison
|
|
739
|
+
if (op === "=" || op === "<>" || op === "<" || op === ">" || op === "<=" || op === ">=") {
|
|
740
|
+
return rvBoolean(compareScalars(left, right, op));
|
|
741
|
+
}
|
|
742
|
+
// Arithmetic
|
|
743
|
+
const lNum = toNumberRV(left);
|
|
744
|
+
if (isError(lNum)) {
|
|
745
|
+
return lNum;
|
|
746
|
+
}
|
|
747
|
+
const rNum = toNumberRV(right);
|
|
748
|
+
if (isError(rNum)) {
|
|
749
|
+
return rNum;
|
|
750
|
+
}
|
|
751
|
+
let result;
|
|
752
|
+
switch (op) {
|
|
753
|
+
case "+":
|
|
754
|
+
result = lNum.value + rNum.value;
|
|
755
|
+
break;
|
|
756
|
+
case "-":
|
|
757
|
+
result = lNum.value - rNum.value;
|
|
758
|
+
break;
|
|
759
|
+
case "*":
|
|
760
|
+
result = lNum.value * rNum.value;
|
|
761
|
+
break;
|
|
762
|
+
case "/":
|
|
763
|
+
if (rNum.value === 0) {
|
|
764
|
+
return ERRORS.DIV0;
|
|
765
|
+
}
|
|
766
|
+
result = lNum.value / rNum.value;
|
|
767
|
+
break;
|
|
768
|
+
case "^":
|
|
769
|
+
// Excel distinguishes `0 ^ n` for n < 0 (→ #DIV/0!, since it's
|
|
770
|
+
// semantically 1/0) from other overflows (→ #NUM!). The generic
|
|
771
|
+
// `isFinite` check below loses that distinction, so route the
|
|
772
|
+
// division-by-zero case explicitly first. 0^0 is conventionally 1
|
|
773
|
+
// (matches Excel and POWER()).
|
|
774
|
+
if (lNum.value === 0) {
|
|
775
|
+
if (rNum.value < 0) {
|
|
776
|
+
return ERRORS.DIV0;
|
|
777
|
+
}
|
|
778
|
+
if (rNum.value === 0) {
|
|
779
|
+
return rvNumber(1);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
result = Math.pow(lNum.value, rNum.value);
|
|
783
|
+
if (Number.isNaN(result)) {
|
|
784
|
+
// `Math.pow(-1, 0.5)` etc. — complex result; Excel reports #NUM!.
|
|
785
|
+
return ERRORS.NUM;
|
|
786
|
+
}
|
|
787
|
+
break;
|
|
788
|
+
default:
|
|
789
|
+
return ERRORS.VALUE;
|
|
790
|
+
}
|
|
791
|
+
return !isFinite(result) ? ERRORS.NUM : rvNumber(result);
|
|
792
|
+
}
|
|
793
|
+
function compareScalars(left, right, op) {
|
|
794
|
+
// Normalize blanks to a neutral form of the opposing kind so formulas like
|
|
795
|
+
// `"" = A1` (where A1 is blank) compare equal. Without this normalisation
|
|
796
|
+
// Excel would route us to the cross-type tiebreak below.
|
|
797
|
+
const l = left.kind === RVKind.Blank
|
|
798
|
+
? right.kind === RVKind.String
|
|
799
|
+
? rvString("")
|
|
800
|
+
: right.kind === RVKind.Boolean
|
|
801
|
+
? rvBoolean(false)
|
|
802
|
+
: rvNumber(0)
|
|
803
|
+
: left;
|
|
804
|
+
const r = right.kind === RVKind.Blank
|
|
805
|
+
? left.kind === RVKind.String
|
|
806
|
+
? rvString("")
|
|
807
|
+
: left.kind === RVKind.Boolean
|
|
808
|
+
? rvBoolean(false)
|
|
809
|
+
: rvNumber(0)
|
|
810
|
+
: right;
|
|
811
|
+
let cmp;
|
|
812
|
+
if (l.kind === r.kind) {
|
|
813
|
+
cmp = compareScalarsSameKind(l, r);
|
|
814
|
+
if (!Number.isFinite(cmp)) {
|
|
815
|
+
cmp = 0;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
// Excel orders scalar kinds: Number < String < Boolean < Error/Blank.
|
|
820
|
+
const order = (v) => {
|
|
821
|
+
if (v.kind === RVKind.Number) {
|
|
822
|
+
return 0;
|
|
823
|
+
}
|
|
824
|
+
if (v.kind === RVKind.String) {
|
|
825
|
+
return 1;
|
|
826
|
+
}
|
|
827
|
+
if (v.kind === RVKind.Boolean) {
|
|
828
|
+
return 2;
|
|
829
|
+
}
|
|
830
|
+
return 3;
|
|
831
|
+
};
|
|
832
|
+
cmp = order(l) - order(r);
|
|
833
|
+
}
|
|
834
|
+
switch (op) {
|
|
835
|
+
case "=":
|
|
836
|
+
return cmp === 0;
|
|
837
|
+
case "<>":
|
|
838
|
+
return cmp !== 0;
|
|
839
|
+
case "<":
|
|
840
|
+
return cmp < 0;
|
|
841
|
+
case ">":
|
|
842
|
+
return cmp > 0;
|
|
843
|
+
case "<=":
|
|
844
|
+
return cmp <= 0;
|
|
845
|
+
case ">=":
|
|
846
|
+
return cmp >= 0;
|
|
847
|
+
default:
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function broadcastBinaryOp(op, left, right) {
|
|
852
|
+
const lArr = left.kind === RVKind.Array ? left : null;
|
|
853
|
+
const rArr = right.kind === RVKind.Array ? right : null;
|
|
854
|
+
const lRows = lArr ? lArr.height : 1;
|
|
855
|
+
const lCols = lArr ? lArr.width : 1;
|
|
856
|
+
const rRows = rArr ? rArr.height : 1;
|
|
857
|
+
const rCols = rArr ? rArr.width : 1;
|
|
858
|
+
const outRows = Math.max(lRows, rRows);
|
|
859
|
+
const outCols = Math.max(lCols, rCols);
|
|
860
|
+
if ((lRows !== 1 && rRows !== 1 && lRows !== rRows) ||
|
|
861
|
+
(lCols !== 1 && rCols !== 1 && lCols !== rCols)) {
|
|
862
|
+
return ERRORS.VALUE;
|
|
863
|
+
}
|
|
864
|
+
// Guard against pathological broadcasts (e.g. A:A * 1:1 = ~17B cells).
|
|
865
|
+
// 10M cells is well beyond any legitimate array use case.
|
|
866
|
+
if (outRows * outCols > 10000000) {
|
|
867
|
+
return ERRORS.CALC;
|
|
868
|
+
}
|
|
869
|
+
// Precompute scalar-broadcast values once outside the hot cell loop.
|
|
870
|
+
// When one side is a non-array `RuntimeValue`, it expands to the same
|
|
871
|
+
// `ScalarValue` for every (r, c); repeating `topLeft(left)` inside the
|
|
872
|
+
// inner loop (outRows × outCols calls) was pure overhead.
|
|
873
|
+
const lScalarFallback = lArr ? undefined : topLeft(left);
|
|
874
|
+
const rScalarFallback = rArr ? undefined : topLeft(right);
|
|
875
|
+
const rows = [];
|
|
876
|
+
for (let r = 0; r < outRows; r++) {
|
|
877
|
+
const row = [];
|
|
878
|
+
for (let c = 0; c < outCols; c++) {
|
|
879
|
+
const lR = lRows === 1 ? 0 : r;
|
|
880
|
+
const lC = lCols === 1 ? 0 : c;
|
|
881
|
+
const rR = rRows === 1 ? 0 : r;
|
|
882
|
+
const rC = rCols === 1 ? 0 : c;
|
|
883
|
+
// Array values are rectangular (normalised by rvArray) so direct
|
|
884
|
+
// indexing is safe; the previous `?? BLANK` fallback was defensive
|
|
885
|
+
// code that never triggered in practice but cost an optional chain
|
|
886
|
+
// per cell in a hot loop.
|
|
887
|
+
const lVal = lArr ? lArr.rows[lR][lC] : lScalarFallback;
|
|
888
|
+
const rVal = rArr ? rArr.rows[rR][rC] : rScalarFallback;
|
|
889
|
+
row.push(applyScalarBinaryOp(op, lVal, rVal));
|
|
890
|
+
}
|
|
891
|
+
rows.push(row);
|
|
892
|
+
}
|
|
893
|
+
// Propagate origin metadata
|
|
894
|
+
const originRow = lArr?.originRow ?? rArr?.originRow;
|
|
895
|
+
const originCol = lArr?.originCol ?? rArr?.originCol;
|
|
896
|
+
return rvArray(rows, originRow, originCol);
|
|
897
|
+
}
|
|
898
|
+
// ============================================================================
|
|
899
|
+
// Unary Operations
|
|
900
|
+
// ============================================================================
|
|
901
|
+
function evaluateUnaryOp(op, operandExpr, ctx, session) {
|
|
902
|
+
const rawVal = evaluate(operandExpr, ctx, session);
|
|
903
|
+
// @ implicit intersection
|
|
904
|
+
if (op === "@") {
|
|
905
|
+
const intersected = implicitIntersect(rawVal, ctx);
|
|
906
|
+
return dereferenceValue(intersected, ctx, session);
|
|
907
|
+
}
|
|
908
|
+
const val = dereferenceValue(rawVal, ctx, session);
|
|
909
|
+
if (val.kind === RVKind.Array) {
|
|
910
|
+
const rows = [];
|
|
911
|
+
for (const row of val.rows) {
|
|
912
|
+
rows.push(row.map(cell => applyScalarUnary(op, cell)));
|
|
913
|
+
}
|
|
914
|
+
return rvArray(rows, val.originRow, val.originCol);
|
|
915
|
+
}
|
|
916
|
+
return applyScalarUnary(op, topLeft(val));
|
|
917
|
+
}
|
|
918
|
+
function applyScalarUnary(op, val) {
|
|
919
|
+
if (isError(val)) {
|
|
920
|
+
return val;
|
|
921
|
+
}
|
|
922
|
+
const n = toNumberRV(val);
|
|
923
|
+
if (isError(n)) {
|
|
924
|
+
return n;
|
|
925
|
+
}
|
|
926
|
+
switch (op) {
|
|
927
|
+
case "-":
|
|
928
|
+
return rvNumber(-n.value);
|
|
929
|
+
case "+":
|
|
930
|
+
return n;
|
|
931
|
+
default:
|
|
932
|
+
return ERRORS.VALUE;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
// ============================================================================
|
|
936
|
+
// Percent
|
|
937
|
+
// ============================================================================
|
|
938
|
+
function evaluatePercent(operandExpr, ctx, session) {
|
|
939
|
+
const val = dereferenceValue(evaluate(operandExpr, ctx, session), ctx, session);
|
|
940
|
+
if (val.kind === RVKind.Array) {
|
|
941
|
+
const rows = [];
|
|
942
|
+
for (const row of val.rows) {
|
|
943
|
+
rows.push(row.map(cell => {
|
|
944
|
+
if (isError(cell)) {
|
|
945
|
+
return cell;
|
|
946
|
+
}
|
|
947
|
+
const n = toNumberRV(cell);
|
|
948
|
+
if (isError(n)) {
|
|
949
|
+
return n;
|
|
950
|
+
}
|
|
951
|
+
return rvNumber(n.value / 100);
|
|
952
|
+
}));
|
|
953
|
+
}
|
|
954
|
+
return rvArray(rows);
|
|
955
|
+
}
|
|
956
|
+
const scalar = topLeft(val);
|
|
957
|
+
if (isError(scalar)) {
|
|
958
|
+
return scalar;
|
|
959
|
+
}
|
|
960
|
+
const n = toNumberRV(scalar);
|
|
961
|
+
if (isError(n)) {
|
|
962
|
+
return n;
|
|
963
|
+
}
|
|
964
|
+
return rvNumber(n.value / 100);
|
|
965
|
+
}
|
|
966
|
+
// ============================================================================
|
|
967
|
+
// Function Call (Eager)
|
|
968
|
+
// ============================================================================
|
|
969
|
+
function evaluateCall(expr, ctx, session) {
|
|
970
|
+
// Reference functions: ROW, COLUMN, ROWS, COLUMNS
|
|
971
|
+
// (Accept _XLFN. prefixed names transparently.)
|
|
972
|
+
const canonical = stripFunctionPrefix(expr.name);
|
|
973
|
+
const refResult = tryEvaluateRefFunction(canonical, expr.args, ctx);
|
|
974
|
+
if (refResult !== undefined) {
|
|
975
|
+
return refResult;
|
|
976
|
+
}
|
|
977
|
+
// Reference-producing functions like INDIRECT/OFFSET yield a
|
|
978
|
+
// ReferenceValue at runtime. ROW/COLUMN/ROWS/COLUMNS need to inspect
|
|
979
|
+
// the resulting reference's address rather than its dereferenced value,
|
|
980
|
+
// so we evaluate the argument *without* dereferencing and extract the
|
|
981
|
+
// geometry directly. Only the 1-arg reference-only forms go down this
|
|
982
|
+
// path; scalar/array arguments still fall through to the eager fallback
|
|
983
|
+
// below, which returns #VALUE! for ROW/COLUMN and the correct count for
|
|
984
|
+
// ROWS/COLUMNS.
|
|
985
|
+
if (expr.args.length === 1 && isSimpleRefFunction(canonical)) {
|
|
986
|
+
const raw = evaluate(expr.args[0], ctx, session);
|
|
987
|
+
if (raw.kind === RVKind.Reference && raw.areas.length > 0) {
|
|
988
|
+
const area = raw.areas[0];
|
|
989
|
+
switch (canonical) {
|
|
990
|
+
case "ROW":
|
|
991
|
+
return rvNumber(area.top);
|
|
992
|
+
case "COLUMN":
|
|
993
|
+
return rvNumber(area.left);
|
|
994
|
+
case "ROWS":
|
|
995
|
+
return rvNumber(area.bottom - area.top + 1);
|
|
996
|
+
case "COLUMNS":
|
|
997
|
+
return rvNumber(area.right - area.left + 1);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
// ── Reference-aware functions (ISREF, CELL) ──
|
|
1002
|
+
// These inspect the argument's reference-ness rather than its dereferenced
|
|
1003
|
+
// value. Handled here so the raw BoundExpr / ReferenceValue is visible.
|
|
1004
|
+
if (canonical === "ISREF") {
|
|
1005
|
+
return evaluateISREF(expr.args, ctx, session);
|
|
1006
|
+
}
|
|
1007
|
+
if (canonical === "CELL") {
|
|
1008
|
+
return evaluateCELL(expr.args, ctx, session);
|
|
1009
|
+
}
|
|
1010
|
+
// Evaluate all arguments eagerly and dereference references
|
|
1011
|
+
const args = expr.args.map(arg => dereferenceValue(evaluate(arg, ctx, session), ctx, session));
|
|
1012
|
+
// Look up function
|
|
1013
|
+
const desc = lookupFunction(expr.name);
|
|
1014
|
+
if (desc) {
|
|
1015
|
+
// Validate arity — produce #VALUE! for wrong argument count
|
|
1016
|
+
if (args.length < desc.minArity || args.length > desc.maxArity) {
|
|
1017
|
+
return ERRORS.VALUE;
|
|
1018
|
+
}
|
|
1019
|
+
// Context-aware overrides for functions that need evaluator state
|
|
1020
|
+
switch (canonical) {
|
|
1021
|
+
case "SHEET": {
|
|
1022
|
+
// SHEET() → current sheet number; SHEET(ref) → sheet number of ref
|
|
1023
|
+
if (args.length === 0) {
|
|
1024
|
+
const idx = ctx.snapshot.worksheets.findIndex(ws => ws.name.toLowerCase() === ctx.currentSheet.toLowerCase());
|
|
1025
|
+
return rvNumber(idx >= 0 ? idx + 1 : 1);
|
|
1026
|
+
}
|
|
1027
|
+
return desc.invoke(args);
|
|
1028
|
+
}
|
|
1029
|
+
case "SHEETS": {
|
|
1030
|
+
// SHEETS() → total sheet count
|
|
1031
|
+
if (args.length === 0) {
|
|
1032
|
+
return rvNumber(ctx.snapshot.worksheets.length);
|
|
1033
|
+
}
|
|
1034
|
+
return desc.invoke(args);
|
|
1035
|
+
}
|
|
1036
|
+
case "ISFORMULA": {
|
|
1037
|
+
// ISFORMULA requires a reference argument. When the raw argument is
|
|
1038
|
+
// a CellRef/AreaRef we can look up the underlying cell's formulaKind.
|
|
1039
|
+
// Any other shape (literal, computed value, etc.) yields #N/A per
|
|
1040
|
+
// Excel's behavior for non-reference arguments.
|
|
1041
|
+
const raw = expr.args[0];
|
|
1042
|
+
if (raw && raw.kind === BoundExprKind.CellRef) {
|
|
1043
|
+
const ws = ctx.snapshot.worksheetsByName.get(raw.sheet.toLowerCase());
|
|
1044
|
+
if (!ws) {
|
|
1045
|
+
return ERRORS.REF;
|
|
1046
|
+
}
|
|
1047
|
+
const cell = ws.cells.get(snapshotCellKey(raw.row, raw.col));
|
|
1048
|
+
return rvBoolean(cell !== undefined && cell.formulaKind !== "none");
|
|
1049
|
+
}
|
|
1050
|
+
if (raw && raw.kind === BoundExprKind.AreaRef) {
|
|
1051
|
+
const ws = ctx.snapshot.worksheetsByName.get(raw.sheet.toLowerCase());
|
|
1052
|
+
if (!ws) {
|
|
1053
|
+
return ERRORS.REF;
|
|
1054
|
+
}
|
|
1055
|
+
// ISFORMULA on an area ref inspects the top-left cell.
|
|
1056
|
+
const cell = ws.cells.get(snapshotCellKey(raw.top, raw.left));
|
|
1057
|
+
return rvBoolean(cell !== undefined && cell.formulaKind !== "none");
|
|
1058
|
+
}
|
|
1059
|
+
return ERRORS.NA;
|
|
1060
|
+
}
|
|
1061
|
+
case "FORMULATEXT": {
|
|
1062
|
+
// FORMULATEXT returns the formula source text at the referenced cell,
|
|
1063
|
+
// or #N/A if the cell has no formula. Non-reference arguments also
|
|
1064
|
+
// yield #N/A.
|
|
1065
|
+
const raw = expr.args[0];
|
|
1066
|
+
if (raw && raw.kind === BoundExprKind.CellRef) {
|
|
1067
|
+
const ws = ctx.snapshot.worksheetsByName.get(raw.sheet.toLowerCase());
|
|
1068
|
+
if (!ws) {
|
|
1069
|
+
return ERRORS.REF;
|
|
1070
|
+
}
|
|
1071
|
+
const cell = ws.cells.get(snapshotCellKey(raw.row, raw.col));
|
|
1072
|
+
if (cell && cell.formulaKind !== "none" && cell.formula !== undefined) {
|
|
1073
|
+
return rvString(`=${cell.formula}`);
|
|
1074
|
+
}
|
|
1075
|
+
return ERRORS.NA;
|
|
1076
|
+
}
|
|
1077
|
+
if (raw && raw.kind === BoundExprKind.AreaRef) {
|
|
1078
|
+
const ws = ctx.snapshot.worksheetsByName.get(raw.sheet.toLowerCase());
|
|
1079
|
+
if (!ws) {
|
|
1080
|
+
return ERRORS.REF;
|
|
1081
|
+
}
|
|
1082
|
+
const cell = ws.cells.get(snapshotCellKey(raw.top, raw.left));
|
|
1083
|
+
if (cell && cell.formulaKind !== "none" && cell.formula !== undefined) {
|
|
1084
|
+
return rvString(`=${cell.formula}`);
|
|
1085
|
+
}
|
|
1086
|
+
return ERRORS.NA;
|
|
1087
|
+
}
|
|
1088
|
+
return ERRORS.NA;
|
|
1089
|
+
}
|
|
1090
|
+
default:
|
|
1091
|
+
return desc.invoke(args);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
// Check if name resolves to a lambda (defined name or local binding)
|
|
1095
|
+
const lambda = resolveLambdaName(expr.name, args, ctx, session);
|
|
1096
|
+
if (lambda !== undefined) {
|
|
1097
|
+
return lambda;
|
|
1098
|
+
}
|
|
1099
|
+
return ERRORS.NAME;
|
|
1100
|
+
}
|
|
1101
|
+
// ============================================================================
|
|
1102
|
+
// Special Form Call (Lazy)
|
|
1103
|
+
// ============================================================================
|
|
1104
|
+
function evaluateSpecialCall(expr, ctx, session) {
|
|
1105
|
+
switch (expr.name) {
|
|
1106
|
+
case "IF":
|
|
1107
|
+
return evaluateIF(expr.args, ctx, session);
|
|
1108
|
+
case "IFERROR":
|
|
1109
|
+
return evaluateIFERROR(expr.args, ctx, session);
|
|
1110
|
+
case "IFNA":
|
|
1111
|
+
return evaluateIFNA(expr.args, ctx, session);
|
|
1112
|
+
case "IFS":
|
|
1113
|
+
return evaluateIFS(expr.args, ctx, session);
|
|
1114
|
+
case "SWITCH":
|
|
1115
|
+
return evaluateSWITCH(expr.args, ctx, session);
|
|
1116
|
+
case "CHOOSE":
|
|
1117
|
+
return evaluateCHOOSE(expr.args, ctx, session);
|
|
1118
|
+
case "LET":
|
|
1119
|
+
return evaluateLET(expr.args, ctx, session);
|
|
1120
|
+
case "LAMBDA":
|
|
1121
|
+
return evaluateLAMBDA(expr.args, ctx);
|
|
1122
|
+
case "INDIRECT":
|
|
1123
|
+
return evaluateINDIRECT(expr.args, ctx, session);
|
|
1124
|
+
case "OFFSET":
|
|
1125
|
+
return evaluateOFFSET(expr.args, ctx, session);
|
|
1126
|
+
case "MAP":
|
|
1127
|
+
case "REDUCE":
|
|
1128
|
+
case "SCAN":
|
|
1129
|
+
case "MAKEARRAY":
|
|
1130
|
+
case "BYROW":
|
|
1131
|
+
case "BYCOL":
|
|
1132
|
+
return evaluateHigherOrder(expr.name, expr.args, ctx, session);
|
|
1133
|
+
default:
|
|
1134
|
+
return ERRORS.VALUE;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
// ============================================================================
|
|
1138
|
+
// Special Forms Implementation
|
|
1139
|
+
// ============================================================================
|
|
1140
|
+
function evaluateIF(args, ctx, session) {
|
|
1141
|
+
if (args.length < 2) {
|
|
1142
|
+
return ERRORS.VALUE;
|
|
1143
|
+
}
|
|
1144
|
+
const condRaw = evalDeref(args[0], ctx, session);
|
|
1145
|
+
// Array condition: element-wise IF. Excel's dynamic-array mode makes
|
|
1146
|
+
// `IF({TRUE,FALSE,TRUE}, "Y", "N")` return `{"Y","N","Y"}`. The branches
|
|
1147
|
+
// are evaluated eagerly — Excel does this too because array broadcasting
|
|
1148
|
+
// requires both shapes to be known — and each cell in the output picks
|
|
1149
|
+
// from the corresponding cell of the chosen branch (with scalar branches
|
|
1150
|
+
// broadcasting to fill the condition array's shape).
|
|
1151
|
+
if (condRaw.kind === RVKind.Array) {
|
|
1152
|
+
const trueVal = evalDeref(args[1], ctx, session);
|
|
1153
|
+
const falseVal = args.length > 2 ? evalDeref(args[2], ctx, session) : rvBoolean(false);
|
|
1154
|
+
const rows = [];
|
|
1155
|
+
for (let r = 0; r < condRaw.height; r++) {
|
|
1156
|
+
const outRow = [];
|
|
1157
|
+
for (let c = 0; c < condRaw.width; c++) {
|
|
1158
|
+
const cell = condRaw.rows[r][c];
|
|
1159
|
+
if (cell.kind === RVKind.Error) {
|
|
1160
|
+
outRow.push(cell);
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
const b = toBooleanRV(cell);
|
|
1164
|
+
if (b.kind === RVKind.Error) {
|
|
1165
|
+
outRow.push(b);
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
const branch = b.value ? trueVal : falseVal;
|
|
1169
|
+
outRow.push(pickCellBroadcast(branch, r, c));
|
|
1170
|
+
}
|
|
1171
|
+
rows.push(outRow);
|
|
1172
|
+
}
|
|
1173
|
+
return rvArray(rows);
|
|
1174
|
+
}
|
|
1175
|
+
const cond = topLeft(condRaw);
|
|
1176
|
+
if (isError(cond)) {
|
|
1177
|
+
return cond;
|
|
1178
|
+
}
|
|
1179
|
+
const bool = toBooleanRV(cond);
|
|
1180
|
+
if (isError(bool)) {
|
|
1181
|
+
return bool;
|
|
1182
|
+
}
|
|
1183
|
+
if (bool.value) {
|
|
1184
|
+
return evaluate(args[1], ctx, session);
|
|
1185
|
+
}
|
|
1186
|
+
return args.length > 2 ? evaluate(args[2], ctx, session) : rvBoolean(false);
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Pick a scalar from `branch` corresponding to grid position (r, c), with
|
|
1190
|
+
* broadcasting: scalar branches are repeated; smaller arrays are indexed
|
|
1191
|
+
* modulo their bounds (out-of-range → BLANK) matching Excel's array
|
|
1192
|
+
* alignment rules for IF/IFS/etc.
|
|
1193
|
+
*/
|
|
1194
|
+
function pickCellBroadcast(branch, r, c) {
|
|
1195
|
+
if (branch.kind !== RVKind.Array) {
|
|
1196
|
+
return topLeft(branch);
|
|
1197
|
+
}
|
|
1198
|
+
const row = r < branch.height ? r : branch.height === 1 ? 0 : -1;
|
|
1199
|
+
const col = c < branch.width ? c : branch.width === 1 ? 0 : -1;
|
|
1200
|
+
if (row < 0 || col < 0) {
|
|
1201
|
+
// Misaligned array branch (smaller than the condition, and not a
|
|
1202
|
+
// broadcastable 1-row / 1-column shape). Excel fills the gaps with
|
|
1203
|
+
// `#N/A` rather than BLANK, so downstream consumers can distinguish
|
|
1204
|
+
// "branch didn't cover this cell" from "branch actually returned
|
|
1205
|
+
// empty". (R6-P1-7)
|
|
1206
|
+
return ERRORS.NA;
|
|
1207
|
+
}
|
|
1208
|
+
return branch.rows[row][col];
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Shared array-aware error replacement used by IFERROR and IFNA. Scans `val`
|
|
1212
|
+
* for cells matching `isMatch`; if any are found, replaces each with the
|
|
1213
|
+
* top-left scalar of `replacement` (lazily evaluated). Scalar inputs follow
|
|
1214
|
+
* the same match-or-pass-through logic.
|
|
1215
|
+
*/
|
|
1216
|
+
function replaceErrorsIn(val, args, ctx, session, isMatch) {
|
|
1217
|
+
if (val.kind === RVKind.Array) {
|
|
1218
|
+
let anyMatch = false;
|
|
1219
|
+
for (const row of val.rows) {
|
|
1220
|
+
for (const cell of row) {
|
|
1221
|
+
if (cell.kind === RVKind.Error && isMatch(cell)) {
|
|
1222
|
+
anyMatch = true;
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
if (anyMatch) {
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (!anyMatch) {
|
|
1231
|
+
return val;
|
|
1232
|
+
}
|
|
1233
|
+
const replaceScalar = topLeft(evalDeref(args[1], ctx, session));
|
|
1234
|
+
const rows = [];
|
|
1235
|
+
for (const row of val.rows) {
|
|
1236
|
+
const newRow = [];
|
|
1237
|
+
for (const cell of row) {
|
|
1238
|
+
newRow.push(cell.kind === RVKind.Error && isMatch(cell) ? replaceScalar : cell);
|
|
1239
|
+
}
|
|
1240
|
+
rows.push(newRow);
|
|
1241
|
+
}
|
|
1242
|
+
return rvArray(rows);
|
|
1243
|
+
}
|
|
1244
|
+
return isError(val) && isMatch(val) ? evalDeref(args[1], ctx, session) : val;
|
|
1245
|
+
}
|
|
1246
|
+
function evaluateIFERROR(args, ctx, session) {
|
|
1247
|
+
if (args.length < 2) {
|
|
1248
|
+
return ERRORS.VALUE;
|
|
1249
|
+
}
|
|
1250
|
+
const val = evalDeref(args[0], ctx, session);
|
|
1251
|
+
return replaceErrorsIn(val, args, ctx, session, () => true);
|
|
1252
|
+
}
|
|
1253
|
+
function evaluateIFNA(args, ctx, session) {
|
|
1254
|
+
if (args.length < 2) {
|
|
1255
|
+
return ERRORS.VALUE;
|
|
1256
|
+
}
|
|
1257
|
+
const val = evalDeref(args[0], ctx, session);
|
|
1258
|
+
return replaceErrorsIn(val, args, ctx, session, err => err.code === "#N/A");
|
|
1259
|
+
}
|
|
1260
|
+
function evaluateIFS(args, ctx, session) {
|
|
1261
|
+
if (args.length < 2) {
|
|
1262
|
+
return ERRORS.VALUE;
|
|
1263
|
+
}
|
|
1264
|
+
// Excel requires IFS args to come in test/value pairs. Odd-length
|
|
1265
|
+
// arg lists imply a trailing test with no value and are #N/A.
|
|
1266
|
+
if (args.length % 2 !== 0) {
|
|
1267
|
+
return ERRORS.NA;
|
|
1268
|
+
}
|
|
1269
|
+
for (let i = 0; i < args.length - 1; i += 2) {
|
|
1270
|
+
const cond = topLeft(evalDeref(args[i], ctx, session));
|
|
1271
|
+
if (isError(cond)) {
|
|
1272
|
+
return cond;
|
|
1273
|
+
}
|
|
1274
|
+
const bool = toBooleanRV(cond);
|
|
1275
|
+
if (isError(bool)) {
|
|
1276
|
+
return bool;
|
|
1277
|
+
}
|
|
1278
|
+
if (bool.value) {
|
|
1279
|
+
return evaluate(args[i + 1], ctx, session);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return ERRORS.NA;
|
|
1283
|
+
}
|
|
1284
|
+
function evaluateSWITCH(args, ctx, session) {
|
|
1285
|
+
if (args.length < 3) {
|
|
1286
|
+
return ERRORS.VALUE;
|
|
1287
|
+
}
|
|
1288
|
+
const expr = topLeft(evalDeref(args[0], ctx, session));
|
|
1289
|
+
if (isError(expr)) {
|
|
1290
|
+
return expr;
|
|
1291
|
+
}
|
|
1292
|
+
for (let i = 1; i < args.length - 1; i += 2) {
|
|
1293
|
+
const caseVal = topLeft(evalDeref(args[i], ctx, session));
|
|
1294
|
+
if (scalarEquals(expr, caseVal)) {
|
|
1295
|
+
return evaluate(args[i + 1], ctx, session);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (args.length % 2 === 0) {
|
|
1299
|
+
return evaluate(args[args.length - 1], ctx, session);
|
|
1300
|
+
}
|
|
1301
|
+
return ERRORS.NA;
|
|
1302
|
+
}
|
|
1303
|
+
function evaluateCHOOSE(args, ctx, session) {
|
|
1304
|
+
if (args.length < 2) {
|
|
1305
|
+
return ERRORS.VALUE;
|
|
1306
|
+
}
|
|
1307
|
+
const idxVal = topLeft(evalDeref(args[0], ctx, session));
|
|
1308
|
+
if (isError(idxVal)) {
|
|
1309
|
+
return idxVal;
|
|
1310
|
+
}
|
|
1311
|
+
const num = toNumberRV(idxVal);
|
|
1312
|
+
if (isError(num)) {
|
|
1313
|
+
return num;
|
|
1314
|
+
}
|
|
1315
|
+
const idx = Math.floor(num.value);
|
|
1316
|
+
if (idx < 1 || idx >= args.length) {
|
|
1317
|
+
return ERRORS.VALUE;
|
|
1318
|
+
}
|
|
1319
|
+
return evaluate(args[idx], ctx, session);
|
|
1320
|
+
}
|
|
1321
|
+
function evaluateLET(args, ctx, session) {
|
|
1322
|
+
if (args.length < 3 || args.length % 2 !== 1) {
|
|
1323
|
+
return ERRORS.VALUE;
|
|
1324
|
+
}
|
|
1325
|
+
const prevBindings = ctx.localBindings;
|
|
1326
|
+
const newBindings = new Map(prevBindings);
|
|
1327
|
+
try {
|
|
1328
|
+
const pairCount = (args.length - 1) / 2;
|
|
1329
|
+
for (let i = 0; i < pairCount; i++) {
|
|
1330
|
+
const nameExpr = args[i * 2];
|
|
1331
|
+
const valueExpr = args[i * 2 + 1];
|
|
1332
|
+
if (nameExpr.kind !== BoundExprKind.NameExpr) {
|
|
1333
|
+
return ERRORS.VALUE;
|
|
1334
|
+
}
|
|
1335
|
+
ctx.localBindings = newBindings;
|
|
1336
|
+
const val = evaluate(valueExpr, ctx, session);
|
|
1337
|
+
newBindings.set(nameExpr.upperName, val);
|
|
1338
|
+
}
|
|
1339
|
+
ctx.localBindings = newBindings;
|
|
1340
|
+
return evaluate(args[args.length - 1], ctx, session);
|
|
1341
|
+
}
|
|
1342
|
+
finally {
|
|
1343
|
+
ctx.localBindings = prevBindings;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
function evaluateLAMBDA(args, ctx) {
|
|
1347
|
+
if (args.length < 1) {
|
|
1348
|
+
return ERRORS.VALUE;
|
|
1349
|
+
}
|
|
1350
|
+
const paramExprs = args.slice(0, -1);
|
|
1351
|
+
const bodyExpr = args[args.length - 1];
|
|
1352
|
+
const params = [];
|
|
1353
|
+
for (const p of paramExprs) {
|
|
1354
|
+
if (p.kind !== BoundExprKind.NameExpr) {
|
|
1355
|
+
return ERRORS.VALUE;
|
|
1356
|
+
}
|
|
1357
|
+
params.push(p.upperName);
|
|
1358
|
+
}
|
|
1359
|
+
return rvLambda(params, bodyExpr, ctx.localBindings ? new Map(ctx.localBindings) : undefined);
|
|
1360
|
+
}
|
|
1361
|
+
function evaluateINDIRECT(args, ctx, session) {
|
|
1362
|
+
if (args.length < 1) {
|
|
1363
|
+
return ERRORS.VALUE;
|
|
1364
|
+
}
|
|
1365
|
+
const refArg = evalDeref(args[0], ctx, session);
|
|
1366
|
+
const refText = toStringRV(topLeft(refArg));
|
|
1367
|
+
if (!refText) {
|
|
1368
|
+
return ERRORS.REF;
|
|
1369
|
+
}
|
|
1370
|
+
let a1 = true;
|
|
1371
|
+
if (args.length >= 2) {
|
|
1372
|
+
const a1Val = topLeft(evalDeref(args[1], ctx, session));
|
|
1373
|
+
a1 =
|
|
1374
|
+
!(a1Val.kind === RVKind.Boolean && !a1Val.value) &&
|
|
1375
|
+
!(a1Val.kind === RVKind.Number && a1Val.value === 0);
|
|
1376
|
+
}
|
|
1377
|
+
if (!a1) {
|
|
1378
|
+
// R1C1 — delegate to runtime R1C1 parser
|
|
1379
|
+
return resolveR1C1(refText, ctx, session);
|
|
1380
|
+
}
|
|
1381
|
+
// A1 style — parse and bind at runtime
|
|
1382
|
+
try {
|
|
1383
|
+
// Use NUL (U+0000) as the separator so sheet names containing `__`
|
|
1384
|
+
// can't collide with distinct INDIRECT call sites. Neither formula
|
|
1385
|
+
// text nor an Excel sheet name is allowed to contain `\0`, so the
|
|
1386
|
+
// key is unambiguous. (R6-P1-12)
|
|
1387
|
+
const cacheKey = `${ctx.currentSheet}\u0000${refText}`;
|
|
1388
|
+
let bound = session.indirectAstCache.get(cacheKey);
|
|
1389
|
+
if (bound) {
|
|
1390
|
+
// LRU touch: delete-then-set moves the hit entry to the Map's
|
|
1391
|
+
// insertion-order tail so the oldest entry is always `keys().next()`.
|
|
1392
|
+
session.indirectAstCache.delete(cacheKey);
|
|
1393
|
+
session.indirectAstCache.set(cacheKey, bound);
|
|
1394
|
+
}
|
|
1395
|
+
else {
|
|
1396
|
+
const tokens = tokenize(refText);
|
|
1397
|
+
const ast = parse(tokens);
|
|
1398
|
+
const bindCtx = { snapshot: ctx.snapshot, currentSheet: ctx.currentSheet };
|
|
1399
|
+
bound = bind(ast, bindCtx);
|
|
1400
|
+
// Bound the cache so an adversarial formula that generates a fresh
|
|
1401
|
+
// INDIRECT string every call can't grow session memory unbounded.
|
|
1402
|
+
// The cap matches `astCache` in calculate-formulas-impl.ts. (R6
|
|
1403
|
+
// architectural note #6)
|
|
1404
|
+
if (session.indirectAstCache.size >= 10000) {
|
|
1405
|
+
const oldestKey = session.indirectAstCache.keys().next().value;
|
|
1406
|
+
if (oldestKey !== undefined) {
|
|
1407
|
+
session.indirectAstCache.delete(oldestKey);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
session.indirectAstCache.set(cacheKey, bound);
|
|
1411
|
+
}
|
|
1412
|
+
return evaluate(bound, ctx, session);
|
|
1413
|
+
}
|
|
1414
|
+
catch {
|
|
1415
|
+
return ERRORS.REF;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
function evaluateOFFSET(args, ctx, session) {
|
|
1419
|
+
if (args.length < 3) {
|
|
1420
|
+
return ERRORS.VALUE;
|
|
1421
|
+
}
|
|
1422
|
+
const refExpr = args[0];
|
|
1423
|
+
let baseRow;
|
|
1424
|
+
let baseCol;
|
|
1425
|
+
let baseSheet;
|
|
1426
|
+
// Remember the base reference's shape — Excel's OFFSET uses it as the
|
|
1427
|
+
// default height/width when those optional arguments are omitted.
|
|
1428
|
+
let baseHeight;
|
|
1429
|
+
let baseWidth;
|
|
1430
|
+
if (refExpr.kind === BoundExprKind.CellRef) {
|
|
1431
|
+
baseRow = refExpr.row;
|
|
1432
|
+
baseCol = refExpr.col;
|
|
1433
|
+
baseSheet = refExpr.sheet;
|
|
1434
|
+
baseHeight = 1;
|
|
1435
|
+
baseWidth = 1;
|
|
1436
|
+
}
|
|
1437
|
+
else if (refExpr.kind === BoundExprKind.AreaRef) {
|
|
1438
|
+
baseRow = refExpr.top;
|
|
1439
|
+
baseCol = refExpr.left;
|
|
1440
|
+
baseSheet = refExpr.sheet;
|
|
1441
|
+
baseHeight = refExpr.bottom - refExpr.top + 1;
|
|
1442
|
+
baseWidth = refExpr.right - refExpr.left + 1;
|
|
1443
|
+
}
|
|
1444
|
+
else {
|
|
1445
|
+
return ERRORS.VALUE;
|
|
1446
|
+
}
|
|
1447
|
+
const rowsVal = topLeft(evalDeref(args[1], ctx, session));
|
|
1448
|
+
const rowsNum = toNumberRV(rowsVal);
|
|
1449
|
+
if (isError(rowsNum)) {
|
|
1450
|
+
return rowsNum;
|
|
1451
|
+
}
|
|
1452
|
+
const colsVal = topLeft(evalDeref(args[2], ctx, session));
|
|
1453
|
+
const colsNum = toNumberRV(colsVal);
|
|
1454
|
+
if (isError(colsNum)) {
|
|
1455
|
+
return colsNum;
|
|
1456
|
+
}
|
|
1457
|
+
// Excel truncates fractional rows/cols toward zero (not floor). Without
|
|
1458
|
+
// the `Math.trunc`, `OFFSET(A5, -0.7, 0)` would resolve to row 4.3 and
|
|
1459
|
+
// then fail the Map lookup silently, returning BLANK instead of A5.
|
|
1460
|
+
const newRow = baseRow + Math.trunc(rowsNum.value);
|
|
1461
|
+
const newCol = baseCol + Math.trunc(colsNum.value);
|
|
1462
|
+
if (newRow < 1 || newCol < 1 || newRow > 1048576 || newCol > 16384) {
|
|
1463
|
+
return ERRORS.REF;
|
|
1464
|
+
}
|
|
1465
|
+
// Default height / width come from the base reference itself. OFFSET
|
|
1466
|
+
// only shrinks/expands when the caller passes an explicit non-missing
|
|
1467
|
+
// fourth/fifth argument. A `MissingNode` (compile-time "omitted
|
|
1468
|
+
// argument") binds to a `null`-valued literal; we treat that the same
|
|
1469
|
+
// as "no argument provided" so `OFFSET(A1:C3, 0, 0, , )` keeps the
|
|
1470
|
+
// 3-row × 3-col span instead of collapsing to #REF! with `height = 0`.
|
|
1471
|
+
const isOmitted = (a) => a.kind === BoundExprKind.Literal && a.value === null && a.errorCode === undefined;
|
|
1472
|
+
let height = baseHeight;
|
|
1473
|
+
let width = baseWidth;
|
|
1474
|
+
if (args.length > 3 && !isOmitted(args[3])) {
|
|
1475
|
+
const h = toNumberRV(topLeft(evalDeref(args[3], ctx, session)));
|
|
1476
|
+
if (isError(h)) {
|
|
1477
|
+
return h;
|
|
1478
|
+
}
|
|
1479
|
+
height = Math.trunc(h.value);
|
|
1480
|
+
if (height === 0) {
|
|
1481
|
+
return ERRORS.REF;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if (args.length > 4 && !isOmitted(args[4])) {
|
|
1485
|
+
const w = toNumberRV(topLeft(evalDeref(args[4], ctx, session)));
|
|
1486
|
+
if (isError(w)) {
|
|
1487
|
+
return w;
|
|
1488
|
+
}
|
|
1489
|
+
width = Math.trunc(w.value);
|
|
1490
|
+
if (width === 0) {
|
|
1491
|
+
return ERRORS.REF;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
// Resolve range coordinates — negative height/width extend upward/leftward
|
|
1495
|
+
let top = newRow;
|
|
1496
|
+
let bottom = newRow + height - 1;
|
|
1497
|
+
if (height < 0) {
|
|
1498
|
+
top = newRow + height + 1;
|
|
1499
|
+
bottom = newRow;
|
|
1500
|
+
}
|
|
1501
|
+
let left = newCol;
|
|
1502
|
+
let right = newCol + width - 1;
|
|
1503
|
+
if (width < 0) {
|
|
1504
|
+
left = newCol + width + 1;
|
|
1505
|
+
right = newCol;
|
|
1506
|
+
}
|
|
1507
|
+
if (top < 1 || left < 1 || bottom > 1048576 || right > 16384) {
|
|
1508
|
+
return ERRORS.REF;
|
|
1509
|
+
}
|
|
1510
|
+
if (top === bottom && left === right) {
|
|
1511
|
+
return getCellValue(baseSheet, top, left, ctx, session);
|
|
1512
|
+
}
|
|
1513
|
+
return buildRangeArray(ctx, session, baseSheet, top, left, bottom, right);
|
|
1514
|
+
}
|
|
1515
|
+
// ============================================================================
|
|
1516
|
+
// Higher-Order Functions
|
|
1517
|
+
// ============================================================================
|
|
1518
|
+
function evaluateHigherOrder(name, args, ctx, session) {
|
|
1519
|
+
switch (name) {
|
|
1520
|
+
case "MAP":
|
|
1521
|
+
return evaluateMAP(args, ctx, session);
|
|
1522
|
+
case "REDUCE":
|
|
1523
|
+
return evaluateREDUCE(args, ctx, session);
|
|
1524
|
+
case "SCAN":
|
|
1525
|
+
return evaluateSCAN(args, ctx, session);
|
|
1526
|
+
case "MAKEARRAY":
|
|
1527
|
+
return evaluateMAKEARRAY(args, ctx, session);
|
|
1528
|
+
case "BYROW":
|
|
1529
|
+
return evaluateBYROW(args, ctx, session);
|
|
1530
|
+
case "BYCOL":
|
|
1531
|
+
return evaluateBYCOL(args, ctx, session);
|
|
1532
|
+
default:
|
|
1533
|
+
return ERRORS.VALUE;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
function invokeLambda(lambda, lambdaArgs, ctx, session) {
|
|
1537
|
+
if (lambdaArgs.length !== lambda.params.length) {
|
|
1538
|
+
return ERRORS.VALUE;
|
|
1539
|
+
}
|
|
1540
|
+
if (session.lambdaDepth >= 256) {
|
|
1541
|
+
return ERRORS.NUM;
|
|
1542
|
+
}
|
|
1543
|
+
session.lambdaDepth++;
|
|
1544
|
+
try {
|
|
1545
|
+
const prevBindings = ctx.localBindings;
|
|
1546
|
+
const newBindings = new Map(lambda.closureBindings);
|
|
1547
|
+
for (let i = 0; i < lambda.params.length; i++) {
|
|
1548
|
+
newBindings.set(lambda.params[i], lambdaArgs[i]);
|
|
1549
|
+
}
|
|
1550
|
+
ctx.localBindings = newBindings;
|
|
1551
|
+
try {
|
|
1552
|
+
return dereferenceValue(evaluate(lambda.body, ctx, session), ctx, session);
|
|
1553
|
+
}
|
|
1554
|
+
finally {
|
|
1555
|
+
ctx.localBindings = prevBindings;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
finally {
|
|
1559
|
+
session.lambdaDepth--;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
function evaluateMAP(args, ctx, session) {
|
|
1563
|
+
if (args.length < 2) {
|
|
1564
|
+
return ERRORS.VALUE;
|
|
1565
|
+
}
|
|
1566
|
+
const arrVal = evalDeref(args[0], ctx, session);
|
|
1567
|
+
const lambdaVal = evalDeref(args[args.length - 1], ctx, session);
|
|
1568
|
+
if (!isLambda(lambdaVal)) {
|
|
1569
|
+
return ERRORS.VALUE;
|
|
1570
|
+
}
|
|
1571
|
+
if (arrVal.kind !== RVKind.Array) {
|
|
1572
|
+
return invokeLambda(lambdaVal, [arrVal], ctx, session);
|
|
1573
|
+
}
|
|
1574
|
+
const rows = [];
|
|
1575
|
+
for (const row of arrVal.rows) {
|
|
1576
|
+
const outRow = [];
|
|
1577
|
+
for (const cell of row) {
|
|
1578
|
+
outRow.push(topLeft(invokeLambda(lambdaVal, [cell], ctx, session)));
|
|
1579
|
+
}
|
|
1580
|
+
rows.push(outRow);
|
|
1581
|
+
}
|
|
1582
|
+
return rvArray(rows);
|
|
1583
|
+
}
|
|
1584
|
+
function evaluateREDUCE(args, ctx, session) {
|
|
1585
|
+
if (args.length < 3) {
|
|
1586
|
+
return ERRORS.VALUE;
|
|
1587
|
+
}
|
|
1588
|
+
let acc = evalDeref(args[0], ctx, session);
|
|
1589
|
+
const arrVal = evalDeref(args[1], ctx, session);
|
|
1590
|
+
const lambdaVal = evalDeref(args[2], ctx, session);
|
|
1591
|
+
if (!isLambda(lambdaVal)) {
|
|
1592
|
+
return ERRORS.VALUE;
|
|
1593
|
+
}
|
|
1594
|
+
if (arrVal.kind === RVKind.Array) {
|
|
1595
|
+
for (const row of arrVal.rows) {
|
|
1596
|
+
for (const cell of row) {
|
|
1597
|
+
acc = invokeLambda(lambdaVal, [topLeft(acc), cell], ctx, session);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
else {
|
|
1602
|
+
// Scalar input: Excel treats the scalar as a 1×1 array and invokes the
|
|
1603
|
+
// reducer exactly once. Previously we returned `init` unchanged, which
|
|
1604
|
+
// silently broke `REDUCE(0, some_scalar, lambda)` callers.
|
|
1605
|
+
acc = invokeLambda(lambdaVal, [topLeft(acc), topLeft(arrVal)], ctx, session);
|
|
1606
|
+
}
|
|
1607
|
+
return acc;
|
|
1608
|
+
}
|
|
1609
|
+
function evaluateSCAN(args, ctx, session) {
|
|
1610
|
+
if (args.length < 3) {
|
|
1611
|
+
return ERRORS.VALUE;
|
|
1612
|
+
}
|
|
1613
|
+
let acc = evalDeref(args[0], ctx, session);
|
|
1614
|
+
const arrVal = evalDeref(args[1], ctx, session);
|
|
1615
|
+
const lambdaVal = evalDeref(args[2], ctx, session);
|
|
1616
|
+
if (!isLambda(lambdaVal)) {
|
|
1617
|
+
return ERRORS.VALUE;
|
|
1618
|
+
}
|
|
1619
|
+
const rows = [];
|
|
1620
|
+
if (arrVal.kind === RVKind.Array) {
|
|
1621
|
+
for (const row of arrVal.rows) {
|
|
1622
|
+
const outRow = [];
|
|
1623
|
+
for (const cell of row) {
|
|
1624
|
+
acc = invokeLambda(lambdaVal, [topLeft(acc), cell], ctx, session);
|
|
1625
|
+
outRow.push(topLeft(acc));
|
|
1626
|
+
}
|
|
1627
|
+
rows.push(outRow);
|
|
1628
|
+
}
|
|
1629
|
+
return rows.length > 0 ? rvArray(rows) : ERRORS.CALC;
|
|
1630
|
+
}
|
|
1631
|
+
// Scalar input: emit a single-cell array containing the one accumulated
|
|
1632
|
+
// value. Previously we returned #CALC! here, which was an artefact of the
|
|
1633
|
+
// array-only implementation path.
|
|
1634
|
+
const result = invokeLambda(lambdaVal, [topLeft(acc), topLeft(arrVal)], ctx, session);
|
|
1635
|
+
return rvArray([[topLeft(result)]]);
|
|
1636
|
+
}
|
|
1637
|
+
function evaluateMAKEARRAY(args, ctx, session) {
|
|
1638
|
+
if (args.length < 3) {
|
|
1639
|
+
return ERRORS.VALUE;
|
|
1640
|
+
}
|
|
1641
|
+
const rowsNum = toNumberRV(topLeft(evalDeref(args[0], ctx, session)));
|
|
1642
|
+
if (isError(rowsNum)) {
|
|
1643
|
+
return rowsNum;
|
|
1644
|
+
}
|
|
1645
|
+
const colsNum = toNumberRV(topLeft(evalDeref(args[1], ctx, session)));
|
|
1646
|
+
if (isError(colsNum)) {
|
|
1647
|
+
return colsNum;
|
|
1648
|
+
}
|
|
1649
|
+
const lambdaVal = evalDeref(args[2], ctx, session);
|
|
1650
|
+
if (!isLambda(lambdaVal)) {
|
|
1651
|
+
return ERRORS.VALUE;
|
|
1652
|
+
}
|
|
1653
|
+
// Truncate toward zero and reject non-positive / overflow sizes.
|
|
1654
|
+
// Without the cell-count cap the engine can silently allocate billions
|
|
1655
|
+
// of scalars before blowing the heap; matching the broadcast limit
|
|
1656
|
+
// keeps MAKEARRAY in line with the rest of the array pipeline.
|
|
1657
|
+
const rCount = Math.trunc(rowsNum.value);
|
|
1658
|
+
const cCount = Math.trunc(colsNum.value);
|
|
1659
|
+
if (rCount < 1 || cCount < 1) {
|
|
1660
|
+
return ERRORS.VALUE;
|
|
1661
|
+
}
|
|
1662
|
+
if (rCount * cCount > 10000000) {
|
|
1663
|
+
return ERRORS.NUM;
|
|
1664
|
+
}
|
|
1665
|
+
const rows = [];
|
|
1666
|
+
for (let r = 1; r <= rCount; r++) {
|
|
1667
|
+
const outRow = [];
|
|
1668
|
+
for (let c = 1; c <= cCount; c++) {
|
|
1669
|
+
outRow.push(topLeft(invokeLambda(lambdaVal, [rvNumber(r), rvNumber(c)], ctx, session)));
|
|
1670
|
+
}
|
|
1671
|
+
rows.push(outRow);
|
|
1672
|
+
}
|
|
1673
|
+
return rvArray(rows);
|
|
1674
|
+
}
|
|
1675
|
+
function evaluateBYROW(args, ctx, session) {
|
|
1676
|
+
if (args.length < 2) {
|
|
1677
|
+
return ERRORS.VALUE;
|
|
1678
|
+
}
|
|
1679
|
+
const arrVal = evalDeref(args[0], ctx, session);
|
|
1680
|
+
const lambdaVal = evalDeref(args[1], ctx, session);
|
|
1681
|
+
if (!isLambda(lambdaVal)) {
|
|
1682
|
+
return ERRORS.VALUE;
|
|
1683
|
+
}
|
|
1684
|
+
if (arrVal.kind !== RVKind.Array) {
|
|
1685
|
+
return invokeLambda(lambdaVal, [rvArray([[topLeft(arrVal)]])], ctx, session);
|
|
1686
|
+
}
|
|
1687
|
+
const rows = [];
|
|
1688
|
+
for (const row of arrVal.rows) {
|
|
1689
|
+
const rowArr = rvArray([row.map(c => c)]);
|
|
1690
|
+
rows.push([topLeft(invokeLambda(lambdaVal, [rowArr], ctx, session))]);
|
|
1691
|
+
}
|
|
1692
|
+
return rvArray(rows);
|
|
1693
|
+
}
|
|
1694
|
+
function evaluateBYCOL(args, ctx, session) {
|
|
1695
|
+
if (args.length < 2) {
|
|
1696
|
+
return ERRORS.VALUE;
|
|
1697
|
+
}
|
|
1698
|
+
const arrVal = evalDeref(args[0], ctx, session);
|
|
1699
|
+
const lambdaVal = evalDeref(args[1], ctx, session);
|
|
1700
|
+
if (!isLambda(lambdaVal)) {
|
|
1701
|
+
return ERRORS.VALUE;
|
|
1702
|
+
}
|
|
1703
|
+
if (arrVal.kind !== RVKind.Array) {
|
|
1704
|
+
return invokeLambda(lambdaVal, [rvArray([[topLeft(arrVal)]])], ctx, session);
|
|
1705
|
+
}
|
|
1706
|
+
const numCols = arrVal.width;
|
|
1707
|
+
const outRow = [];
|
|
1708
|
+
for (let c = 0; c < numCols; c++) {
|
|
1709
|
+
const colArr = rvArray(arrVal.rows.map(row => [row[c]]));
|
|
1710
|
+
outRow.push(topLeft(invokeLambda(lambdaVal, [colArr], ctx, session)));
|
|
1711
|
+
}
|
|
1712
|
+
return rvArray([outRow]);
|
|
1713
|
+
}
|
|
1714
|
+
// ============================================================================
|
|
1715
|
+
// Reference Functions (ROW, COLUMN, ROWS, COLUMNS)
|
|
1716
|
+
// ============================================================================
|
|
1717
|
+
/** Whether `name` is one of the four "inspect a reference's geometry" builtins. */
|
|
1718
|
+
function isSimpleRefFunction(name) {
|
|
1719
|
+
return name === "ROW" || name === "COLUMN" || name === "ROWS" || name === "COLUMNS";
|
|
1720
|
+
}
|
|
1721
|
+
function tryEvaluateRefFunction(name, args, ctx) {
|
|
1722
|
+
switch (name) {
|
|
1723
|
+
case "ROW":
|
|
1724
|
+
if (args.length === 0) {
|
|
1725
|
+
return ctx.currentAddress ? rvNumber(ctx.currentAddress.row) : ERRORS.VALUE;
|
|
1726
|
+
}
|
|
1727
|
+
if (args[0].kind === BoundExprKind.CellRef) {
|
|
1728
|
+
return rvNumber(args[0].row);
|
|
1729
|
+
}
|
|
1730
|
+
if (args[0].kind === BoundExprKind.AreaRef) {
|
|
1731
|
+
return rvNumber(args[0].top);
|
|
1732
|
+
}
|
|
1733
|
+
return undefined;
|
|
1734
|
+
case "COLUMN":
|
|
1735
|
+
if (args.length === 0) {
|
|
1736
|
+
return ctx.currentAddress ? rvNumber(ctx.currentAddress.col) : ERRORS.VALUE;
|
|
1737
|
+
}
|
|
1738
|
+
if (args[0].kind === BoundExprKind.CellRef) {
|
|
1739
|
+
return rvNumber(args[0].col);
|
|
1740
|
+
}
|
|
1741
|
+
if (args[0].kind === BoundExprKind.AreaRef) {
|
|
1742
|
+
return rvNumber(args[0].left);
|
|
1743
|
+
}
|
|
1744
|
+
return undefined;
|
|
1745
|
+
case "ROWS":
|
|
1746
|
+
if (args.length > 0 && args[0].kind === BoundExprKind.AreaRef) {
|
|
1747
|
+
return rvNumber(args[0].bottom - args[0].top + 1);
|
|
1748
|
+
}
|
|
1749
|
+
if (args.length > 0 && args[0].kind === BoundExprKind.CellRef) {
|
|
1750
|
+
return rvNumber(1);
|
|
1751
|
+
}
|
|
1752
|
+
return undefined;
|
|
1753
|
+
case "COLUMNS":
|
|
1754
|
+
if (args.length > 0 && args[0].kind === BoundExprKind.AreaRef) {
|
|
1755
|
+
return rvNumber(args[0].right - args[0].left + 1);
|
|
1756
|
+
}
|
|
1757
|
+
if (args.length > 0 && args[0].kind === BoundExprKind.CellRef) {
|
|
1758
|
+
return rvNumber(1);
|
|
1759
|
+
}
|
|
1760
|
+
return undefined;
|
|
1761
|
+
default:
|
|
1762
|
+
return undefined;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
// ============================================================================
|
|
1766
|
+
// Reference-aware: ISREF
|
|
1767
|
+
// ============================================================================
|
|
1768
|
+
/**
|
|
1769
|
+
* ISREF(value) → TRUE if `value` is a reference; FALSE otherwise.
|
|
1770
|
+
*
|
|
1771
|
+
* Excel's rule is syntactic + runtime: any `CellRef` / `AreaRef` / 3-D ref /
|
|
1772
|
+
* `ColRangeRef` / `RowRangeRef` is a reference, and any call that *produces*
|
|
1773
|
+
* a `ReferenceValue` (INDIRECT, OFFSET) is also a reference. Errors in the
|
|
1774
|
+
* sub-expression are suppressed (Excel returns FALSE for `ISREF(INDIRECT("xx"))`
|
|
1775
|
+
* where INDIRECT returns `#REF!`).
|
|
1776
|
+
*/
|
|
1777
|
+
function evaluateISREF(args, ctx, session) {
|
|
1778
|
+
if (args.length !== 1) {
|
|
1779
|
+
return ERRORS.VALUE;
|
|
1780
|
+
}
|
|
1781
|
+
const arg = args[0];
|
|
1782
|
+
// Purely syntactic reference forms — always TRUE without evaluating.
|
|
1783
|
+
if (arg.kind === BoundExprKind.CellRef ||
|
|
1784
|
+
arg.kind === BoundExprKind.AreaRef ||
|
|
1785
|
+
arg.kind === BoundExprKind.ColRangeRef ||
|
|
1786
|
+
arg.kind === BoundExprKind.RowRangeRef ||
|
|
1787
|
+
arg.kind === BoundExprKind.Ref3D) {
|
|
1788
|
+
return rvBoolean(true);
|
|
1789
|
+
}
|
|
1790
|
+
// Otherwise evaluate without dereferencing. INDIRECT/OFFSET yield a
|
|
1791
|
+
// ReferenceValue when successful; anything else (error, scalar, array)
|
|
1792
|
+
// is not a reference. Per Excel, ISREF suppresses errors to FALSE.
|
|
1793
|
+
const raw = evaluate(arg, ctx, session);
|
|
1794
|
+
return rvBoolean(raw.kind === RVKind.Reference);
|
|
1795
|
+
}
|
|
1796
|
+
// ============================================================================
|
|
1797
|
+
// Reference-aware: CELL
|
|
1798
|
+
// ============================================================================
|
|
1799
|
+
/**
|
|
1800
|
+
* Resolve a CELL(..., ref) argument to a concrete {sheet,row,col} triple.
|
|
1801
|
+
* CELL always inspects the *top-left* cell of the referenced area.
|
|
1802
|
+
* Returns an error value if the argument cannot be resolved to a reference.
|
|
1803
|
+
*/
|
|
1804
|
+
function resolveCellRefArg(arg, ctx, session) {
|
|
1805
|
+
// Syntactic reference forms — extract top-left directly.
|
|
1806
|
+
if (arg.kind === BoundExprKind.CellRef) {
|
|
1807
|
+
return { sheet: arg.sheet, row: arg.row, col: arg.col };
|
|
1808
|
+
}
|
|
1809
|
+
if (arg.kind === BoundExprKind.AreaRef) {
|
|
1810
|
+
return { sheet: arg.sheet, row: arg.top, col: arg.left };
|
|
1811
|
+
}
|
|
1812
|
+
if (arg.kind === BoundExprKind.ColRangeRef) {
|
|
1813
|
+
const ws = ctx.snapshot.worksheetsByName.get(arg.sheet.toLowerCase());
|
|
1814
|
+
const top = ws?.dimensions?.top ?? 1;
|
|
1815
|
+
return { sheet: arg.sheet, row: top, col: arg.leftCol };
|
|
1816
|
+
}
|
|
1817
|
+
if (arg.kind === BoundExprKind.RowRangeRef) {
|
|
1818
|
+
const ws = ctx.snapshot.worksheetsByName.get(arg.sheet.toLowerCase());
|
|
1819
|
+
const left = ws?.dimensions?.left ?? 1;
|
|
1820
|
+
return { sheet: arg.sheet, row: arg.topRow, col: left };
|
|
1821
|
+
}
|
|
1822
|
+
if (arg.kind === BoundExprKind.Ref3D) {
|
|
1823
|
+
const first = arg.sheets[0];
|
|
1824
|
+
if (first === undefined) {
|
|
1825
|
+
return ERRORS.VALUE;
|
|
1826
|
+
}
|
|
1827
|
+
if (arg.inner.kind === BoundExprKind.CellRef) {
|
|
1828
|
+
return { sheet: first, row: arg.inner.row, col: arg.inner.col };
|
|
1829
|
+
}
|
|
1830
|
+
return { sheet: first, row: arg.inner.top, col: arg.inner.left };
|
|
1831
|
+
}
|
|
1832
|
+
// Fall back to evaluating — INDIRECT/OFFSET etc. may produce a ReferenceValue.
|
|
1833
|
+
const raw = evaluate(arg, ctx, session);
|
|
1834
|
+
if (raw.kind === RVKind.Error) {
|
|
1835
|
+
return raw;
|
|
1836
|
+
}
|
|
1837
|
+
if (raw.kind === RVKind.Reference && raw.areas.length > 0) {
|
|
1838
|
+
const area = raw.areas[0];
|
|
1839
|
+
return { sheet: area.sheet, row: area.top, col: area.left };
|
|
1840
|
+
}
|
|
1841
|
+
// Non-reference argument — Excel returns #VALUE! for CELL.
|
|
1842
|
+
return ERRORS.VALUE;
|
|
1843
|
+
}
|
|
1844
|
+
/** Convert a 1-based column number to its letter form (1 → "A", 27 → "AA"). */
|
|
1845
|
+
function colNumberToLetter(colNum) {
|
|
1846
|
+
let col = "";
|
|
1847
|
+
let cv = colNum;
|
|
1848
|
+
while (cv > 0) {
|
|
1849
|
+
cv--;
|
|
1850
|
+
col = String.fromCharCode(65 + (cv % 26)) + col;
|
|
1851
|
+
cv = Math.floor(cv / 26);
|
|
1852
|
+
}
|
|
1853
|
+
return col;
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* CELL(info_type, [reference]) — limited, workbook-internal subset.
|
|
1857
|
+
*
|
|
1858
|
+
* Supported info types:
|
|
1859
|
+
* - `"address"` → "$A$1"-style absolute reference (no sheet name)
|
|
1860
|
+
* - `"row"` → 1-based row number of the top-left cell
|
|
1861
|
+
* - `"col"` → 1-based column number of the top-left cell
|
|
1862
|
+
* - `"contents"` → value of the top-left cell
|
|
1863
|
+
* - `"type"` → "b" (blank), "l" (label/text), "v" (value/other)
|
|
1864
|
+
* - `"width"` → 8 (column width is not tracked in the snapshot)
|
|
1865
|
+
* - `"filename"` → "" (no file path available)
|
|
1866
|
+
*
|
|
1867
|
+
* Any other info type yields `#N/A`, matching Excel's treatment of
|
|
1868
|
+
* workbook-state-dependent info in contexts where the data is unavailable.
|
|
1869
|
+
*
|
|
1870
|
+
* When `reference` is omitted, the current formula's own cell is used —
|
|
1871
|
+
* if that cannot be determined, `#VALUE!` is returned.
|
|
1872
|
+
*/
|
|
1873
|
+
function evaluateCELL(args, ctx, session) {
|
|
1874
|
+
if (args.length < 1 || args.length > 2) {
|
|
1875
|
+
return ERRORS.VALUE;
|
|
1876
|
+
}
|
|
1877
|
+
// Resolve info_type — this is a plain string expression, evaluate normally.
|
|
1878
|
+
const infoRV = dereferenceValue(evaluate(args[0], ctx, session), ctx, session);
|
|
1879
|
+
if (infoRV.kind === RVKind.Error) {
|
|
1880
|
+
return infoRV;
|
|
1881
|
+
}
|
|
1882
|
+
const infoScalar = topLeft(infoRV);
|
|
1883
|
+
if (infoScalar.kind === RVKind.Error) {
|
|
1884
|
+
return infoScalar;
|
|
1885
|
+
}
|
|
1886
|
+
if (infoScalar.kind !== RVKind.String) {
|
|
1887
|
+
return ERRORS.VALUE;
|
|
1888
|
+
}
|
|
1889
|
+
const info = infoScalar.value.toLowerCase();
|
|
1890
|
+
// Resolve reference: explicit arg, or the current formula cell.
|
|
1891
|
+
let target;
|
|
1892
|
+
if (args.length === 2) {
|
|
1893
|
+
const resolved = resolveCellRefArg(args[1], ctx, session);
|
|
1894
|
+
if ("kind" in resolved) {
|
|
1895
|
+
return resolved;
|
|
1896
|
+
}
|
|
1897
|
+
target = resolved;
|
|
1898
|
+
}
|
|
1899
|
+
else {
|
|
1900
|
+
if (!ctx.currentAddress) {
|
|
1901
|
+
return ERRORS.VALUE;
|
|
1902
|
+
}
|
|
1903
|
+
target = {
|
|
1904
|
+
sheet: ctx.currentSheet,
|
|
1905
|
+
row: ctx.currentAddress.row,
|
|
1906
|
+
col: ctx.currentAddress.col
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
switch (info) {
|
|
1910
|
+
case "address": {
|
|
1911
|
+
return rvString(`$${colNumberToLetter(target.col)}$${target.row}`);
|
|
1912
|
+
}
|
|
1913
|
+
case "row":
|
|
1914
|
+
return rvNumber(target.row);
|
|
1915
|
+
case "col":
|
|
1916
|
+
case "column":
|
|
1917
|
+
return rvNumber(target.col);
|
|
1918
|
+
case "contents": {
|
|
1919
|
+
return getCellValue(target.sheet, target.row, target.col, ctx, session);
|
|
1920
|
+
}
|
|
1921
|
+
case "type": {
|
|
1922
|
+
const val = getCellValue(target.sheet, target.row, target.col, ctx, session);
|
|
1923
|
+
if (val.kind === RVKind.Blank) {
|
|
1924
|
+
return rvString("b");
|
|
1925
|
+
}
|
|
1926
|
+
if (val.kind === RVKind.String) {
|
|
1927
|
+
return rvString("l");
|
|
1928
|
+
}
|
|
1929
|
+
// Numbers, booleans, errors — all classified as "value".
|
|
1930
|
+
return rvString("v");
|
|
1931
|
+
}
|
|
1932
|
+
case "width":
|
|
1933
|
+
// Column width is not captured in the snapshot — return Excel's default.
|
|
1934
|
+
return rvNumber(8);
|
|
1935
|
+
case "filename":
|
|
1936
|
+
// No file path is available to the calculation engine.
|
|
1937
|
+
return rvString("");
|
|
1938
|
+
default:
|
|
1939
|
+
return ERRORS.NA;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
// ============================================================================
|
|
1943
|
+
// Name Expression
|
|
1944
|
+
// ============================================================================
|
|
1945
|
+
function evaluateNameExpr(expr, ctx, session) {
|
|
1946
|
+
// Check local bindings first (LET variables)
|
|
1947
|
+
if (ctx.localBindings?.has(expr.upperName)) {
|
|
1948
|
+
return ctx.localBindings.get(expr.upperName);
|
|
1949
|
+
}
|
|
1950
|
+
// Try snapshot defined name resolution (for formula-based names).
|
|
1951
|
+
// Respects scope precedence: sheet-local > workbook-global.
|
|
1952
|
+
const dn = resolveDefinedNameFromSnapshot(ctx.snapshot.definedNames, expr.name, ctx.currentSheet);
|
|
1953
|
+
if (dn && dn.ranges.length > 0) {
|
|
1954
|
+
if (dn.ranges.length > 1) {
|
|
1955
|
+
return ERRORS.VALUE;
|
|
1956
|
+
}
|
|
1957
|
+
const rangeStr = dn.ranges[0];
|
|
1958
|
+
const parsed = parseDefinedNameRange(rangeStr);
|
|
1959
|
+
if (parsed) {
|
|
1960
|
+
if (parsed.startRow === parsed.endRow && parsed.startCol === parsed.endCol) {
|
|
1961
|
+
return getCellValue(parsed.sheet, parsed.startRow, parsed.startCol, ctx, session);
|
|
1962
|
+
}
|
|
1963
|
+
return buildRangeArray(ctx, session, parsed.sheet, Math.min(parsed.startRow, parsed.endRow), Math.min(parsed.startCol, parsed.endCol), Math.max(parsed.startRow, parsed.endRow), Math.max(parsed.startCol, parsed.endCol));
|
|
1964
|
+
}
|
|
1965
|
+
// Formula expression — parse and evaluate via shared helper
|
|
1966
|
+
const nameResult = evaluateFormulaName(expr.upperName, rangeStr, ctx, session);
|
|
1967
|
+
return nameResult ?? ERRORS.NAME;
|
|
1968
|
+
}
|
|
1969
|
+
return ERRORS.NAME;
|
|
1970
|
+
}
|
|
1971
|
+
// ============================================================================
|
|
1972
|
+
// Formula-based Defined Name Evaluation (shared helper)
|
|
1973
|
+
// ============================================================================
|
|
1974
|
+
/**
|
|
1975
|
+
* Evaluate a formula-based defined name expression, with caching.
|
|
1976
|
+
* Cache key includes name + sheet + cell address to handle position-dependent
|
|
1977
|
+
* formulas like ROW()/COLUMN().
|
|
1978
|
+
*
|
|
1979
|
+
* Returns the evaluated result, or undefined if parsing/evaluation fails.
|
|
1980
|
+
*/
|
|
1981
|
+
function evaluateFormulaName(upperName, formulaExpr, ctx, session) {
|
|
1982
|
+
const addr = ctx.currentAddress;
|
|
1983
|
+
const cacheKey = addr
|
|
1984
|
+
? `__NAME__${upperName}__${ctx.currentSheet}__${addr.row}:${addr.col}`
|
|
1985
|
+
: `__NAME__${upperName}__${ctx.currentSheet}`;
|
|
1986
|
+
const cached = session.nameCache.get(cacheKey);
|
|
1987
|
+
if (cached !== undefined) {
|
|
1988
|
+
return cached;
|
|
1989
|
+
}
|
|
1990
|
+
// Guard against recursion through a defined name that references itself.
|
|
1991
|
+
// Uses a dedicated prefix so it cannot collide with formula-cell guard keys.
|
|
1992
|
+
const guardKey = `__NAMEEVAL__${upperName}`;
|
|
1993
|
+
if (session.evaluating.has(guardKey)) {
|
|
1994
|
+
return ERRORS.CALC;
|
|
1995
|
+
}
|
|
1996
|
+
session.evaluating.add(guardKey);
|
|
1997
|
+
try {
|
|
1998
|
+
const tokens = tokenize(formulaExpr);
|
|
1999
|
+
const ast = parse(tokens);
|
|
2000
|
+
const bindCtx = { snapshot: ctx.snapshot, currentSheet: ctx.currentSheet };
|
|
2001
|
+
const bound = bind(ast, bindCtx);
|
|
2002
|
+
const result = evaluate(bound, ctx, session);
|
|
2003
|
+
session.nameCache.set(cacheKey, result);
|
|
2004
|
+
return result;
|
|
2005
|
+
}
|
|
2006
|
+
catch {
|
|
2007
|
+
return undefined;
|
|
2008
|
+
}
|
|
2009
|
+
finally {
|
|
2010
|
+
session.evaluating.delete(guardKey);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
// ============================================================================
|
|
2014
|
+
// Lambda Expression
|
|
2015
|
+
// ============================================================================
|
|
2016
|
+
function evaluateLambdaExpr(expr, ctx) {
|
|
2017
|
+
return rvLambda([...expr.params], expr.body, ctx.localBindings ? new Map(ctx.localBindings) : undefined);
|
|
2018
|
+
}
|
|
2019
|
+
// ============================================================================
|
|
2020
|
+
// Structured Reference (runtime resolution)
|
|
2021
|
+
// ============================================================================
|
|
2022
|
+
function evaluateStructuredRef(expr, ctx, session) {
|
|
2023
|
+
const snapshot = ctx.snapshot;
|
|
2024
|
+
const addr = ctx.currentAddress;
|
|
2025
|
+
// Find the table
|
|
2026
|
+
let tableName = expr.tableName;
|
|
2027
|
+
let tableSheet = null;
|
|
2028
|
+
let tableInfo = null;
|
|
2029
|
+
if (tableName === "") {
|
|
2030
|
+
// Implicit table — find the table containing the current cell
|
|
2031
|
+
if (!addr) {
|
|
2032
|
+
return ERRORS.REF;
|
|
2033
|
+
}
|
|
2034
|
+
// Sheet names are case-insensitive in Excel. Comparing the literal
|
|
2035
|
+
// `ws.name !== addr.sheet` would miss tables when the formula-cell's
|
|
2036
|
+
// address records its sheet in a different case than the workbook's
|
|
2037
|
+
// canonical name (possible after rename / import flows).
|
|
2038
|
+
const addrSheetLower = addr.sheet.toLowerCase();
|
|
2039
|
+
for (const ws of snapshot.worksheets) {
|
|
2040
|
+
if (ws.name.toLowerCase() !== addrSheetLower) {
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
for (const t of ws.tables) {
|
|
2044
|
+
const g = buildTableGeometry(t);
|
|
2045
|
+
const width = t.columns.length;
|
|
2046
|
+
if (addr.row >= g.dataRowStart &&
|
|
2047
|
+
addr.row <= g.dataRowEnd &&
|
|
2048
|
+
addr.col >= t.topLeft.col &&
|
|
2049
|
+
addr.col < t.topLeft.col + width) {
|
|
2050
|
+
tableInfo = t;
|
|
2051
|
+
tableSheet = ws.name;
|
|
2052
|
+
tableName = t.name;
|
|
2053
|
+
break;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
if (tableInfo) {
|
|
2057
|
+
break;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
else {
|
|
2062
|
+
// Named table — use the pre-built index for O(1) lookup
|
|
2063
|
+
const resolved = snapshot.tablesByName.get(tableName.toLowerCase());
|
|
2064
|
+
if (resolved) {
|
|
2065
|
+
tableInfo = resolved.table;
|
|
2066
|
+
tableSheet = resolved.sheetName;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
if (!tableInfo || !tableSheet) {
|
|
2070
|
+
return ERRORS.REF;
|
|
2071
|
+
}
|
|
2072
|
+
const geo = buildTableGeometry(tableInfo);
|
|
2073
|
+
// Strict column resolution — unknown column names surface as #REF!
|
|
2074
|
+
const colRange = resolveStructuredRefColumns(expr.columns, tableInfo, "strict");
|
|
2075
|
+
if (colRange === "error") {
|
|
2076
|
+
return ERRORS.REF;
|
|
2077
|
+
}
|
|
2078
|
+
const rowRange = resolveStructuredRefRows(expr.specials, geo);
|
|
2079
|
+
let rowTop;
|
|
2080
|
+
let rowBottom;
|
|
2081
|
+
if (rowRange === "error") {
|
|
2082
|
+
return ERRORS.REF;
|
|
2083
|
+
}
|
|
2084
|
+
else if (rowRange === "thisRow") {
|
|
2085
|
+
if (addr) {
|
|
2086
|
+
rowTop = addr.row;
|
|
2087
|
+
rowBottom = addr.row;
|
|
2088
|
+
}
|
|
2089
|
+
else {
|
|
2090
|
+
return ERRORS.VALUE;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
else {
|
|
2094
|
+
rowTop = rowRange.rowTop;
|
|
2095
|
+
rowBottom = rowRange.rowBottom;
|
|
2096
|
+
}
|
|
2097
|
+
// Single cell — return as single-cell ReferenceValue
|
|
2098
|
+
if (rowTop === rowBottom && colRange.colLeft === colRange.colRight) {
|
|
2099
|
+
return rvCellRef(tableSheet, rowTop, colRange.colLeft);
|
|
2100
|
+
}
|
|
2101
|
+
// Range — return as area ReferenceValue
|
|
2102
|
+
return rvRef(tableSheet, rowTop, colRange.colLeft, rowBottom, colRange.colRight);
|
|
2103
|
+
}
|
|
2104
|
+
// ============================================================================
|
|
2105
|
+
// Array Literal
|
|
2106
|
+
// ============================================================================
|
|
2107
|
+
function evaluateArrayLiteral(expr, ctx, session) {
|
|
2108
|
+
const rows = [];
|
|
2109
|
+
for (const row of expr.rows) {
|
|
2110
|
+
const evalRow = [];
|
|
2111
|
+
for (const elem of row) {
|
|
2112
|
+
evalRow.push(topLeft(evalDeref(elem, ctx, session)));
|
|
2113
|
+
}
|
|
2114
|
+
rows.push(evalRow);
|
|
2115
|
+
}
|
|
2116
|
+
return rvArray(rows);
|
|
2117
|
+
}
|
|
2118
|
+
// ============================================================================
|
|
2119
|
+
// Implicit Intersection
|
|
2120
|
+
// ============================================================================
|
|
2121
|
+
export function implicitIntersect(val, ctx) {
|
|
2122
|
+
if (isScalar(val)) {
|
|
2123
|
+
return val;
|
|
2124
|
+
}
|
|
2125
|
+
if (val.kind === RVKind.Lambda) {
|
|
2126
|
+
return val;
|
|
2127
|
+
}
|
|
2128
|
+
if (val.kind === RVKind.Reference) {
|
|
2129
|
+
// Implicit intersection on a reference: resolve using formula cell's row/col
|
|
2130
|
+
if (val.areas.length === 0) {
|
|
2131
|
+
return BLANK;
|
|
2132
|
+
}
|
|
2133
|
+
const area = val.areas[0];
|
|
2134
|
+
const isSingleCell = area.top === area.bottom && area.left === area.right;
|
|
2135
|
+
if (isSingleCell) {
|
|
2136
|
+
return val; // Single cell ref — keep as-is, will be dereferenced at use site
|
|
2137
|
+
}
|
|
2138
|
+
// Multi-cell reference — apply implicit intersection
|
|
2139
|
+
const addr = ctx.currentAddress;
|
|
2140
|
+
if (!addr) {
|
|
2141
|
+
return val;
|
|
2142
|
+
}
|
|
2143
|
+
// Single column — pick row
|
|
2144
|
+
if (area.left === area.right && addr.row >= area.top && addr.row <= area.bottom) {
|
|
2145
|
+
return rvCellRef(area.sheet, addr.row, area.left);
|
|
2146
|
+
}
|
|
2147
|
+
// Single row — pick column
|
|
2148
|
+
if (area.top === area.bottom && addr.col >= area.left && addr.col <= area.right) {
|
|
2149
|
+
return rvCellRef(area.sheet, area.top, addr.col);
|
|
2150
|
+
}
|
|
2151
|
+
// Both row and col
|
|
2152
|
+
if (addr.row >= area.top &&
|
|
2153
|
+
addr.row <= area.bottom &&
|
|
2154
|
+
addr.col >= area.left &&
|
|
2155
|
+
addr.col <= area.right) {
|
|
2156
|
+
return rvCellRef(area.sheet, addr.row, addr.col);
|
|
2157
|
+
}
|
|
2158
|
+
return val;
|
|
2159
|
+
}
|
|
2160
|
+
if (val.kind !== RVKind.Array) {
|
|
2161
|
+
return val;
|
|
2162
|
+
}
|
|
2163
|
+
const arr = val;
|
|
2164
|
+
if (arr.height === 0 || arr.width === 0) {
|
|
2165
|
+
return BLANK;
|
|
2166
|
+
}
|
|
2167
|
+
if (arr.height === 1 && arr.width === 1) {
|
|
2168
|
+
return arr.rows[0][0];
|
|
2169
|
+
}
|
|
2170
|
+
const addr = ctx.currentAddress;
|
|
2171
|
+
if (!addr) {
|
|
2172
|
+
return arr.rows[0][0];
|
|
2173
|
+
}
|
|
2174
|
+
// Single row — pick column by offset
|
|
2175
|
+
if (arr.height === 1) {
|
|
2176
|
+
if (arr.originCol !== undefined) {
|
|
2177
|
+
const colIdx = addr.col - arr.originCol;
|
|
2178
|
+
if (colIdx >= 0 && colIdx < arr.width) {
|
|
2179
|
+
return arr.rows[0][colIdx];
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
return arr.rows[0][0];
|
|
2183
|
+
}
|
|
2184
|
+
// Single column — pick row by offset
|
|
2185
|
+
if (arr.width === 1) {
|
|
2186
|
+
if (arr.originRow !== undefined) {
|
|
2187
|
+
const rowIdx = addr.row - arr.originRow;
|
|
2188
|
+
if (rowIdx >= 0 && rowIdx < arr.height) {
|
|
2189
|
+
return arr.rows[rowIdx][0];
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
return arr.rows[0][0];
|
|
2193
|
+
}
|
|
2194
|
+
// Multi-row, multi-column
|
|
2195
|
+
if (arr.originRow !== undefined && arr.originCol !== undefined) {
|
|
2196
|
+
const rowIdx = addr.row - arr.originRow;
|
|
2197
|
+
const colIdx = addr.col - arr.originCol;
|
|
2198
|
+
if (rowIdx >= 0 && rowIdx < arr.height && colIdx >= 0 && colIdx < arr.width) {
|
|
2199
|
+
return arr.rows[rowIdx][colIdx];
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
return arr.rows[0][0];
|
|
2203
|
+
}
|
|
2204
|
+
// ============================================================================
|
|
2205
|
+
// R1C1 Reference Resolution
|
|
2206
|
+
// ============================================================================
|
|
2207
|
+
function resolveR1C1(refText, ctx, session) {
|
|
2208
|
+
const upper = refText.toUpperCase().trim();
|
|
2209
|
+
// Check for range separator
|
|
2210
|
+
let depth = 0;
|
|
2211
|
+
let sepIdx = -1;
|
|
2212
|
+
for (let i = 0; i < upper.length; i++) {
|
|
2213
|
+
if (upper[i] === "[") {
|
|
2214
|
+
depth++;
|
|
2215
|
+
}
|
|
2216
|
+
else if (upper[i] === "]") {
|
|
2217
|
+
depth--;
|
|
2218
|
+
}
|
|
2219
|
+
else if (upper[i] === ":" && depth === 0) {
|
|
2220
|
+
sepIdx = i;
|
|
2221
|
+
break;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
if (sepIdx !== -1) {
|
|
2225
|
+
const s = parseR1C1Single(upper.slice(0, sepIdx), ctx);
|
|
2226
|
+
const e = parseR1C1Single(upper.slice(sepIdx + 1), ctx);
|
|
2227
|
+
if (!s || !e) {
|
|
2228
|
+
return ERRORS.REF;
|
|
2229
|
+
}
|
|
2230
|
+
const top = Math.min(s.row, e.row);
|
|
2231
|
+
const bottom = Math.max(s.row, e.row);
|
|
2232
|
+
const left = Math.min(s.col, e.col);
|
|
2233
|
+
const right = Math.max(s.col, e.col);
|
|
2234
|
+
return rvRef(ctx.currentSheet, top, left, bottom, right);
|
|
2235
|
+
}
|
|
2236
|
+
const ref = parseR1C1Single(upper, ctx);
|
|
2237
|
+
if (!ref || ref.row < 1 || ref.col < 1) {
|
|
2238
|
+
return ERRORS.REF;
|
|
2239
|
+
}
|
|
2240
|
+
return rvCellRef(ctx.currentSheet, ref.row, ref.col);
|
|
2241
|
+
}
|
|
2242
|
+
function parseR1C1Single(text, ctx) {
|
|
2243
|
+
const re = /^R(\[(-?\d+)\]|(\d+))C(\[(-?\d+)\]|(\d+))$/;
|
|
2244
|
+
const m = re.exec(text);
|
|
2245
|
+
if (!m) {
|
|
2246
|
+
return null;
|
|
2247
|
+
}
|
|
2248
|
+
const addr = ctx.currentAddress;
|
|
2249
|
+
const row = m[2] !== undefined ? (addr?.row ?? 1) + parseInt(m[2], 10) : parseInt(m[3], 10);
|
|
2250
|
+
const col = m[5] !== undefined ? (addr?.col ?? 1) + parseInt(m[5], 10) : parseInt(m[6], 10);
|
|
2251
|
+
return { row, col };
|
|
2252
|
+
}
|
|
2253
|
+
// ============================================================================
|
|
2254
|
+
// Helpers
|
|
2255
|
+
// ============================================================================
|
|
2256
|
+
/**
|
|
2257
|
+
* Evaluate a BoundExpr and dereference any resulting ReferenceValue.
|
|
2258
|
+
* Use this whenever a concrete (non-reference) value is needed.
|
|
2259
|
+
*/
|
|
2260
|
+
function evalDeref(expr, ctx, session) {
|
|
2261
|
+
return dereferenceValue(evaluate(expr, ctx, session), ctx, session);
|
|
2262
|
+
}
|
|
2263
|
+
function resolveLambdaName(name, args, ctx, session) {
|
|
2264
|
+
// Check local bindings
|
|
2265
|
+
if (ctx.localBindings?.has(name)) {
|
|
2266
|
+
const val = ctx.localBindings.get(name);
|
|
2267
|
+
if (isLambda(val)) {
|
|
2268
|
+
return invokeLambda(val, args, ctx, session);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
// Check defined names that resolve to lambdas (via snapshot, scope-aware)
|
|
2272
|
+
const dn = resolveDefinedNameFromSnapshot(ctx.snapshot.definedNames, name, ctx.currentSheet);
|
|
2273
|
+
if (dn && dn.ranges.length === 1) {
|
|
2274
|
+
const rangeStr = dn.ranges[0];
|
|
2275
|
+
const parsed = parseDefinedNameRange(rangeStr);
|
|
2276
|
+
if (parsed && parsed.startRow === parsed.endRow && parsed.startCol === parsed.endCol) {
|
|
2277
|
+
const cellVal = getCellValue(parsed.sheet, parsed.startRow, parsed.startCol, ctx, session);
|
|
2278
|
+
if (isLambda(cellVal)) {
|
|
2279
|
+
return invokeLambda(cellVal, args, ctx, session);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
// Formula-based name — evaluate via shared helper
|
|
2283
|
+
if (!parsed) {
|
|
2284
|
+
const nameVal = evaluateFormulaName(name.toUpperCase(), rangeStr, ctx, session);
|
|
2285
|
+
if (nameVal !== undefined && isLambda(nameVal)) {
|
|
2286
|
+
return invokeLambda(nameVal, args, ctx, session);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
return undefined;
|
|
2291
|
+
}
|