@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,1845 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Functions — Native RuntimeValue implementations.
|
|
3
|
+
*/
|
|
4
|
+
import { excelToDate } from "../../../utils/utils.base.js";
|
|
5
|
+
import { RVKind, ERRORS, isError, isArray, toNumberRV, toStringRV, toBooleanRV, topLeft, rvNumber, rvString, rvBoolean, rvArray } from "../runtime/values.js";
|
|
6
|
+
import { isDate1904 } from "./_date-context.js";
|
|
7
|
+
import { argToNumber, checkError, excelWildcardToRegex } from "./_shared.js";
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Local utility
|
|
10
|
+
// ============================================================================
|
|
11
|
+
function escapeRegex(s) {
|
|
12
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13
|
+
}
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// CONCATENATE / CONCAT
|
|
16
|
+
// ============================================================================
|
|
17
|
+
export const fnCONCATENATE = args => {
|
|
18
|
+
const parts = [];
|
|
19
|
+
for (const a of args) {
|
|
20
|
+
if (isArray(a)) {
|
|
21
|
+
for (const row of a.rows) {
|
|
22
|
+
for (const cell of row) {
|
|
23
|
+
const err = checkError(cell);
|
|
24
|
+
if (err) {
|
|
25
|
+
return err;
|
|
26
|
+
}
|
|
27
|
+
parts.push(toStringRV(cell));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const err = checkError(a);
|
|
33
|
+
if (err) {
|
|
34
|
+
return err;
|
|
35
|
+
}
|
|
36
|
+
parts.push(toStringRV(a));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return rvString(parts.join(""));
|
|
40
|
+
};
|
|
41
|
+
// CONCAT has the same semantics as CONCATENATE
|
|
42
|
+
export const fnCONCAT = fnCONCATENATE;
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// TEXTJOIN
|
|
45
|
+
// ============================================================================
|
|
46
|
+
export const fnTEXTJOIN = args => {
|
|
47
|
+
if (args.length < 3) {
|
|
48
|
+
return ERRORS.VALUE;
|
|
49
|
+
}
|
|
50
|
+
const e0 = checkError(args[0]);
|
|
51
|
+
if (e0) {
|
|
52
|
+
return e0;
|
|
53
|
+
}
|
|
54
|
+
const delimiter = toStringRV(args[0]);
|
|
55
|
+
const ignoreEmptyRV = toBooleanRV(args[1]);
|
|
56
|
+
if (isError(ignoreEmptyRV)) {
|
|
57
|
+
return ignoreEmptyRV;
|
|
58
|
+
}
|
|
59
|
+
const ignoreEmpty = ignoreEmptyRV.value;
|
|
60
|
+
const parts = [];
|
|
61
|
+
for (let i = 2; i < args.length; i++) {
|
|
62
|
+
const a = args[i];
|
|
63
|
+
if (isArray(a)) {
|
|
64
|
+
for (const row of a.rows) {
|
|
65
|
+
for (const cell of row) {
|
|
66
|
+
const err = checkError(cell);
|
|
67
|
+
if (err) {
|
|
68
|
+
return err;
|
|
69
|
+
}
|
|
70
|
+
const s = toStringRV(cell);
|
|
71
|
+
if (ignoreEmpty && s === "") {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
parts.push(s);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const err = checkError(a);
|
|
80
|
+
if (err) {
|
|
81
|
+
return err;
|
|
82
|
+
}
|
|
83
|
+
const s = toStringRV(a);
|
|
84
|
+
if (ignoreEmpty && s === "") {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
parts.push(s);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return rvString(parts.join(delimiter));
|
|
91
|
+
};
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// LEFT / RIGHT / MID / LEN
|
|
94
|
+
// ============================================================================
|
|
95
|
+
export const fnLEFT = args => {
|
|
96
|
+
const err = checkError(args[0]);
|
|
97
|
+
if (err) {
|
|
98
|
+
return err;
|
|
99
|
+
}
|
|
100
|
+
const text = toStringRV(args[0]);
|
|
101
|
+
let n;
|
|
102
|
+
if (args.length > 1) {
|
|
103
|
+
// Use `argToNumber` so array arguments get implicit-intersection to
|
|
104
|
+
// their top-left cell before numeric coercion — otherwise
|
|
105
|
+
// `LEFT("abc", A1:A2)` would land in `toNumberRV`'s array path and
|
|
106
|
+
// incorrectly surface #VALUE! instead of using A1.
|
|
107
|
+
const nRV = argToNumber(args[1]);
|
|
108
|
+
if (isError(nRV)) {
|
|
109
|
+
return nRV;
|
|
110
|
+
}
|
|
111
|
+
n = nRV.value;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
n = 1;
|
|
115
|
+
}
|
|
116
|
+
// Excel rejects negative lengths outright. Without this guard,
|
|
117
|
+
// `text.slice(0, -1)` would silently trim the last character.
|
|
118
|
+
if (n < 0) {
|
|
119
|
+
return ERRORS.VALUE;
|
|
120
|
+
}
|
|
121
|
+
return rvString(text.slice(0, Math.trunc(n)));
|
|
122
|
+
};
|
|
123
|
+
export const fnRIGHT = args => {
|
|
124
|
+
const err = checkError(args[0]);
|
|
125
|
+
if (err) {
|
|
126
|
+
return err;
|
|
127
|
+
}
|
|
128
|
+
const text = toStringRV(args[0]);
|
|
129
|
+
let n;
|
|
130
|
+
if (args.length > 1) {
|
|
131
|
+
// Implicit intersection via `argToNumber` — see LEFT for rationale.
|
|
132
|
+
const nRV = argToNumber(args[1]);
|
|
133
|
+
if (isError(nRV)) {
|
|
134
|
+
return nRV;
|
|
135
|
+
}
|
|
136
|
+
n = nRV.value;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
n = 1;
|
|
140
|
+
}
|
|
141
|
+
if (n < 0) {
|
|
142
|
+
return ERRORS.VALUE;
|
|
143
|
+
}
|
|
144
|
+
const k = Math.trunc(n);
|
|
145
|
+
if (k === 0) {
|
|
146
|
+
return rvString("");
|
|
147
|
+
}
|
|
148
|
+
return rvString(text.slice(-k));
|
|
149
|
+
};
|
|
150
|
+
export const fnMID = args => {
|
|
151
|
+
const err = checkError(args[0]);
|
|
152
|
+
if (err) {
|
|
153
|
+
return err;
|
|
154
|
+
}
|
|
155
|
+
const text = toStringRV(args[0]);
|
|
156
|
+
// Implicit intersection on both numeric arguments so array inputs
|
|
157
|
+
// collapse to their top-left cells before coercion.
|
|
158
|
+
const startNumRV = argToNumber(args[1]);
|
|
159
|
+
if (isError(startNumRV)) {
|
|
160
|
+
return startNumRV;
|
|
161
|
+
}
|
|
162
|
+
const startNum = Math.trunc(startNumRV.value);
|
|
163
|
+
const numCharsRV = argToNumber(args[2]);
|
|
164
|
+
if (isError(numCharsRV)) {
|
|
165
|
+
return numCharsRV;
|
|
166
|
+
}
|
|
167
|
+
const numChars = Math.trunc(numCharsRV.value);
|
|
168
|
+
// MID: start_num must be >= 1, num_chars must be >= 0.
|
|
169
|
+
if (startNum < 1 || numChars < 0) {
|
|
170
|
+
return ERRORS.VALUE;
|
|
171
|
+
}
|
|
172
|
+
return rvString(text.slice(startNum - 1, startNum - 1 + numChars));
|
|
173
|
+
};
|
|
174
|
+
export const fnLEN = args => {
|
|
175
|
+
const err = checkError(args[0]);
|
|
176
|
+
if (err) {
|
|
177
|
+
return err;
|
|
178
|
+
}
|
|
179
|
+
// `toStringRV` doesn't dereference arrays — passing `A1:A2` would hit
|
|
180
|
+
// its `default: ""` branch and silently return 0. Do an implicit
|
|
181
|
+
// intersection via `topLeft` so `LEN(A1:A2)` behaves like Excel's
|
|
182
|
+
// legacy implicit-intersection semantics (pick the first cell).
|
|
183
|
+
return rvNumber(toStringRV(topLeft(args[0])).length);
|
|
184
|
+
};
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// TRIM / LOWER / UPPER / PROPER
|
|
187
|
+
// ============================================================================
|
|
188
|
+
export const fnTRIM = args => {
|
|
189
|
+
const err = checkError(args[0]);
|
|
190
|
+
if (err) {
|
|
191
|
+
return err;
|
|
192
|
+
}
|
|
193
|
+
// Implicit intersection: turn an array argument into its top-left
|
|
194
|
+
// cell before stringifying, to match Excel's legacy behaviour.
|
|
195
|
+
// Excel's TRIM only collapses plain ASCII space (U+0020), NOT tabs,
|
|
196
|
+
// newlines, or non-breaking space (U+00A0).
|
|
197
|
+
return rvString(toStringRV(topLeft(args[0]))
|
|
198
|
+
.replace(/^ +| +$/g, "")
|
|
199
|
+
.replace(/ +/g, " "));
|
|
200
|
+
};
|
|
201
|
+
export const fnLOWER = args => {
|
|
202
|
+
const err = checkError(args[0]);
|
|
203
|
+
if (err) {
|
|
204
|
+
return err;
|
|
205
|
+
}
|
|
206
|
+
return rvString(toStringRV(topLeft(args[0])).toLowerCase());
|
|
207
|
+
};
|
|
208
|
+
export const fnUPPER = args => {
|
|
209
|
+
const err = checkError(args[0]);
|
|
210
|
+
if (err) {
|
|
211
|
+
return err;
|
|
212
|
+
}
|
|
213
|
+
return rvString(toStringRV(topLeft(args[0])).toUpperCase());
|
|
214
|
+
};
|
|
215
|
+
export const fnPROPER = args => {
|
|
216
|
+
const err = checkError(args[0]);
|
|
217
|
+
if (err) {
|
|
218
|
+
return err;
|
|
219
|
+
}
|
|
220
|
+
return rvString(toStringRV(topLeft(args[0])).replace(/\p{L}+/gu, word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()));
|
|
221
|
+
};
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// SUBSTITUTE / REPLACE
|
|
224
|
+
// ============================================================================
|
|
225
|
+
export const fnSUBSTITUTE = args => {
|
|
226
|
+
const err0 = checkError(args[0]);
|
|
227
|
+
if (err0) {
|
|
228
|
+
return err0;
|
|
229
|
+
}
|
|
230
|
+
const err1 = checkError(args[1]);
|
|
231
|
+
if (err1) {
|
|
232
|
+
return err1;
|
|
233
|
+
}
|
|
234
|
+
const err2 = checkError(args[2]);
|
|
235
|
+
if (err2) {
|
|
236
|
+
return err2;
|
|
237
|
+
}
|
|
238
|
+
const text = toStringRV(topLeft(args[0]));
|
|
239
|
+
const oldText = toStringRV(topLeft(args[1]));
|
|
240
|
+
const newText = toStringRV(topLeft(args[2]));
|
|
241
|
+
// An empty old_text is a no-op in Excel. Without this guard we would
|
|
242
|
+
// `"abc".split("").join(newText)` and insert newText between every
|
|
243
|
+
// character, and the regex path would match empty strings infinitely.
|
|
244
|
+
if (oldText === "") {
|
|
245
|
+
return rvString(text);
|
|
246
|
+
}
|
|
247
|
+
if (args.length > 3) {
|
|
248
|
+
const instanceNumRV = toNumberRV(args[3]);
|
|
249
|
+
if (isError(instanceNumRV)) {
|
|
250
|
+
return instanceNumRV;
|
|
251
|
+
}
|
|
252
|
+
// Excel requires a positive integer; zero, negative, or non-numeric
|
|
253
|
+
// values are #VALUE!. Previously we let the replace pass silently
|
|
254
|
+
// no-op (since `count === 0` never matched), masking caller bugs.
|
|
255
|
+
if (!Number.isFinite(instanceNumRV.value) || instanceNumRV.value < 1) {
|
|
256
|
+
return ERRORS.VALUE;
|
|
257
|
+
}
|
|
258
|
+
const instanceNum = Math.trunc(instanceNumRV.value);
|
|
259
|
+
let count = 0;
|
|
260
|
+
return rvString(text.replace(new RegExp(escapeRegex(oldText), "g"), match => {
|
|
261
|
+
count++;
|
|
262
|
+
return count === instanceNum ? newText : match;
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
return rvString(text.split(oldText).join(newText));
|
|
266
|
+
};
|
|
267
|
+
export const fnREPLACE = args => {
|
|
268
|
+
const err = checkError(args[0]);
|
|
269
|
+
if (err) {
|
|
270
|
+
return err;
|
|
271
|
+
}
|
|
272
|
+
const text = toStringRV(args[0]);
|
|
273
|
+
// Implicit intersection on the numeric arguments — see LEFT.
|
|
274
|
+
const startNumRV = argToNumber(args[1]);
|
|
275
|
+
if (isError(startNumRV)) {
|
|
276
|
+
return startNumRV;
|
|
277
|
+
}
|
|
278
|
+
const startNum = Math.trunc(startNumRV.value);
|
|
279
|
+
const numCharsRV = argToNumber(args[2]);
|
|
280
|
+
if (isError(numCharsRV)) {
|
|
281
|
+
return numCharsRV;
|
|
282
|
+
}
|
|
283
|
+
const numChars = Math.trunc(numCharsRV.value);
|
|
284
|
+
// REPLACE: start_num >= 1, num_chars >= 0. Without this check, negative
|
|
285
|
+
// start_num becomes a slice with negative index and silently trims from
|
|
286
|
+
// the right, which does not match Excel's #VALUE! result.
|
|
287
|
+
if (startNum < 1 || numChars < 0) {
|
|
288
|
+
return ERRORS.VALUE;
|
|
289
|
+
}
|
|
290
|
+
const e3 = checkError(args[3]);
|
|
291
|
+
if (e3) {
|
|
292
|
+
return e3;
|
|
293
|
+
}
|
|
294
|
+
const newText = toStringRV(args[3]);
|
|
295
|
+
return rvString(text.slice(0, startNum - 1) + newText + text.slice(startNum - 1 + numChars));
|
|
296
|
+
};
|
|
297
|
+
// ============================================================================
|
|
298
|
+
// FIND / SEARCH
|
|
299
|
+
// ============================================================================
|
|
300
|
+
export const fnFIND = args => {
|
|
301
|
+
const err0 = checkError(args[0]);
|
|
302
|
+
if (err0) {
|
|
303
|
+
return err0;
|
|
304
|
+
}
|
|
305
|
+
const err1 = checkError(args[1]);
|
|
306
|
+
if (err1) {
|
|
307
|
+
return err1;
|
|
308
|
+
}
|
|
309
|
+
const findText = toStringRV(args[0]);
|
|
310
|
+
const withinText = toStringRV(args[1]);
|
|
311
|
+
let startNum;
|
|
312
|
+
if (args.length > 2) {
|
|
313
|
+
// Implicit intersection so an array supplied as start_num collapses
|
|
314
|
+
// to its top-left cell — matches Excel and the other text family.
|
|
315
|
+
const startNumRV = argToNumber(args[2]);
|
|
316
|
+
if (isError(startNumRV)) {
|
|
317
|
+
return startNumRV;
|
|
318
|
+
}
|
|
319
|
+
startNum = Math.trunc(startNumRV.value);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
startNum = 1;
|
|
323
|
+
}
|
|
324
|
+
// Excel's FIND rejects start_num outside [1, length(withinText)].
|
|
325
|
+
if (startNum < 1 || startNum > withinText.length + 1) {
|
|
326
|
+
return ERRORS.VALUE;
|
|
327
|
+
}
|
|
328
|
+
const idx = withinText.indexOf(findText, startNum - 1);
|
|
329
|
+
return idx === -1 ? ERRORS.VALUE : rvNumber(idx + 1);
|
|
330
|
+
};
|
|
331
|
+
export const fnSEARCH = args => {
|
|
332
|
+
const err0 = checkError(args[0]);
|
|
333
|
+
if (err0) {
|
|
334
|
+
return err0;
|
|
335
|
+
}
|
|
336
|
+
const err1 = checkError(args[1]);
|
|
337
|
+
if (err1) {
|
|
338
|
+
return err1;
|
|
339
|
+
}
|
|
340
|
+
let findText = toStringRV(args[0]);
|
|
341
|
+
const withinText = toStringRV(args[1]);
|
|
342
|
+
let startNum;
|
|
343
|
+
if (args.length > 2) {
|
|
344
|
+
const startNumRV = argToNumber(args[2]);
|
|
345
|
+
if (isError(startNumRV)) {
|
|
346
|
+
return startNumRV;
|
|
347
|
+
}
|
|
348
|
+
startNum = Math.trunc(startNumRV.value);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
startNum = 1;
|
|
352
|
+
}
|
|
353
|
+
if (startNum < 1 || startNum > withinText.length + 1) {
|
|
354
|
+
return ERRORS.VALUE;
|
|
355
|
+
}
|
|
356
|
+
// Use the shared Excel-wildcard → regex converter so SEARCH, MATCH,
|
|
357
|
+
// XLOOKUP, and SUMIF/COUNTIF agree on escape semantics (`~*`, `~?`, `~~`).
|
|
358
|
+
const pattern = excelWildcardToRegex(findText);
|
|
359
|
+
try {
|
|
360
|
+
const re = new RegExp(pattern, "i");
|
|
361
|
+
const sub = withinText.slice(startNum - 1);
|
|
362
|
+
const match = re.exec(sub);
|
|
363
|
+
return match ? rvNumber(match.index + startNum) : ERRORS.VALUE;
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// If regex is invalid, fall back to simple case-insensitive indexOf.
|
|
367
|
+
findText = findText.toLowerCase();
|
|
368
|
+
const idx = withinText.toLowerCase().indexOf(findText, startNum - 1);
|
|
369
|
+
return idx === -1 ? ERRORS.VALUE : rvNumber(idx + 1);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
// ============================================================================
|
|
373
|
+
// REPT
|
|
374
|
+
// ============================================================================
|
|
375
|
+
export const fnREPT = args => {
|
|
376
|
+
const err = checkError(args[0]);
|
|
377
|
+
if (err) {
|
|
378
|
+
return err;
|
|
379
|
+
}
|
|
380
|
+
const text = toStringRV(topLeft(args[0]));
|
|
381
|
+
const timesRV = toNumberRV(args[1]);
|
|
382
|
+
if (isError(timesRV)) {
|
|
383
|
+
return timesRV;
|
|
384
|
+
}
|
|
385
|
+
const times = Math.floor(timesRV.value);
|
|
386
|
+
if (times < 0) {
|
|
387
|
+
return ERRORS.VALUE;
|
|
388
|
+
}
|
|
389
|
+
// Excel caps the result at 32767 characters; we additionally bail out
|
|
390
|
+
// early on huge products so the engine can't be DoS'd into allocating
|
|
391
|
+
// a multi-gigabyte string. (R6-P1-4)
|
|
392
|
+
const total = text.length * times;
|
|
393
|
+
if (total > 32767) {
|
|
394
|
+
return ERRORS.VALUE;
|
|
395
|
+
}
|
|
396
|
+
return rvString(text.repeat(times));
|
|
397
|
+
};
|
|
398
|
+
// ============================================================================
|
|
399
|
+
// TEXT (complex number/date formatting)
|
|
400
|
+
// ============================================================================
|
|
401
|
+
export const fnTEXT = args => {
|
|
402
|
+
const rawVal = topLeft(args[0]);
|
|
403
|
+
if (isError(rawVal)) {
|
|
404
|
+
return rawVal;
|
|
405
|
+
}
|
|
406
|
+
const e1 = checkError(args[1]);
|
|
407
|
+
if (e1) {
|
|
408
|
+
return e1;
|
|
409
|
+
}
|
|
410
|
+
const fmt = toStringRV(args[1]);
|
|
411
|
+
// "@" format = return text as-is
|
|
412
|
+
if (fmt === "@") {
|
|
413
|
+
return rvString(toStringRV(rawVal));
|
|
414
|
+
}
|
|
415
|
+
// Conditional sections: positive;negative;zero;text (up to 4 parts).
|
|
416
|
+
// Text input — when the value is a String/Boolean that doesn't parse
|
|
417
|
+
// as a number — is routed to the 4th section if present. Without this
|
|
418
|
+
// early dispatch, the later `toNumberRV(rawVal)` would #VALUE! and
|
|
419
|
+
// `TEXT("hi", "0;-0;0;@")` could never reach the 4th section.
|
|
420
|
+
const sections = splitFormatSections(fmt);
|
|
421
|
+
const isTextInput = rawVal.kind === RVKind.String;
|
|
422
|
+
if (isTextInput && sections.length >= 4) {
|
|
423
|
+
// The 4th section formats the text value — `@` is a placeholder
|
|
424
|
+
// that re-emits the source string (like Excel's `@` metacharacter).
|
|
425
|
+
return rvString(sections[3].replace(/@/g, toStringRV(rawVal)));
|
|
426
|
+
}
|
|
427
|
+
const valRV = toNumberRV(rawVal);
|
|
428
|
+
if (isError(valRV)) {
|
|
429
|
+
return valRV;
|
|
430
|
+
}
|
|
431
|
+
const val = valRV.value;
|
|
432
|
+
let activeFmt;
|
|
433
|
+
if (sections.length >= 3) {
|
|
434
|
+
activeFmt = val > 0 ? sections[0] : val < 0 ? sections[1] : sections[2];
|
|
435
|
+
}
|
|
436
|
+
else if (sections.length === 2) {
|
|
437
|
+
activeFmt = val >= 0 ? sections[0] : sections[1];
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
activeFmt = sections[0];
|
|
441
|
+
}
|
|
442
|
+
// For negative section, use absolute value (sign is in the format)
|
|
443
|
+
const useVal = sections.length >= 2 && val < 0 ? Math.abs(val) : val;
|
|
444
|
+
return rvString(formatWithCode(useVal, activeFmt, rawVal));
|
|
445
|
+
};
|
|
446
|
+
/**
|
|
447
|
+
* Split format string on `;` separators, respecting quoted strings.
|
|
448
|
+
*/
|
|
449
|
+
function splitFormatSections(fmt) {
|
|
450
|
+
const sections = [];
|
|
451
|
+
let current = "";
|
|
452
|
+
let inQuotes = false;
|
|
453
|
+
for (let i = 0; i < fmt.length; i++) {
|
|
454
|
+
if (fmt[i] === '"') {
|
|
455
|
+
inQuotes = !inQuotes;
|
|
456
|
+
current += fmt[i];
|
|
457
|
+
}
|
|
458
|
+
else if (fmt[i] === ";" && !inQuotes) {
|
|
459
|
+
sections.push(current);
|
|
460
|
+
current = "";
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
current += fmt[i];
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
sections.push(current);
|
|
467
|
+
return sections;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Format a number using a single format code section.
|
|
471
|
+
*/
|
|
472
|
+
function formatWithCode(val, fmt, rawVal) {
|
|
473
|
+
const upper = fmt.toUpperCase();
|
|
474
|
+
// Date/time formats — detect by presence of date/time tokens
|
|
475
|
+
if (upper.includes("YYYY") ||
|
|
476
|
+
upper.includes("YY") ||
|
|
477
|
+
upper.includes("MMMM") ||
|
|
478
|
+
upper.includes("MMM") ||
|
|
479
|
+
upper.includes("DDDD") ||
|
|
480
|
+
upper.includes("DDD") ||
|
|
481
|
+
upper.includes("DD") ||
|
|
482
|
+
/(?:^|[^H])MM(?!M)/.test(upper) ||
|
|
483
|
+
/(?:^|[^M])M(?!M)/.test(upper.replace(/MMMM|MMM/g, "")) ||
|
|
484
|
+
upper.includes("HH") ||
|
|
485
|
+
/(?:^|[^H])H(?!H)/.test(upper) ||
|
|
486
|
+
upper.includes("SS") ||
|
|
487
|
+
upper.includes("AM/PM") ||
|
|
488
|
+
upper.includes("A/P")) {
|
|
489
|
+
return formatDate(val, fmt);
|
|
490
|
+
}
|
|
491
|
+
// Percentage format
|
|
492
|
+
if (fmt.includes("%")) {
|
|
493
|
+
const stripped = fmt.replace(/[^0#.%,]/g, "");
|
|
494
|
+
const dotIdx = stripped.indexOf(".");
|
|
495
|
+
const afterDot = dotIdx >= 0
|
|
496
|
+
? stripped
|
|
497
|
+
.slice(dotIdx + 1)
|
|
498
|
+
.replace(/%/g, "")
|
|
499
|
+
.replace(/,/g, "")
|
|
500
|
+
: "";
|
|
501
|
+
const decimals = afterDot.length;
|
|
502
|
+
const pctVal = val * 100;
|
|
503
|
+
let result = pctVal.toFixed(decimals);
|
|
504
|
+
if (fmt.includes(",")) {
|
|
505
|
+
const parts = result.split(".");
|
|
506
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
507
|
+
result = parts.join(".");
|
|
508
|
+
}
|
|
509
|
+
return result + "%";
|
|
510
|
+
}
|
|
511
|
+
// Scientific notation: 0.00E+00
|
|
512
|
+
if (/[0#]\.?[0#]*E[+-][0#]+/i.test(fmt)) {
|
|
513
|
+
return formatScientific(val, fmt);
|
|
514
|
+
}
|
|
515
|
+
// Fraction format: # ?/? or # ??/??
|
|
516
|
+
if (/[?]+\/[?]+/.test(fmt)) {
|
|
517
|
+
return formatFraction(val, fmt);
|
|
518
|
+
}
|
|
519
|
+
// Number format with 0 and #
|
|
520
|
+
if (fmt.includes("0") || fmt.includes("#")) {
|
|
521
|
+
return formatNumber(val, fmt);
|
|
522
|
+
}
|
|
523
|
+
// Literal-only section (no numeric placeholders). Excel emits the
|
|
524
|
+
// format string verbatim, substituting any `@` with the stringified
|
|
525
|
+
// value. This matters for 3- and 4-section formats like
|
|
526
|
+
// `"pos;neg;zero"` — the selected section has no `#`/`0` but should
|
|
527
|
+
// still appear in the output.
|
|
528
|
+
return fmt.replace(/@/g, String(val));
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Format number using #, 0, comma patterns.
|
|
532
|
+
*/
|
|
533
|
+
/**
|
|
534
|
+
* Format number using #, 0, comma patterns.
|
|
535
|
+
*
|
|
536
|
+
* The implementation tokenises the pattern left-to-right so that *any*
|
|
537
|
+
* literal character (parentheses, `+`/`-`, currency symbols, letters in
|
|
538
|
+
* quoted `"..."` segments, backslash-escaped characters, leading/trailing
|
|
539
|
+
* punctuation) is preserved in its original position. The previous
|
|
540
|
+
* implementation stripped everything that wasn't `0 # . ,` before analysis
|
|
541
|
+
* and tried to re-inject a handful of literals afterwards, which caused
|
|
542
|
+
* formats like `0;(0)` (→ "(5)" for -5) and `#,##0.00;-#,##0.00` to drop
|
|
543
|
+
* their negative-section literal characters entirely.
|
|
544
|
+
*/
|
|
545
|
+
function formatNumber(val, fmt) {
|
|
546
|
+
// Strip `[color]` and `[condition]` tags up front (Excel-style
|
|
547
|
+
// decorations that don't affect the numeric layout in our engine).
|
|
548
|
+
const cleanFmt = fmt.replace(/\[[^\]]*\]/g, "");
|
|
549
|
+
const tokens = [];
|
|
550
|
+
for (let i = 0; i < cleanFmt.length; i++) {
|
|
551
|
+
const ch = cleanFmt[i];
|
|
552
|
+
if (ch === '"') {
|
|
553
|
+
// Quoted literal run: consume until the closing quote.
|
|
554
|
+
let j = i + 1;
|
|
555
|
+
let buf = "";
|
|
556
|
+
while (j < cleanFmt.length && cleanFmt[j] !== '"') {
|
|
557
|
+
buf += cleanFmt[j];
|
|
558
|
+
j++;
|
|
559
|
+
}
|
|
560
|
+
tokens.push({ kind: "literal", text: buf });
|
|
561
|
+
i = j; // skip closing quote
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (ch === "\\" && i + 1 < cleanFmt.length) {
|
|
565
|
+
// Backslash escape: next character is a literal.
|
|
566
|
+
tokens.push({ kind: "literal", text: cleanFmt[i + 1] });
|
|
567
|
+
i++;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (ch === "0" || ch === "#") {
|
|
571
|
+
tokens.push({ kind: "digit", char: ch });
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
if (ch === ".") {
|
|
575
|
+
tokens.push({ kind: "dot" });
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (ch === ",") {
|
|
579
|
+
tokens.push({ kind: "comma" });
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
// Any other character is a literal (space, parentheses, currency
|
|
583
|
+
// symbol, sign, hyphen, etc.).
|
|
584
|
+
tokens.push({ kind: "literal", text: ch });
|
|
585
|
+
}
|
|
586
|
+
// Count integer/fraction digit slots and decide whether to group.
|
|
587
|
+
let sawDot = false;
|
|
588
|
+
const intDigitSlots = [];
|
|
589
|
+
const fracDigitSlots = [];
|
|
590
|
+
let hasGrouping = false;
|
|
591
|
+
for (const t of tokens) {
|
|
592
|
+
if (t.kind === "dot") {
|
|
593
|
+
sawDot = true;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (t.kind === "digit") {
|
|
597
|
+
if (sawDot) {
|
|
598
|
+
fracDigitSlots.push(t.char);
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
intDigitSlots.push(t.char);
|
|
602
|
+
}
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (t.kind === "comma" && !sawDot) {
|
|
606
|
+
// A comma between digit slots means "thousands grouping".
|
|
607
|
+
hasGrouping = true;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (intDigitSlots.length === 0 && fracDigitSlots.length === 0) {
|
|
611
|
+
return fmt; // Nothing to format; return the (raw) pattern.
|
|
612
|
+
}
|
|
613
|
+
// Round to the requested fractional precision.
|
|
614
|
+
const rounded = roundHalfAwayFromZeroFmt(val, fracDigitSlots.length);
|
|
615
|
+
const sign = rounded < 0 ? "-" : "";
|
|
616
|
+
const absStr = Math.abs(rounded).toFixed(fracDigitSlots.length);
|
|
617
|
+
const [intPartRaw, fracPart = ""] = absStr.split(".");
|
|
618
|
+
let intPart = intPartRaw;
|
|
619
|
+
// Pad the integer part to satisfy mandatory `0` slots.
|
|
620
|
+
const mandatoryInt = intDigitSlots.filter(s => s === "0").length;
|
|
621
|
+
if (intPart.length < mandatoryInt) {
|
|
622
|
+
intPart = intPart.padStart(mandatoryInt, "0");
|
|
623
|
+
}
|
|
624
|
+
// Apply thousand grouping if the pattern requested it.
|
|
625
|
+
let groupedInt = intPart;
|
|
626
|
+
if (hasGrouping) {
|
|
627
|
+
groupedInt = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
628
|
+
}
|
|
629
|
+
// Walk the tokens again and materialise output, interleaving the
|
|
630
|
+
// integer run, decimal point, and fractional run with literals.
|
|
631
|
+
let out = "";
|
|
632
|
+
let intCursor = 0; // index into groupedInt (right-to-left on integer digits)
|
|
633
|
+
// We emit integer digits in source order using a trick: scan tokens
|
|
634
|
+
// left-to-right; the first integer digit slot consumes `groupedInt[0]`
|
|
635
|
+
// if integer has more digits than slots all extras come out before the
|
|
636
|
+
// first digit slot (Excel overflow rule).
|
|
637
|
+
const totalIntSlots = intDigitSlots.length;
|
|
638
|
+
// Determine how much "overflow" we need to prepend. Overflow digits
|
|
639
|
+
// appear right before the first digit token we encounter.
|
|
640
|
+
const overflowDigits = Math.max(0, groupedInt.length - totalIntSlots);
|
|
641
|
+
let fracCursor = 0;
|
|
642
|
+
let emittedOverflow = false;
|
|
643
|
+
let firstDigitTokenIdx = -1;
|
|
644
|
+
for (let k = 0; k < tokens.length; k++) {
|
|
645
|
+
if (tokens[k].kind === "digit") {
|
|
646
|
+
// We only care about the first *integer* digit token — one that
|
|
647
|
+
// comes before the dot. If the pattern has no integer slots, we
|
|
648
|
+
// still need to emit any overflow somewhere; pick the first digit
|
|
649
|
+
// token regardless.
|
|
650
|
+
firstDigitTokenIdx = k;
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
if (tokens[k].kind === "dot") {
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
for (let k = 0; k < tokens.length; k++) {
|
|
658
|
+
const t = tokens[k];
|
|
659
|
+
if (t.kind === "literal") {
|
|
660
|
+
out += t.text;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (t.kind === "dot") {
|
|
664
|
+
if (fracDigitSlots.length > 0 || fracPart.length > 0) {
|
|
665
|
+
out += ".";
|
|
666
|
+
}
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (t.kind === "comma") {
|
|
670
|
+
// Grouping commas are consumed by the integer-build step above,
|
|
671
|
+
// so trailing commas here are literal (unusual but valid).
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
// digit token
|
|
675
|
+
if (sawDotAt(tokens, k)) {
|
|
676
|
+
// fractional slot
|
|
677
|
+
if (fracCursor < fracPart.length) {
|
|
678
|
+
out += fracPart[fracCursor];
|
|
679
|
+
}
|
|
680
|
+
else if (t.char === "0") {
|
|
681
|
+
out += "0";
|
|
682
|
+
}
|
|
683
|
+
fracCursor++;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
// integer slot
|
|
687
|
+
if (k === firstDigitTokenIdx && !emittedOverflow) {
|
|
688
|
+
if (overflowDigits > 0) {
|
|
689
|
+
out += groupedInt.slice(0, overflowDigits);
|
|
690
|
+
}
|
|
691
|
+
emittedOverflow = true;
|
|
692
|
+
}
|
|
693
|
+
const slotIdx = intCursor;
|
|
694
|
+
// Integer slots map right-to-left onto `groupedInt`'s right-to-left
|
|
695
|
+
// ordering. We'll compute the source character for this slot from
|
|
696
|
+
// the right edge.
|
|
697
|
+
const srcIdx = overflowDigits + slotIdx;
|
|
698
|
+
if (srcIdx < groupedInt.length) {
|
|
699
|
+
out += groupedInt[srcIdx];
|
|
700
|
+
}
|
|
701
|
+
else if (t.char === "0") {
|
|
702
|
+
out += "0";
|
|
703
|
+
}
|
|
704
|
+
intCursor++;
|
|
705
|
+
}
|
|
706
|
+
return sign + out;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Did a dot token appear at any index < k in `tokens`? Used to classify
|
|
710
|
+
* each digit slot as integer vs fractional during the emit pass.
|
|
711
|
+
*/
|
|
712
|
+
function sawDotAt(tokens, k) {
|
|
713
|
+
for (let i = 0; i < k; i++) {
|
|
714
|
+
if (tokens[i].kind === "dot") {
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Round `val` to `decimals` fractional digits using Excel's half-away-
|
|
722
|
+
* from-zero convention (the same rule `formatNumber`'s caller relies on
|
|
723
|
+
* to keep `-0.5` displaying as `"-1"` instead of `"0"`).
|
|
724
|
+
*/
|
|
725
|
+
function roundHalfAwayFromZeroFmt(val, decimals) {
|
|
726
|
+
if (!isFinite(val) || decimals < 0) {
|
|
727
|
+
return val;
|
|
728
|
+
}
|
|
729
|
+
const factor = Math.pow(10, decimals);
|
|
730
|
+
return ((val < 0 ? -1 : 1) * Math.round(Math.abs(val) * factor)) / factor;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Format a number in scientific notation: 0.00E+00
|
|
734
|
+
*/
|
|
735
|
+
function formatScientific(val, fmt) {
|
|
736
|
+
const upper = fmt.toUpperCase();
|
|
737
|
+
const eIdx = upper.indexOf("E");
|
|
738
|
+
const beforeE = fmt.slice(0, eIdx);
|
|
739
|
+
const afterE = fmt.slice(eIdx + 1);
|
|
740
|
+
// Count decimal places in mantissa
|
|
741
|
+
const dotIdx = beforeE.indexOf(".");
|
|
742
|
+
const mantissaDecimals = dotIdx >= 0 ? beforeE.slice(dotIdx + 1).replace(/[^0#]/g, "").length : 0;
|
|
743
|
+
// Count exponent digits
|
|
744
|
+
const expSign = afterE[0] === "+" || afterE[0] === "-" ? afterE[0] : "+";
|
|
745
|
+
const expDigits = afterE.replace(/[^0#]/g, "").length;
|
|
746
|
+
const exp = val === 0 ? 0 : Math.floor(Math.log10(Math.abs(val)));
|
|
747
|
+
const mantissa = val / Math.pow(10, exp);
|
|
748
|
+
const expStr = (exp >= 0 && expSign === "+" ? "+" : exp < 0 ? "-" : "") +
|
|
749
|
+
String(Math.abs(exp)).padStart(expDigits, "0");
|
|
750
|
+
return mantissa.toFixed(mantissaDecimals) + "E" + expStr;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Format a number as a fraction: "# ?/?" or "# ??/??"
|
|
754
|
+
*/
|
|
755
|
+
function formatFraction(val, fmt) {
|
|
756
|
+
const whole = Math.floor(Math.abs(val));
|
|
757
|
+
const frac = Math.abs(val) - whole;
|
|
758
|
+
const sign = val < 0 ? "-" : "";
|
|
759
|
+
if (frac === 0) {
|
|
760
|
+
if (fmt.includes("#") || fmt.includes("0")) {
|
|
761
|
+
return sign + whole + " ";
|
|
762
|
+
}
|
|
763
|
+
return sign + whole + " 0/1";
|
|
764
|
+
}
|
|
765
|
+
// Determine max denominator from ? count
|
|
766
|
+
const slashIdx = fmt.indexOf("/");
|
|
767
|
+
const denomPattern = fmt.slice(slashIdx + 1).replace(/[^?0#]/g, "");
|
|
768
|
+
const maxDenom = Math.pow(10, denomPattern.length) - 1;
|
|
769
|
+
// Find best fraction approximation
|
|
770
|
+
let bestNum = 0;
|
|
771
|
+
let bestDen = 1;
|
|
772
|
+
let bestError = Math.abs(frac);
|
|
773
|
+
for (let d = 1; d <= maxDenom; d++) {
|
|
774
|
+
const n = Math.round(frac * d);
|
|
775
|
+
const error = Math.abs(frac - n / d);
|
|
776
|
+
if (error < bestError) {
|
|
777
|
+
bestError = error;
|
|
778
|
+
bestNum = n;
|
|
779
|
+
bestDen = d;
|
|
780
|
+
if (error === 0) {
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const hasWholePart = fmt.indexOf("?") > 0 && fmt[0] !== "?" && fmt[0] !== "/";
|
|
786
|
+
if (hasWholePart) {
|
|
787
|
+
const numStr = String(bestNum).padStart(denomPattern.length, " ");
|
|
788
|
+
const denStr = String(bestDen).padStart(denomPattern.length, " ");
|
|
789
|
+
return sign + (whole > 0 ? whole + " " : "") + numStr + "/" + denStr;
|
|
790
|
+
}
|
|
791
|
+
return sign + (whole * bestDen + bestNum) + "/" + bestDen;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Day name arrays for date formatting.
|
|
795
|
+
*/
|
|
796
|
+
const DAY_NAMES_FULL = [
|
|
797
|
+
"Sunday",
|
|
798
|
+
"Monday",
|
|
799
|
+
"Tuesday",
|
|
800
|
+
"Wednesday",
|
|
801
|
+
"Thursday",
|
|
802
|
+
"Friday",
|
|
803
|
+
"Saturday"
|
|
804
|
+
];
|
|
805
|
+
const DAY_NAMES_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
806
|
+
const MONTH_NAMES_FULL = [
|
|
807
|
+
"January",
|
|
808
|
+
"February",
|
|
809
|
+
"March",
|
|
810
|
+
"April",
|
|
811
|
+
"May",
|
|
812
|
+
"June",
|
|
813
|
+
"July",
|
|
814
|
+
"August",
|
|
815
|
+
"September",
|
|
816
|
+
"October",
|
|
817
|
+
"November",
|
|
818
|
+
"December"
|
|
819
|
+
];
|
|
820
|
+
const MONTH_NAMES_SHORT = [
|
|
821
|
+
"Jan",
|
|
822
|
+
"Feb",
|
|
823
|
+
"Mar",
|
|
824
|
+
"Apr",
|
|
825
|
+
"May",
|
|
826
|
+
"Jun",
|
|
827
|
+
"Jul",
|
|
828
|
+
"Aug",
|
|
829
|
+
"Sep",
|
|
830
|
+
"Oct",
|
|
831
|
+
"Nov",
|
|
832
|
+
"Dec"
|
|
833
|
+
];
|
|
834
|
+
/**
|
|
835
|
+
* Format a numeric Excel serial date with a date/time format code.
|
|
836
|
+
*
|
|
837
|
+
* In the native value system, dates are always numbers (serial values).
|
|
838
|
+
* We always use excelToDate() to convert and then read UTC fields so the
|
|
839
|
+
* output does not drift by a day in timezones west of UTC.
|
|
840
|
+
*/
|
|
841
|
+
/**
|
|
842
|
+
* Format a serial-date value using an Excel-style pattern.
|
|
843
|
+
*
|
|
844
|
+
* Unlike a naive multi-pass regex approach, this implementation tokenises
|
|
845
|
+
* the pattern in a single left-to-right sweep and disambiguates the
|
|
846
|
+
* overloaded `m` / `mm` token by looking at its neighbours:
|
|
847
|
+
*
|
|
848
|
+
* - `mm` after `h:` or before `:ss` is rendered as **minutes**
|
|
849
|
+
* - otherwise `mm` is rendered as **month**
|
|
850
|
+
*
|
|
851
|
+
* Without this, a mixed format like `"yyyy-mm-dd hh:mm:ss"` would render
|
|
852
|
+
* the time's minutes as months (both tokens fire the same regex), giving
|
|
853
|
+
* garbage like `"2023-06-15 14:06:45"` for a timestamp at 14:30:45.
|
|
854
|
+
*/
|
|
855
|
+
function formatDate(val, fmt) {
|
|
856
|
+
const d = excelToDate(val, isDate1904());
|
|
857
|
+
const year = d.getUTCFullYear();
|
|
858
|
+
const month0 = d.getUTCMonth();
|
|
859
|
+
const dayN = d.getUTCDate();
|
|
860
|
+
const dow = d.getUTCDay();
|
|
861
|
+
const fracDay = val % 1;
|
|
862
|
+
const totalSeconds = Math.round(Math.abs(fracDay) * 86400);
|
|
863
|
+
const hours = Math.floor(totalSeconds / 3600) % 24;
|
|
864
|
+
const minutes = Math.floor(totalSeconds / 60) % 60;
|
|
865
|
+
const seconds = totalSeconds % 60;
|
|
866
|
+
const hasAmPmToken = /AM\/PM/i.test(fmt) || /A\/P/i.test(fmt);
|
|
867
|
+
const h12 = hours % 12 === 0 ? 12 : hours % 12;
|
|
868
|
+
const ampm = hours < 12 ? "AM" : "PM";
|
|
869
|
+
const ap = hours < 12 ? "A" : "P";
|
|
870
|
+
const runs = [];
|
|
871
|
+
let i = 0;
|
|
872
|
+
while (i < fmt.length) {
|
|
873
|
+
const ch = fmt[i];
|
|
874
|
+
const lo = ch.toLowerCase();
|
|
875
|
+
// Handle two-char literals that must not tokenise as letters
|
|
876
|
+
if (fmt.slice(i, i + 5).toUpperCase() === "AM/PM") {
|
|
877
|
+
runs.push({ kind: "literal", text: ampm });
|
|
878
|
+
i += 5;
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
if (fmt.slice(i, i + 3).toUpperCase() === "A/P") {
|
|
882
|
+
runs.push({ kind: "literal", text: ap });
|
|
883
|
+
i += 3;
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (lo === "y" || lo === "m" || lo === "d" || lo === "h" || lo === "s") {
|
|
887
|
+
let j = i + 1;
|
|
888
|
+
while (j < fmt.length && fmt[j].toLowerCase() === lo) {
|
|
889
|
+
j++;
|
|
890
|
+
}
|
|
891
|
+
runs.push({ kind: "letters", lower: lo, count: j - i });
|
|
892
|
+
i = j;
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
// Literal: consume until the next token-letter (to keep literal runs
|
|
896
|
+
// contiguous and reduce array churn).
|
|
897
|
+
let j = i + 1;
|
|
898
|
+
while (j < fmt.length) {
|
|
899
|
+
const nx = fmt[j].toLowerCase();
|
|
900
|
+
if (nx === "y" || nx === "m" || nx === "d" || nx === "h" || nx === "s") {
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
if (fmt.slice(j, j + 5).toUpperCase() === "AM/PM") {
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
if (fmt.slice(j, j + 3).toUpperCase() === "A/P") {
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
j++;
|
|
910
|
+
}
|
|
911
|
+
runs.push({ kind: "literal", text: fmt.slice(i, j) });
|
|
912
|
+
i = j;
|
|
913
|
+
}
|
|
914
|
+
// Phase 2: render each run, using neighbour context to decide whether
|
|
915
|
+
// an `m` / `mm` run means minute (when adjacent to an `h` or `s` run)
|
|
916
|
+
// or month (otherwise).
|
|
917
|
+
let out = "";
|
|
918
|
+
for (let r = 0; r < runs.length; r++) {
|
|
919
|
+
const run = runs[r];
|
|
920
|
+
if (run.kind === "literal") {
|
|
921
|
+
out += run.text;
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
switch (run.lower) {
|
|
925
|
+
case "y":
|
|
926
|
+
out += run.count >= 4 ? String(year).padStart(4, "0") : String(year).slice(-2);
|
|
927
|
+
break;
|
|
928
|
+
case "d":
|
|
929
|
+
if (run.count >= 4) {
|
|
930
|
+
out += DAY_NAMES_FULL[dow];
|
|
931
|
+
}
|
|
932
|
+
else if (run.count === 3) {
|
|
933
|
+
out += DAY_NAMES_SHORT[dow];
|
|
934
|
+
}
|
|
935
|
+
else if (run.count === 2) {
|
|
936
|
+
out += String(dayN).padStart(2, "0");
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
out += String(dayN);
|
|
940
|
+
}
|
|
941
|
+
break;
|
|
942
|
+
case "h":
|
|
943
|
+
out +=
|
|
944
|
+
run.count >= 2
|
|
945
|
+
? String(hasAmPmToken ? h12 : hours).padStart(2, "0")
|
|
946
|
+
: String(hasAmPmToken ? h12 : hours);
|
|
947
|
+
break;
|
|
948
|
+
case "s":
|
|
949
|
+
out += run.count >= 2 ? String(seconds).padStart(2, "0") : String(seconds);
|
|
950
|
+
break;
|
|
951
|
+
case "m": {
|
|
952
|
+
// Month-names render independent of context.
|
|
953
|
+
if (run.count === 4) {
|
|
954
|
+
out += MONTH_NAMES_FULL[month0];
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
if (run.count === 3) {
|
|
958
|
+
out += MONTH_NAMES_SHORT[month0];
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
// Otherwise `m` / `mm` is month-or-minute. Follow Excel's rule:
|
|
962
|
+
// treat as minute iff an adjacent letter-run is `h` or `s`.
|
|
963
|
+
const isMinute = isAdjacentTimeContext(runs, r);
|
|
964
|
+
const value = isMinute ? minutes : month0 + 1;
|
|
965
|
+
out += run.count >= 2 ? String(value).padStart(2, "0") : String(value);
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return out;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Determine whether the `m` / `mm` run at `runs[idx]` should render as
|
|
974
|
+
* minutes (true) or months (false). Excel's rule, simplified: minutes
|
|
975
|
+
* when the immediately preceding or following *letter* run is `h` or
|
|
976
|
+
* `s`, ignoring intervening literal separators like `:`.
|
|
977
|
+
*/
|
|
978
|
+
function isAdjacentTimeContext(runs, idx) {
|
|
979
|
+
for (let j = idx - 1; j >= 0; j--) {
|
|
980
|
+
const p = runs[j];
|
|
981
|
+
if (p.kind === "letters") {
|
|
982
|
+
if (p.lower === "h" || p.lower === "s") {
|
|
983
|
+
return true;
|
|
984
|
+
}
|
|
985
|
+
break;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
for (let j = idx + 1; j < runs.length; j++) {
|
|
989
|
+
const p = runs[j];
|
|
990
|
+
if (p.kind === "letters") {
|
|
991
|
+
if (p.lower === "h" || p.lower === "s") {
|
|
992
|
+
return true;
|
|
993
|
+
}
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
// ============================================================================
|
|
1000
|
+
// VALUE / EXACT
|
|
1001
|
+
// ============================================================================
|
|
1002
|
+
export const fnVALUE = args => {
|
|
1003
|
+
const err = checkError(args[0]);
|
|
1004
|
+
if (err) {
|
|
1005
|
+
return err;
|
|
1006
|
+
}
|
|
1007
|
+
// Delegate to the central numeric-string parser. It rejects empty /
|
|
1008
|
+
// whitespace-only / Infinity / NaN / hex forms the way Excel does, which
|
|
1009
|
+
// a naive `Number(s)` would accept silently.
|
|
1010
|
+
return toNumberRV(topLeft(args[0]));
|
|
1011
|
+
};
|
|
1012
|
+
export const fnEXACT = args => {
|
|
1013
|
+
const err0 = checkError(args[0]);
|
|
1014
|
+
if (err0) {
|
|
1015
|
+
return err0;
|
|
1016
|
+
}
|
|
1017
|
+
const err1 = checkError(args[1]);
|
|
1018
|
+
if (err1) {
|
|
1019
|
+
return err1;
|
|
1020
|
+
}
|
|
1021
|
+
return rvBoolean(toStringRV(args[0]) === toStringRV(args[1]));
|
|
1022
|
+
};
|
|
1023
|
+
// ============================================================================
|
|
1024
|
+
// Additional Text Functions
|
|
1025
|
+
// ============================================================================
|
|
1026
|
+
export const fnCODE = args => {
|
|
1027
|
+
const err = checkError(args[0]);
|
|
1028
|
+
if (err) {
|
|
1029
|
+
return err;
|
|
1030
|
+
}
|
|
1031
|
+
const text = toStringRV(topLeft(args[0]));
|
|
1032
|
+
return text.length > 0 ? rvNumber(text.charCodeAt(0)) : ERRORS.VALUE;
|
|
1033
|
+
};
|
|
1034
|
+
export const fnCHAR = args => {
|
|
1035
|
+
const err = checkError(args[0]);
|
|
1036
|
+
if (err) {
|
|
1037
|
+
return err;
|
|
1038
|
+
}
|
|
1039
|
+
const nRV = toNumberRV(topLeft(args[0]));
|
|
1040
|
+
if (isError(nRV)) {
|
|
1041
|
+
return nRV;
|
|
1042
|
+
}
|
|
1043
|
+
// Excel's CHAR accepts integers in [1, 255] only; outside the ANSI range
|
|
1044
|
+
// it returns #VALUE!. We also truncate fractional inputs toward zero to
|
|
1045
|
+
// match Excel's coercion semantics.
|
|
1046
|
+
const code = Math.trunc(nRV.value);
|
|
1047
|
+
if (code < 1 || code > 255) {
|
|
1048
|
+
return ERRORS.VALUE;
|
|
1049
|
+
}
|
|
1050
|
+
return rvString(String.fromCharCode(code));
|
|
1051
|
+
};
|
|
1052
|
+
export const fnCLEAN = args => {
|
|
1053
|
+
const err = checkError(args[0]);
|
|
1054
|
+
if (err) {
|
|
1055
|
+
return err;
|
|
1056
|
+
}
|
|
1057
|
+
const text = toStringRV(topLeft(args[0]));
|
|
1058
|
+
// Remove non-printable ASCII control characters (0x00-0x1F)
|
|
1059
|
+
let result = "";
|
|
1060
|
+
for (let i = 0; i < text.length; i++) {
|
|
1061
|
+
if (text.charCodeAt(i) >= 32) {
|
|
1062
|
+
result += text[i];
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return rvString(result);
|
|
1066
|
+
};
|
|
1067
|
+
export const fnT = args => {
|
|
1068
|
+
const v = topLeft(args[0]);
|
|
1069
|
+
if (isError(v)) {
|
|
1070
|
+
return v;
|
|
1071
|
+
}
|
|
1072
|
+
return v.kind === RVKind.String ? v : rvString("");
|
|
1073
|
+
};
|
|
1074
|
+
// ============================================================================
|
|
1075
|
+
// More Text Functions
|
|
1076
|
+
// ============================================================================
|
|
1077
|
+
export const fnUNICHAR = args => {
|
|
1078
|
+
const err = checkError(args[0]);
|
|
1079
|
+
if (err) {
|
|
1080
|
+
return err;
|
|
1081
|
+
}
|
|
1082
|
+
const nRV = toNumberRV(args[0]);
|
|
1083
|
+
if (isError(nRV)) {
|
|
1084
|
+
return nRV;
|
|
1085
|
+
}
|
|
1086
|
+
const code = Math.floor(nRV.value);
|
|
1087
|
+
if (code < 1) {
|
|
1088
|
+
return ERRORS.VALUE;
|
|
1089
|
+
}
|
|
1090
|
+
try {
|
|
1091
|
+
return rvString(String.fromCodePoint(code));
|
|
1092
|
+
}
|
|
1093
|
+
catch {
|
|
1094
|
+
return ERRORS.VALUE;
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
export const fnUNICODE = args => {
|
|
1098
|
+
const err = checkError(args[0]);
|
|
1099
|
+
if (err) {
|
|
1100
|
+
return err;
|
|
1101
|
+
}
|
|
1102
|
+
const text = toStringRV(args[0]);
|
|
1103
|
+
if (text.length === 0) {
|
|
1104
|
+
return ERRORS.VALUE;
|
|
1105
|
+
}
|
|
1106
|
+
const cp = text.codePointAt(0);
|
|
1107
|
+
return cp !== undefined ? rvNumber(cp) : ERRORS.VALUE;
|
|
1108
|
+
};
|
|
1109
|
+
export const fnBAHTTEXT = args => {
|
|
1110
|
+
const err = checkError(args[0]);
|
|
1111
|
+
if (err) {
|
|
1112
|
+
return err;
|
|
1113
|
+
}
|
|
1114
|
+
return rvString(toStringRV(args[0]));
|
|
1115
|
+
};
|
|
1116
|
+
export const fnDOLLAR = args => {
|
|
1117
|
+
const numRV = toNumberRV(args[0]);
|
|
1118
|
+
if (isError(numRV)) {
|
|
1119
|
+
return numRV;
|
|
1120
|
+
}
|
|
1121
|
+
const num = numRV.value;
|
|
1122
|
+
let decimals;
|
|
1123
|
+
if (args.length > 1) {
|
|
1124
|
+
const decRV = toNumberRV(args[1]);
|
|
1125
|
+
if (isError(decRV)) {
|
|
1126
|
+
return decRV;
|
|
1127
|
+
}
|
|
1128
|
+
decimals = decRV.value;
|
|
1129
|
+
}
|
|
1130
|
+
else {
|
|
1131
|
+
decimals = 2;
|
|
1132
|
+
}
|
|
1133
|
+
const d = Math.floor(decimals);
|
|
1134
|
+
let rounded;
|
|
1135
|
+
if (d < 0) {
|
|
1136
|
+
const factor = Math.pow(10, -d);
|
|
1137
|
+
rounded = Math.round(Math.abs(num) / factor) * factor;
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
rounded = Math.abs(num);
|
|
1141
|
+
}
|
|
1142
|
+
const formatted = rounded.toFixed(Math.max(0, d));
|
|
1143
|
+
const parts = formatted.split(".");
|
|
1144
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
1145
|
+
const result = parts.join(".");
|
|
1146
|
+
return rvString(num < 0 ? `($${result})` : `$${result}`);
|
|
1147
|
+
};
|
|
1148
|
+
export const fnFIXED = args => {
|
|
1149
|
+
const numRV = toNumberRV(args[0]);
|
|
1150
|
+
if (isError(numRV)) {
|
|
1151
|
+
return numRV;
|
|
1152
|
+
}
|
|
1153
|
+
const num = numRV.value;
|
|
1154
|
+
let decimals;
|
|
1155
|
+
if (args.length > 1) {
|
|
1156
|
+
const decRV = toNumberRV(args[1]);
|
|
1157
|
+
if (isError(decRV)) {
|
|
1158
|
+
return decRV;
|
|
1159
|
+
}
|
|
1160
|
+
decimals = decRV.value;
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
decimals = 2;
|
|
1164
|
+
}
|
|
1165
|
+
let noCommas;
|
|
1166
|
+
if (args.length > 2) {
|
|
1167
|
+
const ncRV = toBooleanRV(args[2]);
|
|
1168
|
+
if (isError(ncRV)) {
|
|
1169
|
+
return ncRV;
|
|
1170
|
+
}
|
|
1171
|
+
noCommas = ncRV.value;
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
noCommas = false;
|
|
1175
|
+
}
|
|
1176
|
+
const d = Math.floor(decimals);
|
|
1177
|
+
let rounded;
|
|
1178
|
+
if (d < 0) {
|
|
1179
|
+
const factor = Math.pow(10, -d);
|
|
1180
|
+
rounded = Math.round(num / factor) * factor;
|
|
1181
|
+
}
|
|
1182
|
+
else {
|
|
1183
|
+
rounded = num;
|
|
1184
|
+
}
|
|
1185
|
+
let result = rounded.toFixed(Math.max(0, d));
|
|
1186
|
+
if (!noCommas) {
|
|
1187
|
+
const parts = result.split(".");
|
|
1188
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
1189
|
+
result = parts.join(".");
|
|
1190
|
+
}
|
|
1191
|
+
return rvString(result);
|
|
1192
|
+
};
|
|
1193
|
+
export const fnASC = args => {
|
|
1194
|
+
const err = checkError(args[0]);
|
|
1195
|
+
if (err) {
|
|
1196
|
+
return err;
|
|
1197
|
+
}
|
|
1198
|
+
const text = toStringRV(args[0]);
|
|
1199
|
+
return rvString(text.replace(/[\uFF01-\uFF5E]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0xfee0)));
|
|
1200
|
+
};
|
|
1201
|
+
export const fnDBCS = args => {
|
|
1202
|
+
const err = checkError(args[0]);
|
|
1203
|
+
if (err) {
|
|
1204
|
+
return err;
|
|
1205
|
+
}
|
|
1206
|
+
const text = toStringRV(args[0]);
|
|
1207
|
+
return rvString(text.replace(/[!-~]/g, ch => String.fromCharCode(ch.charCodeAt(0) + 0xfee0)));
|
|
1208
|
+
};
|
|
1209
|
+
export const fnJIS = args => fnDBCS(args);
|
|
1210
|
+
export const fnPHONETIC = args => {
|
|
1211
|
+
const err = checkError(args[0]);
|
|
1212
|
+
if (err) {
|
|
1213
|
+
return err;
|
|
1214
|
+
}
|
|
1215
|
+
// Implicit intersection: array → top-left cell, matching the rest of
|
|
1216
|
+
// the text-function family.
|
|
1217
|
+
return rvString(toStringRV(topLeft(args[0])));
|
|
1218
|
+
};
|
|
1219
|
+
export const fnNUMBERVALUE = args => {
|
|
1220
|
+
const e0 = checkError(args[0]);
|
|
1221
|
+
if (e0) {
|
|
1222
|
+
return e0;
|
|
1223
|
+
}
|
|
1224
|
+
let text = toStringRV(args[0]);
|
|
1225
|
+
let decSep = ".";
|
|
1226
|
+
if (args.length > 1) {
|
|
1227
|
+
const e1 = checkError(args[1]);
|
|
1228
|
+
if (e1) {
|
|
1229
|
+
return e1;
|
|
1230
|
+
}
|
|
1231
|
+
decSep = toStringRV(args[1]);
|
|
1232
|
+
}
|
|
1233
|
+
let grpSep = ",";
|
|
1234
|
+
if (args.length > 2) {
|
|
1235
|
+
const e2 = checkError(args[2]);
|
|
1236
|
+
if (e2) {
|
|
1237
|
+
return e2;
|
|
1238
|
+
}
|
|
1239
|
+
grpSep = toStringRV(args[2]);
|
|
1240
|
+
}
|
|
1241
|
+
text = text.split(grpSep).join("");
|
|
1242
|
+
if (decSep !== ".") {
|
|
1243
|
+
text = text.replace(decSep, ".");
|
|
1244
|
+
}
|
|
1245
|
+
// Handle percentage
|
|
1246
|
+
const isPct = text.endsWith("%");
|
|
1247
|
+
if (isPct) {
|
|
1248
|
+
text = text.slice(0, -1);
|
|
1249
|
+
}
|
|
1250
|
+
// `Number("")` is 0, not NaN — reject empty / whitespace-only inputs
|
|
1251
|
+
// so `NUMBERVALUE("")` does not silently produce 0 (R6-P1-6).
|
|
1252
|
+
if (text.trim() === "") {
|
|
1253
|
+
return ERRORS.VALUE;
|
|
1254
|
+
}
|
|
1255
|
+
const n = Number(text);
|
|
1256
|
+
if (isNaN(n)) {
|
|
1257
|
+
return ERRORS.VALUE;
|
|
1258
|
+
}
|
|
1259
|
+
return rvNumber(isPct ? n / 100 : n);
|
|
1260
|
+
};
|
|
1261
|
+
// ============================================================================
|
|
1262
|
+
// Excel 365 Text Functions: TEXTBEFORE, TEXTAFTER, TEXTSPLIT
|
|
1263
|
+
// ============================================================================
|
|
1264
|
+
/**
|
|
1265
|
+
* Parse the common [instance_num, match_mode, match_end, if_not_found]
|
|
1266
|
+
* tail used by TEXTBEFORE / TEXTAFTER. Returns the numeric values (with
|
|
1267
|
+
* defaults filled in) or an error if any argument is malformed.
|
|
1268
|
+
*
|
|
1269
|
+
* - match_mode: 0 = case-sensitive (default), 1 = case-insensitive.
|
|
1270
|
+
* - match_end: 0 = don't treat string edge as delimiter (default),
|
|
1271
|
+
* 1 = treat string edge as a virtual delimiter so that
|
|
1272
|
+
* TEXTAFTER with a missing delimiter returns "".
|
|
1273
|
+
*/
|
|
1274
|
+
function parseTextBeforeAfterTail(args) {
|
|
1275
|
+
let inst = 1;
|
|
1276
|
+
if (args.length > 2) {
|
|
1277
|
+
const instRV = toNumberRV(args[2]);
|
|
1278
|
+
if (isError(instRV)) {
|
|
1279
|
+
return instRV;
|
|
1280
|
+
}
|
|
1281
|
+
inst = Math.trunc(instRV.value);
|
|
1282
|
+
}
|
|
1283
|
+
let matchMode = 0;
|
|
1284
|
+
if (args.length > 3) {
|
|
1285
|
+
const mmRV = toNumberRV(args[3]);
|
|
1286
|
+
if (isError(mmRV)) {
|
|
1287
|
+
return mmRV;
|
|
1288
|
+
}
|
|
1289
|
+
const mm = Math.trunc(mmRV.value);
|
|
1290
|
+
if (mm !== 0 && mm !== 1) {
|
|
1291
|
+
return ERRORS.VALUE;
|
|
1292
|
+
}
|
|
1293
|
+
matchMode = mm;
|
|
1294
|
+
}
|
|
1295
|
+
let matchEnd = 0;
|
|
1296
|
+
if (args.length > 4) {
|
|
1297
|
+
const meRV = toNumberRV(args[4]);
|
|
1298
|
+
if (isError(meRV)) {
|
|
1299
|
+
return meRV;
|
|
1300
|
+
}
|
|
1301
|
+
const me = Math.trunc(meRV.value);
|
|
1302
|
+
if (me !== 0 && me !== 1) {
|
|
1303
|
+
return ERRORS.VALUE;
|
|
1304
|
+
}
|
|
1305
|
+
matchEnd = me;
|
|
1306
|
+
}
|
|
1307
|
+
const ifNotFound = args.length > 5 ? args[5] : null;
|
|
1308
|
+
return { inst, matchMode, matchEnd, ifNotFound };
|
|
1309
|
+
}
|
|
1310
|
+
export const fnTEXTBEFORE = args => {
|
|
1311
|
+
const e0 = checkError(args[0]);
|
|
1312
|
+
if (e0) {
|
|
1313
|
+
return e0;
|
|
1314
|
+
}
|
|
1315
|
+
const e1 = checkError(args[1]);
|
|
1316
|
+
if (e1) {
|
|
1317
|
+
return e1;
|
|
1318
|
+
}
|
|
1319
|
+
const text = toStringRV(args[0]);
|
|
1320
|
+
const delimiter = toStringRV(args[1]);
|
|
1321
|
+
const tail = parseTextBeforeAfterTail(args);
|
|
1322
|
+
if ("kind" in tail && tail.kind === RVKind.Error) {
|
|
1323
|
+
return tail;
|
|
1324
|
+
}
|
|
1325
|
+
const { inst, matchMode, matchEnd, ifNotFound } = tail;
|
|
1326
|
+
if (inst === 0) {
|
|
1327
|
+
return ERRORS.VALUE;
|
|
1328
|
+
}
|
|
1329
|
+
if (delimiter === "") {
|
|
1330
|
+
return rvString(inst > 0 ? "" : text);
|
|
1331
|
+
}
|
|
1332
|
+
// For case-insensitive matching we search within the lower-cased
|
|
1333
|
+
// haystack but slice against the original so the returned prefix/
|
|
1334
|
+
// suffix preserves the source text's case.
|
|
1335
|
+
const haystack = matchMode === 1 ? text.toLowerCase() : text;
|
|
1336
|
+
const needle = matchMode === 1 ? delimiter.toLowerCase() : delimiter;
|
|
1337
|
+
const notFound = () => {
|
|
1338
|
+
if (matchEnd === 1 && inst === 1) {
|
|
1339
|
+
// Treat the string end as a virtual delimiter: everything is "before".
|
|
1340
|
+
return rvString(text);
|
|
1341
|
+
}
|
|
1342
|
+
return ifNotFound !== null ? ifNotFound : ERRORS.NA;
|
|
1343
|
+
};
|
|
1344
|
+
if (inst > 0) {
|
|
1345
|
+
let pos = -1;
|
|
1346
|
+
for (let i = 0; i < inst; i++) {
|
|
1347
|
+
pos = haystack.indexOf(needle, pos + 1);
|
|
1348
|
+
if (pos === -1) {
|
|
1349
|
+
return notFound();
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return rvString(text.slice(0, pos));
|
|
1353
|
+
}
|
|
1354
|
+
// Negative: search from end
|
|
1355
|
+
let pos = haystack.length;
|
|
1356
|
+
for (let i = 0; i < -inst; i++) {
|
|
1357
|
+
pos = haystack.lastIndexOf(needle, pos - 1);
|
|
1358
|
+
if (pos === -1) {
|
|
1359
|
+
return notFound();
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
return rvString(text.slice(0, pos));
|
|
1363
|
+
};
|
|
1364
|
+
export const fnTEXTAFTER = args => {
|
|
1365
|
+
const e0 = checkError(args[0]);
|
|
1366
|
+
if (e0) {
|
|
1367
|
+
return e0;
|
|
1368
|
+
}
|
|
1369
|
+
const e1 = checkError(args[1]);
|
|
1370
|
+
if (e1) {
|
|
1371
|
+
return e1;
|
|
1372
|
+
}
|
|
1373
|
+
const text = toStringRV(args[0]);
|
|
1374
|
+
const delimiter = toStringRV(args[1]);
|
|
1375
|
+
const tail = parseTextBeforeAfterTail(args);
|
|
1376
|
+
if ("kind" in tail && tail.kind === RVKind.Error) {
|
|
1377
|
+
return tail;
|
|
1378
|
+
}
|
|
1379
|
+
const { inst, matchMode, matchEnd, ifNotFound } = tail;
|
|
1380
|
+
if (inst === 0) {
|
|
1381
|
+
return ERRORS.VALUE;
|
|
1382
|
+
}
|
|
1383
|
+
if (delimiter === "") {
|
|
1384
|
+
return rvString(inst > 0 ? text : "");
|
|
1385
|
+
}
|
|
1386
|
+
const haystack = matchMode === 1 ? text.toLowerCase() : text;
|
|
1387
|
+
const needle = matchMode === 1 ? delimiter.toLowerCase() : delimiter;
|
|
1388
|
+
const notFound = () => {
|
|
1389
|
+
if (matchEnd === 1 && inst === 1) {
|
|
1390
|
+
// String end is a virtual delimiter → everything after it is "".
|
|
1391
|
+
return rvString("");
|
|
1392
|
+
}
|
|
1393
|
+
return ifNotFound !== null ? ifNotFound : ERRORS.NA;
|
|
1394
|
+
};
|
|
1395
|
+
if (inst > 0) {
|
|
1396
|
+
let pos = -1;
|
|
1397
|
+
for (let i = 0; i < inst; i++) {
|
|
1398
|
+
pos = haystack.indexOf(needle, pos + 1);
|
|
1399
|
+
if (pos === -1) {
|
|
1400
|
+
return notFound();
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return rvString(text.slice(pos + delimiter.length));
|
|
1404
|
+
}
|
|
1405
|
+
let pos = haystack.length;
|
|
1406
|
+
for (let i = 0; i < -inst; i++) {
|
|
1407
|
+
pos = haystack.lastIndexOf(needle, pos - 1);
|
|
1408
|
+
if (pos === -1) {
|
|
1409
|
+
return notFound();
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return rvString(text.slice(pos + delimiter.length));
|
|
1413
|
+
};
|
|
1414
|
+
export const fnTEXTSPLIT = args => {
|
|
1415
|
+
const e0 = checkError(args[0]);
|
|
1416
|
+
if (e0) {
|
|
1417
|
+
return e0;
|
|
1418
|
+
}
|
|
1419
|
+
const text = toStringRV(args[0]);
|
|
1420
|
+
let colDelimiter = "";
|
|
1421
|
+
if (args.length > 1) {
|
|
1422
|
+
const e1 = checkError(args[1]);
|
|
1423
|
+
if (e1) {
|
|
1424
|
+
return e1;
|
|
1425
|
+
}
|
|
1426
|
+
colDelimiter = toStringRV(args[1]);
|
|
1427
|
+
}
|
|
1428
|
+
const rowDelimiter = args.length > 2 && args[2].kind !== RVKind.Blank ? toStringRV(args[2]) : "";
|
|
1429
|
+
// `ignore_empty` (4th arg, default FALSE) — when TRUE, suppress empty
|
|
1430
|
+
// fragments produced by consecutive delimiters.
|
|
1431
|
+
let ignoreEmpty = false;
|
|
1432
|
+
if (args.length > 3 && args[3].kind !== RVKind.Blank) {
|
|
1433
|
+
const ieRV = toBooleanRV(args[3]);
|
|
1434
|
+
if (isError(ieRV)) {
|
|
1435
|
+
return ieRV;
|
|
1436
|
+
}
|
|
1437
|
+
ignoreEmpty = ieRV.value;
|
|
1438
|
+
}
|
|
1439
|
+
// `match_mode` (5th arg, default 0 = case-sensitive). When 1 the
|
|
1440
|
+
// delimiter match is case-insensitive; we implement that by lowercasing
|
|
1441
|
+
// both the haystack and the delimiter(s) before splitting, which is
|
|
1442
|
+
// consistent with Excel's specification for TEXTSPLIT.
|
|
1443
|
+
let matchMode = 0;
|
|
1444
|
+
if (args.length > 4 && args[4].kind !== RVKind.Blank) {
|
|
1445
|
+
const mmRV = toNumberRV(args[4]);
|
|
1446
|
+
if (isError(mmRV)) {
|
|
1447
|
+
return mmRV;
|
|
1448
|
+
}
|
|
1449
|
+
matchMode = Math.trunc(mmRV.value);
|
|
1450
|
+
if (matchMode !== 0 && matchMode !== 1) {
|
|
1451
|
+
return ERRORS.VALUE;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
// `pad_with` (6th arg, default #N/A) — value used to fill shorter rows
|
|
1455
|
+
// when the split produces a ragged 2D shape. Explicit error arguments
|
|
1456
|
+
// (e.g. `TEXTSPLIT(…, #VALUE!)`) propagate into the pad cells verbatim,
|
|
1457
|
+
// matching Excel.
|
|
1458
|
+
const pad = args.length > 5 ? topLeft(args[5]) : ERRORS.NA;
|
|
1459
|
+
const splitString = (s, delim) => {
|
|
1460
|
+
if (!delim) {
|
|
1461
|
+
return [s];
|
|
1462
|
+
}
|
|
1463
|
+
if (matchMode === 1) {
|
|
1464
|
+
// Case-insensitive split — find positions by scanning the lowercased
|
|
1465
|
+
// haystack but slice the original so case is preserved in output.
|
|
1466
|
+
const haystack = s.toLowerCase();
|
|
1467
|
+
const needle = delim.toLowerCase();
|
|
1468
|
+
const parts = [];
|
|
1469
|
+
let last = 0;
|
|
1470
|
+
let i = 0;
|
|
1471
|
+
while (i <= haystack.length - needle.length) {
|
|
1472
|
+
if (haystack.slice(i, i + needle.length) === needle) {
|
|
1473
|
+
parts.push(s.slice(last, i));
|
|
1474
|
+
i += needle.length;
|
|
1475
|
+
last = i;
|
|
1476
|
+
}
|
|
1477
|
+
else {
|
|
1478
|
+
i++;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
parts.push(s.slice(last));
|
|
1482
|
+
return parts;
|
|
1483
|
+
}
|
|
1484
|
+
return s.split(delim);
|
|
1485
|
+
};
|
|
1486
|
+
let rows;
|
|
1487
|
+
if (rowDelimiter) {
|
|
1488
|
+
rows = splitString(text, rowDelimiter);
|
|
1489
|
+
}
|
|
1490
|
+
else {
|
|
1491
|
+
rows = [text];
|
|
1492
|
+
}
|
|
1493
|
+
// Split each row into columns, applying ignore_empty per row after the
|
|
1494
|
+
// split. When ignore_empty is TRUE at the row level we also drop rows
|
|
1495
|
+
// that were themselves empty (i.e. empty string from consecutive row
|
|
1496
|
+
// delimiters).
|
|
1497
|
+
const matrix = [];
|
|
1498
|
+
let maxWidth = 0;
|
|
1499
|
+
for (const row of rows) {
|
|
1500
|
+
if (ignoreEmpty && row === "") {
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
let parts;
|
|
1504
|
+
if (colDelimiter) {
|
|
1505
|
+
parts = splitString(row, colDelimiter);
|
|
1506
|
+
if (ignoreEmpty) {
|
|
1507
|
+
parts = parts.filter(p => p !== "");
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
else {
|
|
1511
|
+
parts = [row];
|
|
1512
|
+
}
|
|
1513
|
+
if (parts.length === 0) {
|
|
1514
|
+
// All fragments were empty and ignore_empty discarded them; keep a
|
|
1515
|
+
// pad row so the result is still a well-formed rectangle.
|
|
1516
|
+
parts = [""];
|
|
1517
|
+
}
|
|
1518
|
+
matrix.push(parts.map(p => rvString(p)));
|
|
1519
|
+
if (parts.length > maxWidth) {
|
|
1520
|
+
maxWidth = parts.length;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
if (matrix.length === 0) {
|
|
1524
|
+
// ignore_empty consumed everything → return a single pad cell so the
|
|
1525
|
+
// array is still a valid 1×1 spill (matches Excel).
|
|
1526
|
+
return rvArray([[pad]]);
|
|
1527
|
+
}
|
|
1528
|
+
// Pad ragged rows out to the maximum width with `pad_with`.
|
|
1529
|
+
const result = [];
|
|
1530
|
+
for (const row of matrix) {
|
|
1531
|
+
if (row.length < maxWidth) {
|
|
1532
|
+
const padded = row.slice();
|
|
1533
|
+
while (padded.length < maxWidth) {
|
|
1534
|
+
padded.push(pad);
|
|
1535
|
+
}
|
|
1536
|
+
result.push(padded);
|
|
1537
|
+
}
|
|
1538
|
+
else {
|
|
1539
|
+
result.push(row);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return rvArray(result);
|
|
1543
|
+
};
|
|
1544
|
+
// ============================================================================
|
|
1545
|
+
// REGEX family (Excel 365, 2024)
|
|
1546
|
+
// ============================================================================
|
|
1547
|
+
/**
|
|
1548
|
+
* Convert an Excel REGEX pattern to a JavaScript RegExp. Excel's regex
|
|
1549
|
+
* dialect is close to PCRE; JavaScript's RegExp is close enough for the
|
|
1550
|
+
* vast majority of practical patterns, but a few constructs (named
|
|
1551
|
+
* captures, look-behind, some Unicode classes) behave slightly
|
|
1552
|
+
* differently. We pass patterns through as-is and let JavaScript's
|
|
1553
|
+
* parser surface #VALUE! on the rare incompatibility.
|
|
1554
|
+
*/
|
|
1555
|
+
function compileExcelRegex(pattern, caseSensitive, global) {
|
|
1556
|
+
try {
|
|
1557
|
+
let flags = "u";
|
|
1558
|
+
if (!caseSensitive) {
|
|
1559
|
+
flags += "i";
|
|
1560
|
+
}
|
|
1561
|
+
if (global) {
|
|
1562
|
+
flags += "g";
|
|
1563
|
+
}
|
|
1564
|
+
return new RegExp(pattern, flags);
|
|
1565
|
+
}
|
|
1566
|
+
catch {
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Resolve the optional `case_sensitivity` argument used by every REGEX
|
|
1572
|
+
* function. `0`/FALSE/omitted → case-insensitive (Excel default),
|
|
1573
|
+
* any other value → case-sensitive. Errors propagate.
|
|
1574
|
+
*/
|
|
1575
|
+
function resolveCaseSensitivity(arg) {
|
|
1576
|
+
if (arg === undefined) {
|
|
1577
|
+
return { caseSensitive: false };
|
|
1578
|
+
}
|
|
1579
|
+
// Accept boolean or number; anything else coerced via toBooleanRV.
|
|
1580
|
+
const b = toBooleanRV(arg);
|
|
1581
|
+
if (isError(b)) {
|
|
1582
|
+
return b;
|
|
1583
|
+
}
|
|
1584
|
+
return { caseSensitive: b.value };
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* REGEXTEST(text, pattern, [case_sensitivity]) — returns TRUE iff the
|
|
1588
|
+
* regex matches any substring of `text`.
|
|
1589
|
+
*/
|
|
1590
|
+
export const fnREGEXTEST = args => {
|
|
1591
|
+
const textV = toStringRV(topLeft(args[0]));
|
|
1592
|
+
const patternV = toStringRV(topLeft(args[1]));
|
|
1593
|
+
const cs = resolveCaseSensitivity(args[2]);
|
|
1594
|
+
if ("kind" in cs) {
|
|
1595
|
+
return cs; // error
|
|
1596
|
+
}
|
|
1597
|
+
const errCheck = checkError(args[0]) ?? checkError(args[1]);
|
|
1598
|
+
if (errCheck) {
|
|
1599
|
+
return errCheck;
|
|
1600
|
+
}
|
|
1601
|
+
const re = compileExcelRegex(patternV, cs.caseSensitive, false);
|
|
1602
|
+
if (!re) {
|
|
1603
|
+
return ERRORS.VALUE;
|
|
1604
|
+
}
|
|
1605
|
+
return rvBoolean(re.test(textV));
|
|
1606
|
+
};
|
|
1607
|
+
/**
|
|
1608
|
+
* REGEXEXTRACT(text, pattern, [return_mode], [case_sensitivity]) —
|
|
1609
|
+
* return_mode = 0 (default) → first match as a string
|
|
1610
|
+
* return_mode = 1 → all matches as a 1-column array
|
|
1611
|
+
* return_mode = 2 → capture groups of the first match as a 1-row array
|
|
1612
|
+
*/
|
|
1613
|
+
export const fnREGEXEXTRACT = args => {
|
|
1614
|
+
const textV = toStringRV(topLeft(args[0]));
|
|
1615
|
+
const patternV = toStringRV(topLeft(args[1]));
|
|
1616
|
+
const errCheck = checkError(args[0]) ?? checkError(args[1]);
|
|
1617
|
+
if (errCheck) {
|
|
1618
|
+
return errCheck;
|
|
1619
|
+
}
|
|
1620
|
+
const modeV = args.length > 2 ? toNumberRV(topLeft(args[2])) : rvNumber(0);
|
|
1621
|
+
if (isError(modeV)) {
|
|
1622
|
+
return modeV;
|
|
1623
|
+
}
|
|
1624
|
+
const mode = Math.trunc(modeV.value);
|
|
1625
|
+
if (mode !== 0 && mode !== 1 && mode !== 2) {
|
|
1626
|
+
return ERRORS.VALUE;
|
|
1627
|
+
}
|
|
1628
|
+
const cs = resolveCaseSensitivity(args[3]);
|
|
1629
|
+
if ("kind" in cs) {
|
|
1630
|
+
return cs;
|
|
1631
|
+
}
|
|
1632
|
+
const needGlobal = mode === 1;
|
|
1633
|
+
const re = compileExcelRegex(patternV, cs.caseSensitive, needGlobal);
|
|
1634
|
+
if (!re) {
|
|
1635
|
+
return ERRORS.VALUE;
|
|
1636
|
+
}
|
|
1637
|
+
if (mode === 0) {
|
|
1638
|
+
const m = re.exec(textV);
|
|
1639
|
+
if (!m) {
|
|
1640
|
+
return ERRORS.NA;
|
|
1641
|
+
}
|
|
1642
|
+
return rvString(m[0]);
|
|
1643
|
+
}
|
|
1644
|
+
if (mode === 1) {
|
|
1645
|
+
const matches = [];
|
|
1646
|
+
let m;
|
|
1647
|
+
// eslint-disable-next-line no-cond-assign
|
|
1648
|
+
while ((m = re.exec(textV)) !== null) {
|
|
1649
|
+
matches.push(m[0]);
|
|
1650
|
+
// Guard against zero-length matches causing an infinite loop.
|
|
1651
|
+
if (m.index === re.lastIndex) {
|
|
1652
|
+
re.lastIndex++;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
if (matches.length === 0) {
|
|
1656
|
+
return ERRORS.NA;
|
|
1657
|
+
}
|
|
1658
|
+
return rvArray(matches.map(s => [rvString(s)]));
|
|
1659
|
+
}
|
|
1660
|
+
// mode === 2 — capture groups of first match as a row array.
|
|
1661
|
+
const m = re.exec(textV);
|
|
1662
|
+
if (!m) {
|
|
1663
|
+
return ERRORS.NA;
|
|
1664
|
+
}
|
|
1665
|
+
// Exclude the full-match element (index 0) — only capture groups.
|
|
1666
|
+
if (m.length <= 1) {
|
|
1667
|
+
// No capture groups defined in the pattern — return the full match.
|
|
1668
|
+
return rvArray([[rvString(m[0])]]);
|
|
1669
|
+
}
|
|
1670
|
+
const row = [];
|
|
1671
|
+
for (let i = 1; i < m.length; i++) {
|
|
1672
|
+
row.push(rvString(m[i] ?? ""));
|
|
1673
|
+
}
|
|
1674
|
+
return rvArray([row]);
|
|
1675
|
+
};
|
|
1676
|
+
/**
|
|
1677
|
+
* REGEXREPLACE(text, pattern, replacement, [occurrence], [case_sensitivity]) —
|
|
1678
|
+
* occurrence = 0 (default) → replace all
|
|
1679
|
+
* occurrence = n (positive) → replace only the n-th match
|
|
1680
|
+
* occurrence = n (negative) → replace only the n-th-last match
|
|
1681
|
+
*/
|
|
1682
|
+
export const fnREGEXREPLACE = args => {
|
|
1683
|
+
const textV = toStringRV(topLeft(args[0]));
|
|
1684
|
+
const patternV = toStringRV(topLeft(args[1]));
|
|
1685
|
+
const replacementV = toStringRV(topLeft(args[2]));
|
|
1686
|
+
const errCheck = checkError(args[0]) ?? checkError(args[1]) ?? checkError(args[2]);
|
|
1687
|
+
if (errCheck) {
|
|
1688
|
+
return errCheck;
|
|
1689
|
+
}
|
|
1690
|
+
const occurrenceV = args.length > 3 ? toNumberRV(topLeft(args[3])) : rvNumber(0);
|
|
1691
|
+
if (isError(occurrenceV)) {
|
|
1692
|
+
return occurrenceV;
|
|
1693
|
+
}
|
|
1694
|
+
const occurrence = Math.trunc(occurrenceV.value);
|
|
1695
|
+
const cs = resolveCaseSensitivity(args[4]);
|
|
1696
|
+
if ("kind" in cs) {
|
|
1697
|
+
return cs;
|
|
1698
|
+
}
|
|
1699
|
+
// Always compile with the global flag — we need to enumerate matches
|
|
1700
|
+
// to apply the occurrence filter; `String.replace` without `/g` would
|
|
1701
|
+
// only see the first match and we wouldn't be able to address later
|
|
1702
|
+
// hits for `occurrence > 1`.
|
|
1703
|
+
const re = compileExcelRegex(patternV, cs.caseSensitive, true);
|
|
1704
|
+
if (!re) {
|
|
1705
|
+
return ERRORS.VALUE;
|
|
1706
|
+
}
|
|
1707
|
+
if (occurrence === 0) {
|
|
1708
|
+
// Replace all.
|
|
1709
|
+
return rvString(textV.replace(re, replacementV));
|
|
1710
|
+
}
|
|
1711
|
+
// Collect every match's range so we can address them by index.
|
|
1712
|
+
const ranges = [];
|
|
1713
|
+
let m;
|
|
1714
|
+
// eslint-disable-next-line no-cond-assign
|
|
1715
|
+
while ((m = re.exec(textV)) !== null) {
|
|
1716
|
+
ranges.push({ start: m.index, end: m.index + m[0].length });
|
|
1717
|
+
if (m.index === re.lastIndex) {
|
|
1718
|
+
re.lastIndex++;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (ranges.length === 0) {
|
|
1722
|
+
return rvString(textV); // no match → unchanged (Excel behavior)
|
|
1723
|
+
}
|
|
1724
|
+
// Negative index counts from the end; -1 is the last match.
|
|
1725
|
+
const idx = occurrence > 0 ? occurrence - 1 : ranges.length + occurrence;
|
|
1726
|
+
if (idx < 0 || idx >= ranges.length) {
|
|
1727
|
+
// Out-of-range occurrence → unchanged (Excel behavior).
|
|
1728
|
+
return rvString(textV);
|
|
1729
|
+
}
|
|
1730
|
+
const { start, end } = ranges[idx];
|
|
1731
|
+
return rvString(textV.slice(0, start) + replacementV + textV.slice(end));
|
|
1732
|
+
};
|
|
1733
|
+
// ============================================================================
|
|
1734
|
+
// VALUETOTEXT / ARRAYTOTEXT (Excel 365)
|
|
1735
|
+
// ============================================================================
|
|
1736
|
+
/**
|
|
1737
|
+
* Format a single scalar for VALUETOTEXT / ARRAYTOTEXT.
|
|
1738
|
+
*
|
|
1739
|
+
* Format 0 (concise, default):
|
|
1740
|
+
* - Number → plain number string
|
|
1741
|
+
* - String → the string itself (no quotes)
|
|
1742
|
+
* - Boolean → "TRUE" / "FALSE"
|
|
1743
|
+
* - Error → error text (e.g. "#N/A")
|
|
1744
|
+
* - Blank → ""
|
|
1745
|
+
*
|
|
1746
|
+
* Format 1 (strict):
|
|
1747
|
+
* - String → wrapped in double quotes with `""` escapes
|
|
1748
|
+
* - Everything else → same as format 0
|
|
1749
|
+
*/
|
|
1750
|
+
function scalarToText(v, strict) {
|
|
1751
|
+
switch (v.kind) {
|
|
1752
|
+
case RVKind.Number:
|
|
1753
|
+
return String(v.value);
|
|
1754
|
+
case RVKind.String:
|
|
1755
|
+
if (strict) {
|
|
1756
|
+
return `"${v.value.replace(/"/g, '""')}"`;
|
|
1757
|
+
}
|
|
1758
|
+
return v.value;
|
|
1759
|
+
case RVKind.Boolean:
|
|
1760
|
+
return v.value ? "TRUE" : "FALSE";
|
|
1761
|
+
case RVKind.Error:
|
|
1762
|
+
return v.code;
|
|
1763
|
+
case RVKind.Blank:
|
|
1764
|
+
return "";
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* VALUETOTEXT(value, [format]) — format a scalar or 1×1 array as text.
|
|
1769
|
+
* For multi-cell arrays, this applies implicit intersection at the
|
|
1770
|
+
* evaluator layer — so by the time we see args[0] it is already scalar.
|
|
1771
|
+
*/
|
|
1772
|
+
export const fnVALUETOTEXT = args => {
|
|
1773
|
+
const formatV = args.length > 1 ? toNumberRV(topLeft(args[1])) : rvNumber(0);
|
|
1774
|
+
if (isError(formatV)) {
|
|
1775
|
+
return formatV;
|
|
1776
|
+
}
|
|
1777
|
+
const fmt = Math.trunc(formatV.value);
|
|
1778
|
+
if (fmt !== 0 && fmt !== 1) {
|
|
1779
|
+
return ERRORS.VALUE;
|
|
1780
|
+
}
|
|
1781
|
+
const strict = fmt === 1;
|
|
1782
|
+
return rvString(scalarToText(topLeft(args[0]), strict));
|
|
1783
|
+
};
|
|
1784
|
+
/**
|
|
1785
|
+
* ARRAYTOTEXT(array, [format]) — flatten an array to a delimited text
|
|
1786
|
+
* representation.
|
|
1787
|
+
*
|
|
1788
|
+
* Format 0 (concise, default): row-major join with ", ".
|
|
1789
|
+
* Format 1 (strict): wraps output in `{…}`, rows separated by `;`,
|
|
1790
|
+
* cells by `,`; strings inside quoted.
|
|
1791
|
+
*/
|
|
1792
|
+
export const fnARRAYTOTEXT = args => {
|
|
1793
|
+
const formatV = args.length > 1 ? toNumberRV(topLeft(args[1])) : rvNumber(0);
|
|
1794
|
+
if (isError(formatV)) {
|
|
1795
|
+
return formatV;
|
|
1796
|
+
}
|
|
1797
|
+
const fmt = Math.trunc(formatV.value);
|
|
1798
|
+
if (fmt !== 0 && fmt !== 1) {
|
|
1799
|
+
return ERRORS.VALUE;
|
|
1800
|
+
}
|
|
1801
|
+
const strict = fmt === 1;
|
|
1802
|
+
const arg = args[0];
|
|
1803
|
+
if (arg.kind !== RVKind.Array) {
|
|
1804
|
+
return rvString(scalarToText(topLeft(arg), strict));
|
|
1805
|
+
}
|
|
1806
|
+
if (!strict) {
|
|
1807
|
+
// Concise: flatten row-major, join with ", ".
|
|
1808
|
+
const parts = [];
|
|
1809
|
+
for (const row of arg.rows) {
|
|
1810
|
+
for (const cell of row) {
|
|
1811
|
+
parts.push(scalarToText(cell, false));
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
return rvString(parts.join(", "));
|
|
1815
|
+
}
|
|
1816
|
+
// Strict: `{row1;row2;...}` with rows as `a,b,c` and strings quoted.
|
|
1817
|
+
const rowStrs = [];
|
|
1818
|
+
for (const row of arg.rows) {
|
|
1819
|
+
const cellStrs = [];
|
|
1820
|
+
for (const cell of row) {
|
|
1821
|
+
cellStrs.push(scalarToText(cell, true));
|
|
1822
|
+
}
|
|
1823
|
+
rowStrs.push(cellStrs.join(","));
|
|
1824
|
+
}
|
|
1825
|
+
return rvString(`{${rowStrs.join(";")}}`);
|
|
1826
|
+
};
|
|
1827
|
+
// ============================================================================
|
|
1828
|
+
// ENCODEURL
|
|
1829
|
+
// ============================================================================
|
|
1830
|
+
/**
|
|
1831
|
+
* ENCODEURL(text) — percent-encode a string for URL use.
|
|
1832
|
+
*
|
|
1833
|
+
* Excel follows RFC 3986's "unreserved" character rule: A-Z, a-z, 0-9,
|
|
1834
|
+
* and `- _ . ~` are kept verbatim; everything else is encoded as
|
|
1835
|
+
* `%HH` using the UTF-8 byte sequence. This is exactly what JavaScript's
|
|
1836
|
+
* `encodeURIComponent` does, so we delegate to it.
|
|
1837
|
+
*/
|
|
1838
|
+
export const fnENCODEURL = args => {
|
|
1839
|
+
const err = checkError(args[0]);
|
|
1840
|
+
if (err) {
|
|
1841
|
+
return err;
|
|
1842
|
+
}
|
|
1843
|
+
const s = toStringRV(topLeft(args[0]));
|
|
1844
|
+
return rvString(encodeURIComponent(s));
|
|
1845
|
+
};
|