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