@pixerate/editor 0.46.0 → 0.47.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/THIRD_PARTY_NOTICES.md +20 -0
- package/dist/agent/index.cjs +20 -19
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +2 -2
- package/dist/agent/index.d.ts +2 -2
- package/dist/agent/index.js +4 -3
- package/dist/agent/index.js.map +1 -1
- package/dist/{builtins-DghNQbT-.d.ts → builtins-D2nMtw2W.d.ts} +1 -1
- package/dist/{builtins-CZW-aYKS.d.cts → builtins-xs7rK6ys.d.cts} +1 -1
- package/dist/chart/index.cjs +3965 -821
- package/dist/chart/index.cjs.map +1 -1
- package/dist/chart/index.d.cts +964 -240
- package/dist/chart/index.d.ts +964 -240
- package/dist/chart/index.js +3922 -778
- package/dist/chart/index.js.map +1 -1
- package/dist/{chunk-PQM5W7ZN.js → chunk-4FX5ACVG.js} +29 -2
- package/dist/{chunk-PQM5W7ZN.js.map → chunk-4FX5ACVG.js.map} +1 -1
- package/dist/{chunk-3RPOW4CS.js → chunk-5VXI5GED.js} +12 -10
- package/dist/{chunk-3RPOW4CS.js.map → chunk-5VXI5GED.js.map} +1 -1
- package/dist/{chunk-HHQ4XJWT.js → chunk-AMWXSYCI.js} +2 -2
- package/dist/{chunk-U7VGDSNY.cjs → chunk-BBRK6MOI.cjs} +101 -99
- package/dist/chunk-BBRK6MOI.cjs.map +1 -0
- package/dist/chunk-C3NWJE3K.js +1182 -0
- package/dist/chunk-C3NWJE3K.js.map +1 -0
- package/dist/{chunk-N4MLNJPO.cjs → chunk-H4PICYNL.cjs} +59 -12
- package/dist/chunk-H4PICYNL.cjs.map +1 -0
- package/dist/chunk-LYGEEDNR.cjs +1895 -0
- package/dist/chunk-LYGEEDNR.cjs.map +1 -0
- package/dist/chunk-NEOO6QJD.cjs +1182 -0
- package/dist/chunk-NEOO6QJD.cjs.map +1 -0
- package/dist/{chunk-Q25ZS4F7.js → chunk-PQFAHT5O.js} +53 -6
- package/dist/chunk-PQFAHT5O.js.map +1 -0
- package/dist/{chunk-ZRNHGAQT.js → chunk-REAGGLXS.js} +43 -1891
- package/dist/chunk-REAGGLXS.js.map +1 -0
- package/dist/{chunk-EFDCLENW.cjs → chunk-RIWPYSKU.cjs} +29 -2
- package/dist/chunk-RIWPYSKU.cjs.map +1 -0
- package/dist/{chunk-EWCOFSCQ.cjs → chunk-SQC26VQV.cjs} +252 -2100
- package/dist/chunk-SQC26VQV.cjs.map +1 -0
- package/dist/{chunk-K2LVZ4FA.cjs → chunk-XFYUFVAX.cjs} +3 -3
- package/dist/{chunk-K2LVZ4FA.cjs.map → chunk-XFYUFVAX.cjs.map} +1 -1
- package/dist/chunk-ZFBBVGCI.js +1895 -0
- package/dist/chunk-ZFBBVGCI.js.map +1 -0
- package/dist/commands/index.cjs +93 -11
- package/dist/commands/index.cjs.map +1 -1
- package/dist/commands/index.d.cts +9 -7
- package/dist/commands/index.d.ts +9 -7
- package/dist/commands/index.js +92 -10
- package/dist/commands/index.js.map +1 -1
- package/dist/extensions/index.cjs +3 -2
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +3 -2
- package/dist/extensions/index.d.ts +3 -2
- package/dist/extensions/index.js +2 -1
- package/dist/generation/index.cjs +2 -2
- package/dist/generation/index.js +1 -1
- package/dist/{index-jwJ_onWf.d.ts → index-CSUCQYAH.d.ts} +23 -2
- package/dist/{index-CfOQf70b.d.cts → index-QrqMmZ75.d.cts} +23 -2
- package/dist/index.cjs +12 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +26 -23
- package/dist/index.js.map +1 -1
- package/dist/parameters/index.d.cts +1 -1
- package/dist/parameters/index.d.ts +1 -1
- package/dist/{registry-vz6HOiiy.d.cts → registry-CaR7LC6W.d.cts} +2 -0
- package/dist/{registry-BfVD-vVg.d.ts → registry-rVtFJjRM.d.ts} +2 -0
- package/dist/spreadsheet/index.cjs +5 -3
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.js +24 -22
- package/dist/tools/index.cjs +3 -3
- package/dist/tools/index.js +2 -2
- package/dist/types-BVqWm3d1.d.cts +320 -0
- package/dist/types-BVqWm3d1.d.ts +320 -0
- package/dist/vector/index.d.cts +1 -1
- package/dist/vector/index.d.ts +1 -1
- package/package.json +3 -2
- package/dist/chunk-EFDCLENW.cjs.map +0 -1
- package/dist/chunk-EWCOFSCQ.cjs.map +0 -1
- package/dist/chunk-N4MLNJPO.cjs.map +0 -1
- package/dist/chunk-Q25ZS4F7.js.map +0 -1
- package/dist/chunk-U7VGDSNY.cjs.map +0 -1
- package/dist/chunk-ZRNHGAQT.js.map +0 -1
- /package/dist/{chunk-HHQ4XJWT.js.map → chunk-AMWXSYCI.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-LYGEEDNR.cjs","../src/spreadsheet/formula/values.ts","../src/spreadsheet/formula/parse-input.ts","../src/spreadsheet/numfmt/decimal.ts","../src/spreadsheet/numfmt/builtin.ts","../src/spreadsheet/numfmt/parse.ts","../src/spreadsheet/numfmt/render.ts","../src/spreadsheet/numfmt/index.ts"],"names":["pad","y","m","out","UNIX_1899_12_30","isDigitTok","serial"],"mappings":"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;AACA,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAMgB;AACR,EAAA;AACD,EAAA;AACD,EAAA;AACI,EAAA;AACF,EAAA;AACC,EAAA;AACT;AAEM;AAEG;AACF,EAAA;AACD,EAAA;AACA,EAAA;AACG,EAAA;AACT;AAEMA;AAGG;AACH,EAAA;AACE,EAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACG,EAAA;AACT;AAES;AACH,EAAA;AAEE,EAAA;AACE,IAAA;AACD,IAAA;AACL,IAAA;AACF,EAAA;AACM,EAAA;AACF,EAAA;AACE,EAAA;AACF,EAAA;AACG,EAAA;AACT;AAEM;AAGG;AACC,EAAA;AACJ,EAAA;AACA,EAAA;AACA,EAAA;AACE,EAAA;AACE,IAAA;AACA,IAAA;AACR,EAAA;AACQ,IAAA;AACR,EAAA;AACM,EAAA;AACJ,IAAA;AACM,IAAA;AACD,IAAA;AACH,MAAA;AACI,MAAA;AACN,IAAA;AACF,EAAA;AACM,EAAA;AACJ,IAAA;AACM,IAAA;AACR,EAAA;AACI,EAAA;AACE,EAAA;AACF,EAAA;AACA,EAAA;AAEI,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,IAAA;AACJ,IAAA;AACK,EAAA;AACL,IAAA;AACF,EAAA;AACK,EAAA;AAGA,EAAA;AACG,EAAA;AACH,EAAA;AACC,EAAA;AACF,EAAA;AACK,EAAA;AACH,EAAA;AACA,EAAA;AASC,EAAA;AACT;AAEM;AAIG;AACD,EAAA;AACF,EAAA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF,EAAA;AACE,EAAA;AACA,EAAA;AACF,EAAA;AACE,EAAA;AACC,EAAA;AACT;AAES;AACD,EAAA;AACA,EAAA;AACA,EAAA;AACC,EAAA;AACT;AAGS;AACD,EAAA;AACF,EAAA;AACA,EAAA;AACJ,EAAA;AACM,IAAA;AACF,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AACM,EAAA;AACF,EAAA;AACA,EAAA;AACE,EAAA;AACE,EAAA;AACF,EAAA;AACA,EAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACN,EAAA;AACE,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAEM;AAEG;AAEH,EAAA;AACA,EAAA;AACG,EAAA;AACT;AAES;AACD,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF,EAAA;AACI,IAAA;AAEA,IAAA;AACJ,MAAA;AACIC,MAAAA;AACN,IAAA;AACA,IAAA;AACE,MAAA;AACIC,MAAAA;AACN,IAAA;AAEM,IAAA;AACF,IAAA;AACN,EAAA;AACI,EAAA;AACI,IAAA;AACA,IAAA;AACF,IAAA;AAEE,MAAA;AAEA,MAAA;AACN,IAAA;AACM,IAAA;AACA,IAAA;AACF,IAAA;AACE,IAAA;AACA,IAAA;AACF,IAAA;AACN,EAAA;AAEM,EAAA;AACF,EAAA;AACI,IAAA;AACD,IAAA;AACD,IAAA;AACA,IAAA;AACA,IAAA;AACN,EAAA;AACO,EAAA;AACT;AAES;AACD,EAAA;AACF,EAAA;AACI,IAAA;AAMN,IAAA;AACF,EAAA;AACM,EAAA;AACF,EAAA;AAEJ,EAAA;AACQ,IAAA;AACA,IAAA;AAEF,IAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AFqHU;AACA;AGpfG;AAMJ;AACD,EAAA;AACA,EAAA;AACA,EAAA;AACN,EAAA;AACQ,IAAA;AACF,IAAA;AACN,EAAA;AACO,EAAA;AACT;AAGS;AACD,EAAA;AACD,EAAA;AACC,EAAA;AACA,EAAA;AACA,EAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACF,IAAA;AACK,EAAA;AACL,IAAA;AACA,IAAA;AACF,EAAA;AACM,EAAA;AACC,EAAA;AACT;AAGS;AACD,EAAA;AAEC,EAAA;AACT;AAUgB;AACT,EAAA;AACG,EAAA;AACF,EAAA;AACC,EAAA;AACD,EAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AAEE,IAAA;AACN,IAAA;AACA,IAAA;AACF,EAAA;AACQ,EAAA;AACJ,EAAA;AACJ,EAAA;AACM,IAAA;AACF,MAAA;AACF,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AACI,EAAA;AACF,IAAA;AACA,IAAA;AACK,IAAA;AACP,EAAA;AACO,EAAA;AACT;AAOgB;AACR,EAAA;AACA,EAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACN,EAAA;AACM,IAAA;AACJ,IAAA;AACK,EAAA;AACD,IAAA;AACC,IAAA;AACC,MAAA;AACJ,MAAA;AACE,QAAA;AACE,UAAA;AACF,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACF,MAAA;AACI,MAAA;AACN,IAAA;AACF,EAAA;AACI,EAAA;AACG,EAAA;AACD,EAAA;AACA,EAAA;AACF,EAAA;AACA,EAAA;AACE,EAAA;AACE,IAAA;AACN,IAAA;AACK,EAAA;AACC,IAAA;AACA,IAAA;AACN,IAAA;AACF,EAAA;AACI,EAAA;AACI,EAAA;AACV;AAGgB;AACT,EAAA;AACG,EAAA;AACD,EAAA;AACT;AAEM;AAOU;AACT,EAAA;AACD,EAAA;AACE,EAAA;AACE,EAAA;AACF,EAAA;AAEF,EAAA;AACI,IAAA;AACF,IAAA;AACJ,IAAA;AACE,MAAA;AACA,MAAA;AACI,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACJ,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACC,IAAA;AACD,IAAA;AACF,MAAA;AACC,MAAA;AACH,IAAA;AACK,IAAA;AACD,IAAA;AACA,IAAA;AACC,IAAA;AACD,IAAA;AACN,EAAA;AACO,EAAA;AACT;AHkdU;AACA;AIhpBJ;AACA,EAAA;AACG,EAAA;AACH,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACI,EAAA;AACH,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACG,EAAA;AACV;AAMgB;AACP,EAAA;AACT;AAGgB;AACR,EAAA;AACF,EAAA;AACE,EAAA;AACA,EAAA;AACC,EAAA;AACT;AJ2oBU;AACA;AKrrBJ;AAQO;AAsBG;AACN,EAAA;AACD,IAAA;AACA,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACF,IAAA;AACE,MAAA;AACJ,EAAA;AACF;AASgB;AACN,EAAA;AACD,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACJ,EAAA;AACF;AAqBM;AAQU;AACR,EAAA;AACA,EAAA;AACA,EAAA;AACF,EAAA;AACA,EAAA;AACI,EAAA;AACD,EAAA;AACC,IAAA;AACD,IAAA;AACC,IAAA;AACF,IAAA;AACE,MAAA;AACJ,MAAA;AACA,MAAA;AACF,IAAA;AACI,IAAA;AACF,MAAA;AACI,MAAA;AACN,IAAA;AACE,MAAA;AACI,MAAA;AACN,IAAA;AAEM,MAAA;AACJ,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACI,MAAA;AACN,IAAA;AACM,MAAA;AACA,MAAA;AACN,IAAA;AACM,MAAA;AACN,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAEM;AAEG;AACP,EAAA;AACQ,IAAA;AACF,IAAA;AACN,EAAA;AACO,EAAA;AACT;AAES;AACH,EAAA;AACE,EAAA;AACF,EAAA;AACC,EAAA;AACP;AAEM;AACA;AAEA;AAEG;AACH,EAAA;AACI,EAAA;AACF,EAAA;AACD,E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AA;AACF,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAES;AACF,EAAA;AACC,EAAA;AACE,EAAA;AACR,EAAA;AACM,IAAA;AACC,IAAA;AACP,EAAA;AACO,EAAA;AACT;AAGS;AACH,EAAA;AACJ,EAAA;AACM,IAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AACK,EAAA;AACG,IAAA;AACN,IAAA;AACD,EAAA;AACH;AAES;AACD,EAAA;AACN,EAAA;AACO,EAAA;AACT;AAGgB;AACN,EAAA;AAEA,EAAA;AACA,EAAA;AACH,EAAA;AACC,EAAA;AACA,EAAA;AAGA,EAAA;AACA,EAAA;AACA,EAAA;AACF,EAAA;AACI,EAAA;AACN,IAAA;AACK,EAAA;AACC,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACF,MAAA;AACA,MAAA;AACI,MAAA;AACA,MAAA;AAEF,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACE,QAAA;AACA,QAAA;AACC,QAAA;AACD,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACF,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACI,MAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AAGI,MAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACE,QAAA;AACF,MAAA;AACF,IAAA;AACG,MAAA;AACH,IAAA;AACA,IAAA;AACA,IAAA;AACM,IAAA;AACF,IAAA;AACJ,IAAA;AACI,IAAA;AACN,EAAA;AACM,EAAA;AACN,EAAA;AACM,IAAA;AACE,IAAA;AACA,IAAA;AAAoC,IAAA;AACF,IAAA;AACA,IAAA;AAEnC,IAAA;AACP,EAAA;AACI,EAAA;AACG,EAAA;AACT;AAGgB;AACV,EAAA;AACA,EAAA;AACC,EAAA;AACG,EAAA;AACR,EAAA;AACQ,IAAA;AACF,IAAA;AACE,IAAA;AACF,IAAA;AACE,IAAA;AACD,IAAA;AACC,IAAA;AAC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","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-LYGEEDNR.cjs","sourcesContent":[null,"// Portions ported from gridcraft/crates/core/src/value.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/**\n * Value helpers shared by the formula evaluator and the built-in functions:\n * error values, spreadsheet coercion rules, comparisons, ranges and dates.\n */\nimport { parseNumberText } from './parse-input';\n\n/** Spreadsheet error values. They propagate through operators and functions. */\nexport const FORMULA_ERRORS = new Set([\n '#DIV/0!',\n '#VALUE!',\n '#REF!',\n '#NAME?',\n '#N/A',\n '#NUM!',\n '#CYCLE!',\n '#ERROR!',\n '#SPILL!',\n '#CALC!'\n]);\n\nexport type FormulaErrorCode =\n | '#DIV/0!'\n | '#VALUE!'\n | '#REF!'\n | '#NAME?'\n | '#N/A'\n | '#NUM!'\n | '#CYCLE!'\n | '#ERROR!'\n | '#SPILL!'\n | '#CALC!';\n\nexport function isFormulaError(value: unknown): value is string {\n return typeof value === 'string' && FORMULA_ERRORS.has(value);\n}\n\nexport const isBlank = (value: unknown): boolean => value === '' || value === null || value === undefined;\n\n/**\n * Coerces to a number like arithmetic does: blanks are 0, booleans 1/0,\n * numeric text parses (`\"1,000\"`, `\"50%\"`, `\"$3\"`, `\"(5)\"`, dates and times\n * as serial numbers), other text is #VALUE!.\n */\nexport function toNumber(value: unknown): number | string {\n if (isFormulaError(value)) return value;\n if (isBlank(value)) return 0;\n if (typeof value === 'boolean') return value ? 1 : 0;\n if (typeof value === 'number') return value;\n return parseNumberText(String(value)) ?? '#VALUE!';\n}\n\n/** Coerces to a boolean: numbers are non-zero, \"TRUE\"/\"FALSE\" text is parsed, other text is #VALUE!. */\nexport function toBoolean(value: unknown): boolean | string {\n if (isFormulaError(value)) return value;\n if (isBlank(value)) return false;\n if (typeof value === 'boolean') return value;\n if (typeof value === 'number') return value !== 0;\n const upper = String(value).trim().toUpperCase();\n if (upper === 'TRUE') return true;\n if (upper === 'FALSE') return false;\n return '#VALUE!';\n}\n\n/** Coerces to text like `&` does: numbers use up to 15 significant digits (`0.1+0.2` is \"0.3\"). */\nexport const toText = (value: unknown): string =>\n isBlank(value)\n ? ''\n : typeof value === 'boolean'\n ? (value ? 'TRUE' : 'FALSE')\n : typeof value === 'number'\n ? numberToText(value)\n : String(value);\n\n/**\n * Number to text as Excel converts it in `&` and General display: up to 15\n * significant digits, scientific for very large or very small magnitudes.\n */\nexport function numberToText(n: number): string {\n if (n === 0 || !Number.isFinite(n)) return '0';\n const a = Math.abs(n);\n if (((a < 1e-9 || a >= 1e15) && Math.round(a) !== a) || a >= 1e15) return sciText(n, 15);\n let mag = Math.floor(Math.log10(a));\n if (a >= Math.pow(10, mag + 1)) mag++; // log10 can land just below an exact power of ten\n const decimals = Math.min(Math.max(14 - mag, 0), 30);\n return trimDecimal(n.toFixed(decimals));\n}\n\n/** Removes trailing zeros (and a trailing point) from a decimal string. */\nfunction trimDecimal(s: string): string {\n if (!s.includes('.')) return s;\n const t = s.replace(/0+$/, '').replace(/\\.$/, '');\n return t === '-0' ? '0' : t;\n}\n\n/** Scientific text like `1.23456789012346E+15` with up to `sig` significant digits. */\nfunction sciText(n: number, sig: number): string {\n const [mantissa, exp] = n.toExponential(Math.max(sig - 1, 0)).split('e');\n const e = Number(exp);\n return `${trimDecimal(mantissa)}E${e < 0 ? '-' : '+'}${String(Math.abs(e)).padStart(2, '0')}`;\n}\n\n/**\n * Number held by a value, or null. Typed cell values are stored as text, so\n * numeric text (\"120\") counts as a number here; booleans and blanks do not.\n */\nexport function numericValue(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n if (typeof value !== 'string' || FORMULA_ERRORS.has(value)) return null;\n const trimmed = value.trim();\n if (trimmed === '') return null;\n const num = Number(trimmed);\n return Number.isFinite(num) ? num : null;\n}\n\n/** Normalizes a cell value for lookups and criteria: numeric text becomes a number. */\nexport function normalizeValue(value: unknown): unknown {\n const num = typeof value === 'string' ? numericValue(value) : null;\n return num === null ? value : num;\n}\n\n/** `ERROR.TYPE`-style order of error values, used when two errors are compared. */\nconst ERROR_ORDER = ['#NULL!', '#DIV/0!', '#VALUE!', '#REF!', '#NAME?', '#NUM!', '#N/A', '#SPILL!', '#CALC!', '#CYCLE!', '#ERROR!'];\n\n/** Type rank for comparison and sorting: numbers < text < booleans < errors; blanks rank last. */\nfunction valueRank(value: unknown): number {\n if (isBlank(value)) return 4;\n if (typeof value === 'number') return 0;\n if (typeof value === 'boolean') return 2;\n if (typeof value === 'string' && FORMULA_ERRORS.has(value)) return 3;\n return 1;\n}\n\n/**\n * Compares numbers the way Excel does: both are rounded to 15 significant\n * digits first, so binary noise such as `0.1+0.2` versus `0.3` counts as equal.\n */\nexport function compareNumbers(a: number, b: number): number {\n if (a === b) return 0;\n // Numbers further apart than the 15th digit can't round together, and\n // rounding keeps their order: skip the rounding on the sort hot path.\n if (Math.abs(a - b) > Math.max(Math.abs(a), Math.abs(b)) * 1e-13) return a < b ? -1 : 1;\n const round15 = (x: number) => (!Number.isFinite(x) || x === 0 ? x : Number(x.toExponential(14)));\n const x = round15(a);\n const y = round15(b);\n return x < y ? -1 : x > y ? 1 : 0;\n}\n\n/** Case-insensitive text order. */\nexport function compareText(a: string, b: string): number {\n const x = a.toLowerCase();\n const y = b.toLowerCase();\n return x < y ? -1 : x > y ? 1 : 0;\n}\n\n/**\n * Excel's comparison for the `=`, `<>`, `<`… operators: numbers < text <\n * booleans < errors, numbers compared at 15 significant digits, text\n * case-insensitively. A blank acts as 0, \"\" or FALSE depending on the other\n * side. Dates are ISO text: two ISO dates compare chronologically\n * (\"2026-1-5\" is before \"2026-01-10\"), and an ISO date against a number\n * compares as its serial number.\n */\nexport function compareValues(left: unknown, right: unknown): number {\n const lRank = valueRank(left);\n const rRank = valueRank(right);\n if (lRank === 4 || rRank === 4) {\n if (lRank === rRank) return 0;\n // A blank against a number, text or boolean takes that type's empty value.\n const blankFirst = lRank === 4;\n const other = blankFirst ? right : left;\n const otherRank = blankFirst ? rRank : lRank;\n let cmp: number;\n if (otherRank === 0) cmp = compareNumbers(0, other as number);\n else if (otherRank === 1) cmp = String(other) === '' ? 0 : -1;\n else if (otherRank === 2) cmp = other ? -1 : 0;\n else return lRank - rRank;\n return blankFirst ? cmp : -cmp;\n }\n if (lRank === 1 && rRank === 1) {\n const a = String(left);\n const b = String(right);\n const ta = isoDateTime(a);\n const tb = ta === null ? null : isoDateTime(b);\n if (ta !== null && tb !== null) return Math.sign(ta - tb);\n return compareText(a, b);\n }\n if ((lRank === 0 && rRank === 1) || (lRank === 1 && rRank === 0)) {\n const date = parseIsoDate(lRank === 1 ? left : right);\n if (date) {\n const serial = dateToSerial(date);\n return lRank === 0 ? compareNumbers(left as number, serial) : compareNumbers(serial, right as number);\n }\n }\n if (lRank !== rRank) return lRank - rRank;\n if (lRank === 0) return compareNumbers(left as number, right as number);\n if (lRank === 2) return Number(left) - Number(right);\n return Math.sign(ERROR_ORDER.indexOf(left as string) - ERROR_ORDER.indexOf(right as string));\n}\n\n/**\n * Sort order (ascending): numbers, text, booleans, errors, then blanks last.\n * Errors are equal to each other, so sorting keeps their order.\n */\nexport function sortCompareValues(left: unknown, right: unknown): number {\n const lBlank = isBlank(left);\n const rBlank = isBlank(right);\n if (lBlank || rBlank) return lBlank === rBlank ? 0 : lBlank ? 1 : -1;\n if (isFormulaError(left) && isFormulaError(right)) return 0;\n return compareValues(left, right);\n}\n\n/** Rounds half away from zero, compensating for binary floating point (ROUND(1.005, 2) = 1.01). */\nexport function roundHalfAwayFromZero(value: number, decimals: number): number {\n const factor = Math.pow(10, decimals);\n const scaled = Number((Math.abs(value) * factor).toPrecision(15));\n return (Math.sign(value) * Math.round(scaled)) / factor;\n}\n\n/** Strips binary floating point noise (0.1 * 3 = 0.30000000000000004 -> 0.3). */\nexport const cleanFloat = (value: number): number => Number(value.toPrecision(15));\n\nexport function colNameToIndex(col: string): number {\n let index = 0;\n const upper = col.toUpperCase();\n for (let i = 0; i < upper.length; i++) {\n index = index * 26 + (upper.charCodeAt(i) - 65 + 1);\n }\n return index - 1;\n}\n\nexport function indexToColName(index: number): string {\n let num = index + 1;\n let name = '';\n while (num > 0) {\n const mod = (num - 1) % 26;\n name = String.fromCharCode(65 + mod) + name;\n num = Math.floor((num - mod) / 26);\n }\n return name;\n}\n\n// --- Ranges ---\n\n/**\n * A rectangular block of values. `values` is column-major (the order in which\n * `EvaluationContext.resolveRange` returns cells): `values[col * rows + row]`.\n */\nexport interface FormulaRange {\n rows: number;\n cols: number;\n values: any[];\n}\n\n/** Shapes of the arrays produced by range references and range-returning functions. */\nconst rangeShapes = new WeakMap<any[], { rows: number; cols: number }>();\n\n/** Records the shape of a column-major array so functions can treat it as a 2D range. */\nexport function withRangeShape<T extends any[]>(values: T, rows: number, cols: number): T {\n rangeShapes.set(values, { rows, cols });\n return values;\n}\n\n/** Views an array as a range. Arrays without a known shape are a single column. */\nexport function toFormulaRange(values: any[]): FormulaRange {\n const shape = rangeShapes.get(values);\n if (shape && shape.rows * shape.cols === values.length) return { ...shape, values };\n return { rows: values.length, cols: 1, values };\n}\n\nexport const rangeGet = (range: FormulaRange, row: number, col: number): any =>\n row < range.rows && col < range.cols ? range.values[col * range.rows + row] : '';\n\n/** Builds a column-major array (with its shape recorded) from a sub-block of a range. */\nexport function sliceRange(\n range: FormulaRange,\n rowStart: number,\n rowCount: number,\n colStart: number,\n colCount: number,\n): any[] {\n const out: any[] = [];\n for (let c = colStart; c < colStart + colCount; c++) {\n for (let r = rowStart; r < rowStart + rowCount; r++) out.push(rangeGet(range, r, c));\n }\n return withRangeShape(out, rowCount, colCount);\n}\n\n// --- Dates ---\n//\n// Dates are ISO text (\"2026-01-31\", or \"2026-01-31 13:45:00\" with a time), as\n// returned by TODAY(). Numbers are read as spreadsheet serial dates (days since\n// 1899-12-30), so values pasted from Excel or Numbers still work.\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\nconst pad = (n: number, width = 2) => String(n).padStart(width, '0');\n\n/** Parses a date value (ISO text, serial number or Date) into a local Date, or null. */\nexport function toDate(value: unknown): Date | null {\n if (value instanceof Date) return isNaN(value.getTime()) ? null : value;\n if (typeof value === 'number') return serialToDate(value);\n if (typeof value !== 'string' || FORMULA_ERRORS.has(value)) return null;\n const text = value.trim();\n if (text === '') return null;\n const iso = /^(\\d{4})-(\\d{1,2})-(\\d{1,2})(?:[ T](\\d{1,2}):(\\d{2})(?::(\\d{2}))?)?$/.exec(text);\n if (iso) {\n const [, y, m, d, hh = '0', mm = '0', ss = '0'] = iso;\n const date = new Date(Number(y), Number(m) - 1, Number(d), Number(hh), Number(mm), Number(ss));\n // Reject impossible dates such as 2026-02-30 instead of rolling them over.\n return date.getMonth() === Number(m) - 1 ? date : null;\n }\n const serial = numericValue(text);\n if (serial !== null) return serialToDate(serial);\n // Other textual dates (\"Jan 5, 2026\", \"1/5/2026\") use the platform parser.\n if (/[a-z/]/i.test(text)) {\n const parsed = Date.parse(text);\n return isNaN(parsed) ? null : new Date(parsed);\n }\n return null;\n}\n\nconst ISO_DATE = /^(\\d{4})-(\\d{1,2})-(\\d{1,2})(?:[ T](\\d{1,2}):(\\d{2})(?::(\\d{2}))?)?$/;\n\n/**\n * Parses ISO date text (\"2026-01-31\" or \"2026-01-31 13:45[:00]\") into a\n * local Date, or null. Only ISO text counts: date arithmetic and date\n * comparisons never reinterpret other text or numbers as dates.\n */\nexport function parseIsoDate(value: unknown): Date | null {\n if (typeof value !== 'string' || value.length < 8 || value.length > 19) return null;\n const iso = ISO_DATE.exec(value.trim());\n if (!iso) return null;\n const [, y, m, d, hh = '0', mm = '0', ss = '0'] = iso;\n const date = new Date(Number(y), Number(m) - 1, Number(d), Number(hh), Number(mm), Number(ss));\n return date.getMonth() === Number(m) - 1 && date.getDate() === Number(d) ? date : null;\n}\n\n/** Whether ISO date text includes a time of day. */\nexport const isoDateHasTime = (value: string): boolean => /[ T]\\d/.test(value.trim());\n\nfunction isoDateTime(value: string): number | null {\n // Cheap pre-check: ISO dates start with four digits and a dash.\n if (value.length < 8 || value.charCodeAt(4) !== 45 /* - */) return null;\n return parseIsoDate(value)?.getTime() ?? null;\n}\n\n/** Spreadsheet serial number (days since 1899-12-30, fraction = time of day). */\nexport function dateToSerial(date: Date): number {\n const utc = Date.UTC(\n date.getFullYear(),\n date.getMonth(),\n date.getDate(),\n date.getHours(),\n date.getMinutes(),\n date.getSeconds(),\n );\n return cleanFloat((utc - Date.UTC(1899, 11, 30)) / MS_PER_DAY);\n}\n\nexport function serialToDate(serial: number): Date | null {\n if (!Number.isFinite(serial)) return null;\n const utc = new Date(Date.UTC(1899, 11, 30) + Math.round(serial * MS_PER_DAY));\n return new Date(\n utc.getUTCFullYear(),\n utc.getUTCMonth(),\n utc.getUTCDate(),\n utc.getUTCHours(),\n utc.getUTCMinutes(),\n utc.getUTCSeconds(),\n );\n}\n\n/** Formats a local date as \"YYYY-MM-DD\" (the format TODAY() returns). */\nexport const formatIsoDate = (date: Date): string =>\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n\n/** Formats a local date and time as \"YYYY-MM-DD HH:MM:SS\" (the format NOW() returns). */\nexport const formatIsoDateTime = (date: Date): string =>\n `${formatIsoDate(date)} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;\n\n/** Whole days from `start` to `end`, ignoring time of day and DST shifts. */\nexport const daysBetween = (start: Date, end: Date): number =>\n Math.round(\n (Date.UTC(end.getFullYear(), end.getMonth(), end.getDate()) -\n Date.UTC(start.getFullYear(), start.getMonth(), start.getDate())) /\n MS_PER_DAY,\n );\n","// Portions ported from gridcraft/crates/core/src/parse.rs and gridcraft/crates/core/src/date.rs\n// (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/**\n * Parsing what people type into a cell (and numeric text in formulas).\n *\n * Recognises numbers with thousands separators, percentages, currency,\n * scientific notation, fractions (`1 1/2`), dates (`3/4/2026`, `2026-03-04`,\n * `4-Mar-2026`, `Mar 4, 2026`), times (`10:30`, `10:30:15 PM`) and\n * date-times. Locale: en-US (month/day/year).\n *\n * Representation: our engine keeps dates as ISO text (`2026-03-04`, or\n * `2026-03-04 18:00:00` with a time), so a typed date becomes ISO text, not a\n * serial number. A time typed on its own (`12:00`) has no date to anchor ISO\n * text to, so it becomes a fraction of a day (0.5), as in Excel.\n */\nimport type { CellFormat } from '../types';\nimport { FORMULA_ERRORS } from './values';\n\n/** Kind of automatic number format chosen for typed input. */\nexport type CellInputFormatKind = 'number' | 'currency' | 'percentage' | 'date' | 'time';\n\nexport interface ParsedCellInput {\n /** The typed value: number, boolean, ISO date text, error code or text. `''` for empty input. */\n value: string | number | boolean;\n type: 'empty' | 'text' | 'number' | 'boolean' | 'error' | 'date';\n /**\n * Number format code Excel would apply automatically (e.g. `\"$\"#,##0.00`,\n * `0%`, `#,##0`, `m/d/yyyy`, `h:mm`), or undefined for General.\n */\n numberFormat?: string;\n /** What kind of format that is, if any. */\n formatKind?: CellInputFormatKind;\n /** `formatKind` mapped onto `CellFormat` (times have no `CellFormat` and map to undefined). */\n cellFormat?: CellFormat;\n}\n\n// --- Date helpers (from date.rs) ---\n\nexport const MONTH_NAMES = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n];\n\n/** Days since 1970-01-01 for a civil date (Howard Hinnant's algorithm). */\nexport function daysFromCivil(year: number, month: number, day: number): number {\n const y = month <= 2 ? year - 1 : year;\n const era = Math.floor((y >= 0 ? y : y - 399) / 400);\n const yoe = y - era * 400;\n const doy = Math.floor((153 * (month > 2 ? month - 3 : month + 9) + 2) / 5) + day - 1;\n const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;\n return era * 146097 + doe - 719468;\n}\n\nexport const isLeapYear = (y: number): boolean => (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;\n\nexport function daysInMonth(year: number, month: number): number {\n if (month === 2) return isLeapYear(year) ? 29 : 28;\n if ([4, 6, 9, 11].includes(month)) return 30;\n return month >= 1 && month <= 12 ? 31 : 0;\n}\n\n/**\n * Days in a month of Excel's 1900 date system: February 1900 has 29 days\n * (serial 60 is the fictitious 1900-02-29, kept for Lotus 1-2-3 compatibility).\n */\nexport const daysInMonth1900 = (year: number, month: number): number =>\n year === 1900 && month === 2 ? 29 : daysInMonth(year, month);\n\nconst UNIX_1899_12_30 = -25569;\nconst MAX_SERIAL_1900 = 2_958_465; // 9999-12-31\n\n/**\n * Excel 1900-system serial for a date (1 = 1900-01-01, 60 = the fictitious\n * 1900-02-29). Month and day overflow roll over like `DATE()`; years 0–1899\n * are added to 1900. Null before the epoch or after 9999-12-31.\n */\nexport function serialFromYmd(year: number, month: number, day: number): number | null {\n if (![year, month, day].every(Number.isSafeInteger)) return null;\n let y = year >= 0 && year < 1900 ? year + 1900 : year;\n const m0 = month - 1;\n y += Math.floor(m0 / 12);\n const m = (((m0 % 12) + 12) % 12) + 1;\n if (y < 1900 || y > 9999) return null;\n // Up to February 1900 the serial is one lower than the day count from 1899-12-30.\n const first = daysFromCivil(y, m, 1) - UNIX_1899_12_30 - (y === 1900 && m <= 2 ? 1 : 0);\n const serial = first + day - 1;\n return serial >= 0 && serial <= MAX_SERIAL_1900 ? serial : null;\n}\n\n/** Fraction of a day for a time. */\nexport const timeFraction = (h: number, m: number, s: number): number => (h * 3600 + m * 60 + s) / 86400;\n\n/** The year used for dates typed without one. */\nconst currentYear = (): number => new Date().getFullYear();\n\n// --- Parsing (from parse.rs) ---\n\ninterface DateParts {\n year: number;\n month: number;\n day: number;\n /** Fraction of a day, when a time was typed. */\n time?: number;\n}\n\ntype Numeric =\n | { kind: 'number'; value: number; format?: string }\n | { kind: 'date'; date: DateParts; format: string }\n | { kind: 'time'; value: number; format: string };\n\n/**\n * Interprets typed cell input (not formulas). Text that isn't a number,\n * date, time, boolean or error stays text; a leading apostrophe forces text.\n */\nexport function parseCellInput(input: string): ParsedCellInput {\n if (input === '') return { value: '', type: 'empty' };\n if (input.startsWith(\"'\")) return { value: input.slice(1), type: 'text' };\n const t = input.trim();\n const upper = t.toUpperCase();\n if (upper === 'TRUE') return { value: true, type: 'boolean' };\n if (upper === 'FALSE') return { value: false, type: 'boolean' };\n if (FORMULA_ERRORS.has(upper)) return { value: upper, type: 'error' };\n const numeric = parseNumeric(t);\n if (numeric) {\n if (numeric.kind === 'date') {\n const iso = isoText(numeric.date);\n // ISO text can't hold the fictitious 1900-02-29: it stays text.\n if (iso !== null) {\n return { value: iso, type: 'date', numberFormat: numeric.format, formatKind: 'date', cellFormat: 'date' };\n }\n } else {\n const formatKind = numeric.kind === 'time' ? 'time' : formatKindOf(numeric.format);\n return {\n value: normalizeZero(numeric.value),\n type: 'number',\n ...(numeric.format ? { numberFormat: numeric.format } : {}),\n ...(formatKind ? { formatKind } : {}),\n ...(formatKind && formatKind !== 'time' ? { cellFormat: formatKind } : {}),\n };\n }\n }\n return { value: input, type: 'text' };\n}\n\n/**\n * Numeric text as arithmetic coerces it (\"1,000\", \"50%\", \"$3\", \"1/2/2020\",\n * \"10:00\"); dates become 1900-system serial numbers. Null when it isn't numeric.\n */\nexport function parseNumberText(text: string): number | null {\n const numeric = parseNumeric(text.trim());\n if (!numeric) return null;\n if (numeric.kind !== 'date') return normalizeZero(numeric.value);\n const { year, month, day, time = 0 } = numeric.date;\n const serial = serialFromYmd(year, month, day);\n return serial === null ? null : serial + time;\n}\n\nconst normalizeZero = (n: number) => (n === 0 ? 0 : n);\n\nfunction formatKindOf(format: string | undefined): CellInputFormatKind | undefined {\n if (!format) return undefined;\n if (format.includes('$')) return 'currency';\n if (format.includes('%')) return 'percentage';\n return 'number';\n}\n\nconst pad = (n: number, width = 2) => String(n).padStart(width, '0');\n\n/** ISO text for parsed date parts, or null for the fictitious 1900-02-29. */\nfunction isoText({ year, month, day, time }: DateParts): string | null {\n if (day > daysInMonth(year, month)) return null;\n const date = `${pad(year, 4)}-${pad(month)}-${pad(day)}`;\n if (time === undefined) return date;\n let seconds = Math.round(time * 86400);\n if (seconds >= 86400) seconds = 86399; // never roll a typed time into the next day\n return `${date} ${pad(Math.floor(seconds / 3600))}:${pad(Math.floor(seconds / 60) % 60)}:${pad(seconds % 60)}`;\n}\n\nfunction parseNumeric(t: string): Numeric | null {\n if (t === '') return null;\n // Parenthesised negatives: (100) = -100.\n if (t.startsWith('(') && t.endsWith(')') && t.length >= 2) {\n const inner = parseNumeric(t.slice(1, -1).trim());\n if (!inner || inner.kind === 'date') return null;\n return { ...inner, value: -inner.value };\n }\n const plain = parsePlain(t);\n if (plain) return plain;\n const fraction = parseFraction(t);\n if (fraction !== null) return { kind: 'number', value: fraction, format: '# ?/?' };\n return parseDateTime(t);\n}\n\nconst startsWithDigitOrDot = (s: string) => /^[0-9.]/.test(s);\n\n/** Signs, currency, thousands separators, percent, exponent. */\nfunction parsePlain(t: string): Numeric | null {\n let s = t;\n let neg = false;\n let currency = false;\n let percent = false;\n if (s.startsWith('-')) {\n neg = true;\n s = s.slice(1).trimStart();\n } else if (s.startsWith('+')) {\n s = s.slice(1).trimStart();\n }\n if (s.startsWith('$')) {\n currency = true;\n s = s.slice(1).trimStart();\n if (!neg && s.startsWith('-')) {\n neg = true;\n s = s.slice(1);\n }\n }\n if (s.endsWith('%')) {\n percent = true;\n s = s.slice(0, -1).trimEnd();\n }\n if (s === '') return null;\n const hasSep = s.includes(',');\n let digits: string;\n if (hasSep) {\n // Thousands separators must be in groups of three before the decimal point.\n const dot = s.indexOf('.');\n const int = dot === -1 ? s : s.slice(0, dot);\n const frac = dot === -1 ? null : s.slice(dot + 1);\n const groups = int.split(',');\n const first = groups[0];\n if (first === '' || first.length > 3 || groups.slice(1).some((g) => g.length !== 3)) return null;\n digits = groups.join('') + (frac !== null ? `.${frac}` : '');\n } else {\n digits = s;\n }\n if (!/^[0-9.eE+-]+$/.test(digits) || !startsWithDigitOrDot(digits)) return null;\n // Rust's f64 parser accepts exactly decimal literals; JS `Number` is looser\n // (\"1.\" and \"1e\" differ), so check the shape too.\n if (!/^(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?$/.test(digits)) return null;\n let n = Number(digits);\n if (!Number.isFinite(n)) return null;\n const sci = /[eE]/.test(digits);\n if (percent) n /= 100;\n if (neg) n = -n;\n const hasDot = digits.includes('.');\n const format = currency\n ? hasDot ? '\"$\"#,##0.00' : '\"$\"#,##0'\n : percent\n ? hasDot ? '0.00%' : '0%'\n : sci\n ? '0.00E+00'\n : hasSep\n ? hasDot ? '#,##0.00' : '#,##0'\n : undefined;\n return { kind: 'number', value: n, ...(format ? { format } : {}) };\n}\n\nconst parseDecimal = (s: string): number | null =>\n /^[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?$/.test(s) ? Number(s) : null;\n\n/** `1 1/2`, `0 3/4` (a bare `1/2` is a date). */\nfunction parseFraction(t: string): number | null {\n const space = t.indexOf(' ');\n if (space === -1) return null;\n const whole = t.slice(0, space);\n const frac = t.slice(space + 1).trim();\n const neg = whole.startsWith('-');\n const w = parseDecimal(whole.replace(/^-+/, ''));\n const slash = frac.indexOf('/');\n if (w === null || slash === -1) return null;\n const a = parseDecimal(frac.slice(0, slash));\n const b = parseDecimal(frac.slice(slash + 1));\n if (a === null || b === null || b === 0 || !Number.isFinite(w)) return null;\n const v = w + a / b;\n return neg ? -v : v;\n}\n\nfunction monthFromName(s: string): number | null {\n const l = s.toLowerCase();\n if (l.length < 3) return null;\n const index = MONTH_NAMES.findIndex((m) => m.toLowerCase().startsWith(l));\n return index === -1 ? null : index + 1;\n}\n\n/** `h:mm`, `h:mm:ss`, `h:mm:ss.000`, `h AM`, with optional AM/PM. */\nfunction parseTime(input: string): { fraction: number; seconds: boolean; ampm: boolean } | null {\n const lower = input.trim().toLowerCase();\n let body = lower;\n let pm: boolean | null = null;\n for (const [suffix, isPm] of [['am', false], ['a', false], ['pm', true], ['p', true]] as const) {\n if (lower.endsWith(suffix)) {\n body = lower.slice(0, -suffix.length).trim();\n pm = isPm;\n break;\n }\n }\n const parts = body.split(':');\n if (parts.length === 1 && pm === null) return null;\n if (parts.length > 3 || parts.some((p) => p === '')) return null;\n const num = (p: string | undefined) => (p === undefined ? 0 : parseDecimal(p));\n let h = num(parts[0]);\n const m = num(parts[1]);\n const s = num(parts[2]);\n if (h === null || m === null || s === null) return null;\n if (m >= 60 || s >= 60 || h < 0 || m < 0 || s < 0) return null;\n if (pm !== null) {\n if (h > 12) return null;\n if (h === 12) h = 0;\n if (pm) h += 12;\n } else if (h >= 10000) {\n return null;\n }\n return { fraction: timeFraction(h, m, s), seconds: parts.length === 3, ampm: pm !== null };\n}\n\nconst expandYear = (text: string, y: number) => (text.length <= 2 ? (y < 30 ? 2000 + y : 1900 + y) : y);\n\nfunction validYmd(year: number, month: number, day: number): DateParts | null {\n // Dates before 1900 stay text (`DATE()` would add 1900 to such a year).\n if (month < 1 || month > 12 || day < 1 || year < 1900 || year > 9999) return null;\n if (day > daysInMonth1900(year, month)) return null;\n return { year, month, day };\n}\n\nfunction parseDate(input: string): { date: DateParts; format: string } | null {\n const t = input.trim().replace(/,+$/, '');\n const num = (s: string) => (/^[+-]?\\d+$/.test(s.trim()) ? Number(s.trim()) : null);\n const withFormat = (date: DateParts | null, format: string) => (date ? { date, format } : null);\n const parts = t.split(/[/.-]/);\n if (parts.length === 3) {\n const [a, b, c] = parts.map((p) => p.trim());\n // ISO yyyy-mm-dd or yyyy/mm/dd\n if (a.length === 4) {\n const [y, m, d] = [num(a), num(b), num(c)];\n if (y !== null && m !== null && d !== null) return withFormat(validYmd(y, m, d), 'yyyy-mm-dd');\n }\n {\n const [m, d, y] = [num(a), num(b), num(c)];\n if (m !== null && d !== null && y !== null) return withFormat(validYmd(expandYear(c, y), m, d), 'm/d/yyyy');\n }\n // 4-Mar-2026\n const [d, m, y] = [num(a), monthFromName(b), num(c)];\n if (d !== null && m !== null && y !== null) return withFormat(validYmd(expandYear(c, y), m, d), 'd-mmm-yy');\n }\n if (parts.length === 2) {\n const [a, b] = parts.map((p) => p.trim());\n const [x, y] = [num(a), num(b)];\n if (x !== null && y !== null) {\n // m/d in the current year\n if (x >= 1 && x <= 12 && y >= 1 && y <= 31) return withFormat(validYmd(currentYear(), x, y), 'd-mmm');\n // m/yyyy\n if (x >= 1 && x <= 12 && b.length === 4) return withFormat(validYmd(y, x, 1), 'mmm-yy');\n }\n const dayA = num(a);\n const monthB = monthFromName(b);\n if (dayA !== null && monthB !== null) return withFormat(validYmd(currentYear(), monthB, dayA), 'd-mmm');\n const monthA = monthFromName(a);\n const yearB = num(b);\n if (monthA !== null && yearB !== null) return withFormat(validYmd(expandYear(b, yearB), monthA, 1), 'mmm-yy');\n }\n // \"Mar 4, 2026\" / \"March 4 2026\" / \"4 March 2026\"\n const words = t.split(/[ ,]/).filter((w) => w !== '');\n if (words.length === 3) {\n const [w0, w1, w2] = words;\n let [m, d, y] = [monthFromName(w0), num(w1), num(w2)];\n if (m !== null && d !== null && y !== null) return withFormat(validYmd(expandYear(w2, y), m, d), 'd-mmm-yy');\n [d, m, y] = [num(w0), monthFromName(w1), num(w2)];\n if (d !== null && m !== null && y !== null) return withFormat(validYmd(expandYear(w2, y), m, d), 'd-mmm-yy');\n }\n return null;\n}\n\nfunction parseDateTime(t: string): Numeric | null {\n const time = parseTime(t);\n if (time) {\n const format =\n time.fraction >= 1\n ? '[h]:mm:ss' // 24 hours or more: elapsed hours\n : time.seconds\n ? time.ampm ? 'h:mm:ss AM/PM' : 'h:mm:ss'\n : time.ampm ? 'h:mm AM/PM' : 'h:mm';\n return { kind: 'time', value: time.fraction, format };\n }\n const date = parseDate(t);\n if (date) return { kind: 'date', date: date.date, format: date.format };\n // date + time: split at a space before a time-looking tail.\n for (let i = t.indexOf(' '); i !== -1; i = t.indexOf(' ', i + 1)) {\n const d = parseDate(t.slice(0, i));\n const tm = d ? parseTime(t.slice(i + 1)) : null;\n // A date-time's time of day must stay within the day.\n if (d && tm && tm.fraction < 1) {\n return { kind: 'date', date: { ...d.date, time: tm.fraction }, format: 'm/d/yyyy h:mm' };\n }\n }\n return null;\n}\n","// Portions ported from gridcraft/crates/numfmt/src/decimal.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/**\n * Decimal-string rounding and the General format.\n *\n * Excel works with 15 significant digits, so numbers are first written with\n * 15 significant digits and then rounded half away from zero on that decimal\n * string. That makes 2.675 with `0.00` show 2.68 and 1.005 show 1.01, as in\n * Excel, even though neither is exact in binary.\n */\n\n/** Most decimals ever computed; placeholders beyond this are padded with zeros. */\nexport const MAX_DECIMALS = 400;\n\n/**\n * `n` significant digits of a positive finite `a` (correctly rounded) and the\n * power of ten of the first digit.\n */\nfunction significantDigits(a: number, n: number): { digits: number[]; exp: number } {\n const s = a.toExponential(Math.max(0, Math.min(100, n - 1)));\n const e = s.indexOf('e');\n const digits: number[] = [];\n for (let i = 0; i < e; i++) {\n const c = s.charCodeAt(i);\n if (c >= 48 && c <= 57) digits.push(c - 48);\n }\n return { digits, exp: Number(s.slice(e + 1)) };\n}\n\n/** The exact decimal expansion of a positive finite double, as significant digits (no trailing zeros). */\nfunction exactDigits(a: number): string {\n const view = new DataView(new ArrayBuffer(8));\n view.setFloat64(0, a);\n const hi = view.getUint32(0);\n const lo = view.getUint32(4);\n const biased = (hi >>> 20) & 0x7ff;\n let mantissa = (BigInt(hi & 0xfffff) << 32n) | BigInt(lo);\n let exp2: number;\n if (biased === 0) {\n exp2 = -1074;\n } else {\n mantissa |= 1n << 52n;\n exp2 = biased - 1075;\n }\n const digits = exp2 >= 0 ? (mantissa << BigInt(exp2)).toString() : (mantissa * 5n ** BigInt(-exp2)).toString();\n return digits.replace(/0+$/, '');\n}\n\n/** True when `a`'s exact decimal expansion has a 5 as its 16th significant digit and only zeros after. */\nfunction isExactTie(a: number): boolean {\n const isTie = (ds: string) => ds.length >= 16 && ds[15] === '5' && /^0*$/.test(ds.slice(16));\n // A cheap screen first (30 digits), then the exact expansion.\n return isTie(significantDigits(a, 30).digits.join('')) && isTie(exactDigits(a).padEnd(16, '0'));\n}\n\n/**\n * The 15 significant digits of a positive finite number and the power of ten\n * of the first digit: `a ≈ d0.d1d2… × 10^exp`.\n *\n * The value is rounded to the nearest 15-digit decimal. An exact tie at the\n * 16th digit goes toward zero, as Excel does: `1234567890123455` shows\n * `1234567890123450`, not `…460`.\n */\nexport function decimalDigits(a: number): { digits: number[]; exp: number } {\n if (!Number.isFinite(a) || a <= 0) return { digits: new Array(15).fill(0), exp: 0 };\n const { digits: d16, exp } = significantDigits(a, 16);\n const digits = d16.slice(0, 15);\n while (digits.length < 15) digits.push(0);\n const sixteenth = d16[15] ?? 0;\n if (sixteenth <= 4) return { digits, exp };\n if (sixteenth === 5) {\n if (isExactTie(a)) return { digits, exp };\n // Not a tie: round the exact value (the 16-digit rounding may be a double rounding).\n const r = significantDigits(a, 15);\n while (r.digits.length < 15) r.digits.push(0);\n return r;\n }\n let e = exp;\n let carry = true;\n for (let i = 14; i >= 0; i--) {\n if (digits[i] === 9) {\n digits[i] = 0;\n } else {\n digits[i] += 1;\n carry = false;\n break;\n }\n }\n if (carry) {\n digits.fill(0);\n digits[0] = 1;\n e += 1;\n }\n return { digits, exp: e };\n}\n\n/**\n * Rounds the number `digits × 10^exp` (see `decimalDigits`) to `decimals`\n * places, half away from zero. Returns the integer digits (`''` when the\n * integer part is zero) and exactly `decimals` fraction digits.\n */\nexport function fixedFromDigits(digits: number[], exp: number, decimals: number): [string, string] {\n const decimalsCalc = Math.min(decimals, MAX_DECIMALS);\n const keep = exp + decimalsCalc + 1;\n let m: number[];\n if (keep <= 0) {\n m = keep === 0 && (digits[0] ?? 0) >= 5 ? [1] : [];\n } else if (keep >= digits.length) {\n m = digits.slice();\n while (m.length < keep) m.push(0);\n } else {\n m = digits.slice(0, keep);\n if ((digits[keep] ?? 0) >= 5) {\n let carry = true;\n for (let i = m.length - 1; i >= 0; i--) {\n if (m[i] === 9) {\n m[i] = 0;\n } else {\n m[i] += 1;\n carry = false;\n break;\n }\n }\n if (carry) m.unshift(1);\n }\n }\n let lead = 0;\n while (lead < m.length && m[lead] === 0) lead++;\n m = m.slice(lead);\n const toS = (ds: number[]) => ds.join('');\n let int: string;\n let frac: string;\n if (m.length <= decimalsCalc) {\n int = '';\n frac = '0'.repeat(decimalsCalc - m.length) + toS(m);\n } else {\n const split = m.length - decimalsCalc;\n int = toS(m.slice(0, split));\n frac = toS(m.slice(split));\n }\n if (decimals > decimalsCalc) frac += '0'.repeat(decimals - decimalsCalc);\n return [int, frac];\n}\n\n/** Rounds a non-negative number to `decimals` places (see `fixedFromDigits`). */\nexport function fixed(a: number, decimals: number): [string, string] {\n if (!Number.isFinite(a) || a <= 0) return ['', '0'.repeat(Math.min(decimals, MAX_DECIMALS * 4))];\n const { digits, exp } = decimalDigits(a);\n return fixedFromDigits(digits, exp, decimals);\n}\n\nconst charCount = (s: string) => Array.from(s).length;\n\n/**\n * The General format squeezed into `maxChars` characters, as Excel shows it\n * in a narrow column: fewer decimals, then scientific notation; `null` when\n * even that does not fit (the caller paints `####`).\n */\nexport function formatGeneralFit(n: number, maxChars: number): string | null {\n if (!Number.isFinite(n)) return null;\n if (n === 0) return maxChars >= 1 ? '0' : null;\n const sign = n < 0 ? '-' : '';\n const { digits, exp: x } = decimalDigits(Math.abs(n));\n const intLen = sign.length + Math.max(x, 0) + 1;\n // Fixed notation for magnitudes from 1E-4 up to what fits.\n if (x >= -4 && intLen <= maxChars) {\n const avail = maxChars - intLen;\n let decimals = avail >= 2 ? avail - 1 : 0;\n for (;;) {\n const [i, f0] = fixedFromDigits(digits, x, decimals);\n const f = f0.replace(/0+$/, '');\n if (i === '' && f === '') break; // rounded away to zero: use scientific\n let s = sign + (i === '' ? '0' : i);\n if (f !== '') s += `.${f}`;\n if (charCount(s) <= maxChars) return s;\n if (decimals === 0) break;\n decimals -= 1;\n }\n }\n for (let md = Math.min(maxChars, 14); md >= 0; md--) {\n let e = x;\n let [mi, mf] = fixedFromDigits(digits, 0, md);\n if (mi.length > 1) {\n e += 1;\n [mi, mf] = fixedFromDigits(digits, -1, md);\n }\n mf = mf.replace(/0+$/, '');\n let s = sign + (mi === '' ? '0' : mi);\n if (mf !== '') s += `.${mf}`;\n s += `E${e < 0 ? '-' : '+'}${String(Math.abs(e)).padStart(2, '0')}`;\n if (charCount(s) <= maxChars) return s;\n }\n return null;\n}\n","// Portions ported from gridcraft/crates/numfmt/src/builtin.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/** Built-in number formats (ECMA-376 §18.8.30, en-US). */\n\nconst BUILTINS: ReadonlyArray<readonly [number, string]> = [\n [0, 'General'],\n [1, '0'],\n [2, '0.00'],\n [3, '#,##0'],\n [4, '#,##0.00'],\n [5, '\"$\"#,##0_);\\\\(\"$\"#,##0\\\\)'],\n [6, '\"$\"#,##0_);[Red]\\\\(\"$\"#,##0\\\\)'],\n [7, '\"$\"#,##0.00_);\\\\(\"$\"#,##0.00\\\\)'],\n [8, '\"$\"#,##0.00_);[Red]\\\\(\"$\"#,##0.00\\\\)'],\n [9, '0%'],\n [10, '0.00%'],\n [11, '0.00E+00'],\n [12, '# ?/?'],\n [13, '# ??/??'],\n [14, 'm/d/yyyy'],\n [15, 'd-mmm-yy'],\n [16, 'd-mmm'],\n [17, 'mmm-yy'],\n [18, 'h:mm AM/PM'],\n [19, 'h:mm:ss AM/PM'],\n [20, 'h:mm'],\n [21, 'h:mm:ss'],\n [22, 'm/d/yyyy h:mm'],\n [37, '#,##0 ;(#,##0)'],\n [38, '#,##0 ;[Red](#,##0)'],\n [39, '#,##0.00;(#,##0.00)'],\n [40, '#,##0.00;[Red](#,##0.00)'],\n [41, '_(* #,##0_);_(* \\\\(#,##0\\\\);_(* \"-\"_);_(@_)'],\n [42, '_(\"$\"* #,##0_);_(\"$\"* \\\\(#,##0\\\\);_(\"$\"* \"-\"_);_(@_)'],\n [43, '_(* #,##0.00_);_(* \\\\(#,##0.00\\\\);_(* \"-\"??_);_(@_)'],\n [44, '_(\"$\"* #,##0.00_);_(\"$\"* \\\\(#,##0.00\\\\);_(\"$\"* \"-\"??_);_(@_)'],\n [45, 'mm:ss'],\n [46, '[h]:mm:ss'],\n [47, 'mmss.0'],\n [48, '##0.0E+0'],\n [49, '@'],\n];\n\n/**\n * Built-in format code for an id (0–49, en-US), e.g. 14 → `m/d/yyyy`.\n * Locale-specific ids (23–36) and unknown ids return undefined.\n */\nexport function builtinNumberFormat(id: number): string | undefined {\n return BUILTINS.find(([i]) => i === id)?.[1];\n}\n\n/** Id of a built-in format code. Matching ignores case and backslash escapes. */\nexport function builtinNumberFormatId(code: string): number | undefined {\n const exact = BUILTINS.find(([, c]) => c === code);\n if (exact) return exact[0];\n const norm = (s: string) => s.replace(/\\\\/g, '').toLowerCase();\n const n = norm(code);\n return BUILTINS.find(([, c]) => norm(c) === n)?.[0];\n}\n","// Portions ported from gridcraft/crates/numfmt/src/parse.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/** Number format code parsing: sections, bracket tags and tokens. */\n\n/** Colour from a `[Red]` or `[ColorN]` section tag (`color1`…`color56` = palette index). */\nexport type NumberFormatColor =\n | 'black'\n | 'blue'\n | 'cyan'\n | 'green'\n | 'magenta'\n | 'red'\n | 'white'\n | 'yellow'\n | `color${number}`;\n\n/** At most this many sections are honoured (`pos;neg;zero;text`); extra ones are ignored. */\nconst MAX_SECTIONS = 4;\n\nexport type DatePart =\n | { k: 'year' | 'month' | 'minute' | 'day' | 'hour' | 'second' | 'subsec' | 'eh' | 'em' | 'es'; n: number }\n | { k: 'ampm' }\n /** `A/P` with the letters as typed. */\n | { k: 'ap'; a: string; p: string };\n\nexport const isCalendarPart = (d: DatePart): boolean => d.k === 'year' || d.k === 'month' || d.k === 'day';\n\nexport type Tok =\n | { t: 'lit'; s: string }\n /** `0`, `#` or `?`. */\n | { t: 'digit'; c: string }\n /** The decimal point (only the first one in a number section). */\n | { t: 'point' }\n /** Unresolved comma (resolved during analysis into thousands/scale markers or a literal). */\n | { t: 'comma' }\n | { t: 'percent' }\n | { t: 'exp'; upper: boolean; plus: boolean }\n /** The fraction bar (only after analysis decides the section is a fraction). */\n | { t: 'slash' }\n /** A fixed fraction denominator such as the `8` in `?/8`. */\n | { t: 'denom'; s: string }\n | { t: 'fill'; c: string }\n | { t: 'at' }\n | { t: 'general' }\n | { t: 'date'; d: DatePart };\n\n/** The text a token shows when it has no special meaning in its section. */\nexport function literalText(tok: Tok): string {\n switch (tok.t) {\n case 'lit':\n case 'denom':\n return tok.s;\n case 'digit':\n return tok.c;\n case 'point':\n return '.';\n case 'comma':\n return ',';\n case 'percent':\n return '%';\n case 'exp':\n return `${tok.upper ? 'E' : 'e'}${tok.plus ? '+' : '-'}`;\n case 'slash':\n return '/';\n default:\n return '';\n }\n}\n\nexport type CondOp = '=' | '<>' | '<' | '<=' | '>' | '>=';\n\nexport interface Cond {\n op: CondOp;\n value: number;\n}\n\nexport function testCond(c: Cond, v: number): boolean {\n switch (c.op) {\n case '=':\n return v === c.value;\n case '<>':\n return v !== c.value;\n case '<':\n return v < c.value;\n case '<=':\n return v <= c.value;\n case '>':\n return v > c.value;\n case '>=':\n return v >= c.value;\n }\n}\n\nexport type SecKind = 'number' | 'date' | 'text' | 'general';\n\nexport interface Section {\n toks: Tok[];\n color?: NumberFormatColor;\n cond?: Cond;\n kind: SecKind;\n thousands: boolean;\n /** Number of scaling commas (each divides by 1000). */\n scale: number;\n percent: number;\n hasAt: boolean;\n hasDigits: boolean;\n exp: boolean;\n fraction: boolean;\n currency: boolean;\n fill: boolean;\n}\n\nconst isDigitTok = (t: Tok | undefined, c?: string): boolean => t?.t === 'digit' && (c === undefined || t.c === c);\n\n/**\n * Splits a code into sections at unquoted, unescaped, unbracketed semicolons.\n * A `[` opens a bracket only when a `]` follows it somewhere; the bracket then\n * runs to the next `]`. An unclosed `[` is a literal, so a later `;` still\n * separates sections. Linear in the code length.\n */\nexport function splitSections(code: string): string[] {\n const cs = Array.from(code);\n const lastClose = cs.lastIndexOf(']');\n const out: string[] = [''];\n let quoted = false;\n let escape = false;\n let i = 0;\n while (i < cs.length) {\n const c = cs[i];\n i += 1;\n const cur = out.length - 1;\n if (escape) {\n out[cur] += c;\n escape = false;\n continue;\n }\n if (c === '\"') {\n quoted = !quoted;\n out[cur] += c;\n } else if ((c === '\\\\' || c === '_' || c === '*') && !quoted) {\n escape = true;\n out[cur] += c;\n } else if (c === '[' && !quoted && lastClose >= i) {\n // `i` is already past the `[`; the closing bracket is at or after it.\n let s = c;\n while (i < cs.length) {\n const d = cs[i];\n i += 1;\n s += d;\n if (d === ']') break;\n }\n out[cur] += s;\n } else if (c === ';' && !quoted) {\n if (out.length >= MAX_SECTIONS) break;\n out.push('');\n } else {\n out[cur] += c;\n }\n }\n return out;\n}\n\nconst lowerAscii = (c: string) => (c >= 'A' && c <= 'Z' ? c.toLowerCase() : c);\n\nfunction startsWithCi(cs: string[], i: number, pat: string): boolean {\n for (let k = 0; k < pat.length; k++) {\n const c = cs[i + k];\n if (c === undefined || lowerAscii(c) !== pat[k]) return false;\n }\n return true;\n}\n\nfunction pushLit(toks: Tok[], s: string): void {\n if (s === '') return;\n const last = toks[toks.length - 1];\n if (last?.t === 'lit') last.s += s;\n else toks.push({ t: 'lit', s });\n}\n\nconst CURRENCY_CHARS = new Set(['$', '€', '£', '¥', '₹', '₩', '₽', '¢', '₪', '₫', '฿']);\nconst isCurrencyChar = (c: string) => CURRENCY_CHARS.has(c);\n\nconst NAMED_COLORS = new Set(['black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow']);\n\nfunction parseColor(lc: string): NumberFormatColor | undefined {\n if (NAMED_COLORS.has(lc)) return lc as NumberFormatColor;\n if (!lc.startsWith('color')) return undefined;\n const rest = lc.slice(5).trim();\n if (!/^\\+?\\d{1,3}$/.test(rest)) return undefined;\n const n = Number(rest);\n return n >= 1 && n <= 56 ? `color${n}` : undefined;\n}\n\nfunction parseCond(s: string): Cond | undefined {\n const t = s.trim();\n let op: CondOp;\n let rest: string;\n if (t.startsWith('<=')) [op, rest] = ['<=', t.slice(2)];\n else if (t.startsWith('>=')) [op, rest] = ['>=', t.slice(2)];\n else if (t.startsWith('<>')) [op, rest] = ['<>', t.slice(2)];\n else if (t.startsWith('<')) [op, rest] = ['<', t.slice(1)];\n else if (t.startsWith('>')) [op, rest] = ['>', t.slice(1)];\n else if (t.startsWith('=')) [op, rest] = ['=', t.slice(1)];\n else return undefined;\n const r = rest.trim();\n if (!/^[+-]?(\\d+\\.?\\d*|\\.\\d+)(e[+-]?\\d+)?$/i.test(r)) return undefined;\n const value = Number(r);\n return Number.isFinite(value) ? { op, value } : undefined;\n}\n\ninterface Raw {\n toks: Tok[];\n color?: NumberFormatColor;\n cond?: Cond;\n currency: boolean;\n}\n\n/** Handles the content of a `[...]` tag. */\nfunction bracket(raw: Raw, content: string): void {\n const lc = content.trim().toLowerCase();\n const color = parseColor(lc);\n if (color) {\n raw.color = color;\n return;\n }\n if (lc.startsWith('<') || lc.startsWith('>') || lc.startsWith('=')) {\n const c = parseCond(lc);\n if (c) raw.cond = c;\n return;\n }\n if (content.startsWith('$')) {\n const sym = content.slice(1).split('-')[0];\n if (sym !== '') {\n raw.currency = true;\n pushLit(raw.toks, sym);\n }\n return;\n }\n const first = lc[0];\n if ((first === 'h' || first === 'm' || first === 's') && Array.from(lc).every((c) => c === first)) {\n const n = Math.min(Array.from(lc).length, 255);\n raw.toks.push({ t: 'date', d: { k: first === 'h' ? 'eh' : first === 'm' ? 'em' : 'es', n } });\n }\n // Anything else ([DBNum1], [natural], locale tags…) is ignored.\n}\n\nfunction tokenize(src: string): Raw {\n const cs = Array.from(src);\n const raw: Raw = { toks: [], currency: false };\n const toks = raw.toks;\n // With no `]` after a `[`, that `[` is a literal; checking this once keeps the scan linear.\n const lastClose = cs.lastIndexOf(']');\n let i = 0;\n while (i < cs.length) {\n const c = cs[i];\n if (c === '\"') {\n let s = '';\n i += 1;\n while (i < cs.length) {\n const d = cs[i];\n i += 1;\n if (d === '\"') break;\n s += d;\n }\n if (Array.from(s).some(isCurrencyChar)) raw.currency = true;\n pushLit(toks, s);\n continue;\n }\n if (c === '\\\\') {\n const d = cs[i + 1];\n if (d !== undefined) {\n if (isCurrencyChar(d)) raw.currency = true;\n pushLit(toks, d);\n }\n i += 2;\n continue;\n }\n if (c === '_') {\n if (cs[i + 1] !== undefined) pushLit(toks, ' ');\n i += 2;\n continue;\n }\n if (c === '*') {\n const d = cs[i + 1];\n if (d !== undefined) toks.push({ t: 'fill', c: d });\n i += 2;\n continue;\n }\n if (c === '[') {\n const close = lastClose > i ? cs.indexOf(']', i + 1) : -1;\n if (close !== -1) {\n bracket(raw, cs.slice(i + 1, close).join(''));\n i = close + 1;\n } else {\n pushLit(toks, '[');\n i += 1;\n }\n continue;\n }\n if (c === '0' || c === '#' || c === '?') toks.push({ t: 'digit', c });\n else if (c === '.') toks.push({ t: 'point' });\n else if (c === ',') toks.push({ t: 'comma' });\n else if (c === '%') toks.push({ t: 'percent' });\n else if (c === '@') toks.push({ t: 'at' });\n else if (c === '/') {\n toks.push({ t: 'slash' });\n i += 1;\n if (cs[i] !== undefined && cs[i] >= '1' && cs[i] <= '9') {\n let s = '';\n while (i < cs.length && cs[i] >= '0' && cs[i] <= '9') {\n s += cs[i];\n i += 1;\n }\n toks.push({ t: 'denom', s });\n }\n continue;\n } else if ((c === 'e' || c === 'E') && (cs[i + 1] === '+' || cs[i + 1] === '-')) {\n toks.push({ t: 'exp', upper: c === 'E', plus: cs[i + 1] === '+' });\n i += 2;\n continue;\n } else if ((c === 'G' || c === 'g') && startsWithCi(cs, i, 'general')) {\n toks.push({ t: 'general' });\n i += 7;\n continue;\n } else if ((c === 'A' || c === 'a') && startsWithCi(cs, i, 'am/pm')) {\n toks.push({ t: 'date', d: { k: 'ampm' } });\n i += 5;\n continue;\n } else if ((c === 'A' || c === 'a') && startsWithCi(cs, i, 'a/p')) {\n toks.push({ t: 'date', d: { k: 'ap', a: c, p: cs[i + 2] ?? 'P' } });\n i += 3;\n continue;\n } else if ('yYmMdDhHsSeE'.includes(c)) {\n const lc = lowerAscii(c);\n let n = 0;\n while (cs[i + n] !== undefined && lowerAscii(cs[i + n]) === lc) n += 1;\n const n8 = Math.min(n, 255);\n const d: DatePart =\n lc === 'y'\n ? { k: 'year', n: n8 }\n : lc === 'e'\n ? { k: 'year', n: 4 }\n : lc === 'm'\n ? { k: 'month', n: n8 }\n : lc === 'd'\n ? { k: 'day', n: n8 }\n : lc === 'h'\n ? { k: 'hour', n: n8 }\n : { k: 'second', n: n8 };\n toks.push({ t: 'date', d });\n i += Math.max(n, 1);\n continue;\n } else {\n if (isCurrencyChar(c)) raw.currency = true;\n pushLit(toks, c);\n }\n i += 1;\n }\n return raw;\n}\n\nconst datePartOf = (t: Tok | undefined): DatePart | undefined => (t?.t === 'date' ? t.d : undefined);\n\nfunction analyze(raw: Raw): Section {\n const toks = raw.toks;\n const isDate = toks.some((t) => t.t === 'date');\n const hasGeneral = toks.some((t) => t.t === 'general');\n const sec: Section = {\n toks: [],\n color: raw.color,\n cond: raw.cond,\n kind: 'number',\n thousands: false,\n scale: 0,\n percent: 0,\n hasAt: toks.some((t) => t.t === 'at'),\n hasDigits: false,\n exp: false,\n fraction: false,\n currency: raw.currency,\n fill: toks.some((t) => t.t === 'fill'),\n };\n if (isDate) {\n sec.kind = 'date';\n // m/mm after hours or before seconds means minutes. Scanning for the\n // nearest date part either side is linear overall (prev/next are tracked).\n const prevPart: Array<DatePart | undefined> = [];\n let last: DatePart | undefined;\n for (let i = 0; i < toks.length; i++) {\n prevPart.push(last);\n const d = datePartOf(toks[i]);\n if (d) last = d;\n }\n const nextPart: Array<DatePart | undefined> = new Array(toks.length);\n let next: DatePart | undefined;\n for (let i = toks.length - 1; i >= 0; i--) {\n nextPart[i] = next;\n const d = datePartOf(toks[i]);\n if (d) next = d;\n }\n for (let i = 0; i < toks.length; i++) {\n const t = toks[i];\n if (t.t !== 'date' || t.d.k !== 'month' || t.d.n > 2) continue;\n const prev = prevPart[i];\n const nxt = nextPart[i];\n const afterH = prev?.k === 'hour' || prev?.k === 'eh';\n const beforeS = nxt?.k === 'second' || nxt?.k === 'es';\n if (afterH || beforeS) toks[i] = { t: 'date', d: { k: 'minute', n: t.d.n } };\n }\n const out: Tok[] = [];\n let i = 0;\n while (i < toks.length) {\n const t = toks[i];\n if (t.t === 'point' && isDigitTok(toks[i + 1], '0')) {\n let n = 0;\n while (isDigitTok(toks[i + 1 + n], '0')) n += 1;\n out.push({ t: 'date', d: { k: 'subsec', n: Math.min(n, 255) } });\n i += 1 + n;\n continue;\n }\n if (t.t === 'date' || t.t === 'fill') out.push(t);\n else pushLit(out, literalText(t));\n i += 1;\n }\n sec.toks = out;\n return sec;\n }\n if (hasGeneral) {\n sec.kind = 'general';\n const out: Tok[] = [];\n for (const t of toks) {\n if (t.t === 'general' || t.t === 'fill') out.push(t);\n else pushLit(out, literalText(t));\n }\n sec.toks = out;\n return sec;\n }\n sec.hasDigits = toks.some((t) => t.t === 'digit');\n if (sec.hasAt && !sec.hasDigits) {\n sec.kind = 'text';\n const out: Tok[] = [];\n for (const t of toks) {\n if (t.t === 'at' || t.t === 'fill') out.push(t);\n else pushLit(out, literalText(t));\n }\n sec.toks = out;\n return sec;\n }\n // Number section.\n let seenPoint = false;\n let seenExp = false;\n let seenDigit = false;\n let fractionSlash = false;\n // Precomputed so each comma and slash is O(1): whether the first non-comma\n // token after a position is a digit, the first non-literal token after a\n // position, and where the last exponent marker is.\n const digitAfterCommas: boolean[] = new Array(toks.length).fill(false);\n const firstNonLitAfter: Array<Tok | undefined> = new Array(toks.length);\n let after = false;\n let nonLit: Tok | undefined;\n for (let j = toks.length - 1; j >= 0; j--) {\n digitAfterCommas[j] = after;\n firstNonLitAfter[j] = nonLit;\n const t = toks[j];\n if (t.t !== 'comma') after = t.t === 'digit';\n if (t.t !== 'lit') nonLit = t;\n }\n let lastExp = -1;\n for (let j = toks.length - 1; j >= 0; j--) {\n if (toks[j].t === 'exp') {\n lastExp = j;\n break;\n }\n }\n const firstDigit = toks.findIndex((t) => t.t === 'digit');\n const scientific = firstDigit !== -1 && toks.slice(firstDigit).some((t) => t.t === 'exp');\n for (let i = 0; i < toks.length; i++) {\n const t = toks[i];\n let replace: Tok | undefined;\n switch (t.t) {\n case 'digit':\n seenDigit = true;\n break;\n case 'point':\n if (seenPoint || seenExp) replace = { t: 'lit', s: '.' };\n else seenPoint = true;\n break;\n case 'percent':\n sec.percent += 1;\n break;\n case 'exp':\n if (seenExp || !seenDigit) replace = { t: 'lit', s: literalText(t) };\n else {\n seenExp = true;\n sec.exp = true;\n }\n break;\n case 'comma': {\n const nextDigit = digitAfterCommas[i];\n if (seenDigit && nextDigit && !seenPoint && !seenExp) {\n sec.thousands = true;\n replace = { t: 'lit', s: '' };\n } else if (seenDigit && scientific) {\n // Observed: a scientific section never scales; any other comma after\n // the first digit shows nothing (`#,e+#`, `#e+#,`, `#e+000,00`).\n replace = { t: 'lit', s: '' };\n } else if (seenDigit && !nextDigit) {\n sec.scale += 1;\n replace = { t: 'lit', s: '' };\n } else {\n replace = { t: 'lit', s: ',' };\n }\n break;\n }\n case 'slash': {\n // Literal text may sit between the bar and the denominator (`#/ #`, `# ?=/=?`).\n const nextOk = toks[i + 1]?.t === 'denom' || firstNonLitAfter[i]?.t === 'digit';\n // An exponent kept before this bar or any marker still ahead of it.\n const hasExp = seenExp || lastExp > i;\n if (!fractionSlash && seenDigit && nextOk && !seenPoint && !hasExp) {\n fractionSlash = true;\n sec.fraction = true;\n } else {\n replace = { t: 'lit', s: '/' };\n }\n break;\n }\n case 'denom':\n if (!(toks[i - 1]?.t === 'slash' && sec.fraction)) replace = { t: 'lit', s: t.s };\n break;\n default:\n break;\n }\n if (replace) toks[i] = replace;\n }\n if (sec.fraction) {\n // A decimal point in a fraction section is literal.\n for (let i = 0; i < toks.length; i++) if (toks[i].t === 'point') toks[i] = { t: 'lit', s: '.' };\n }\n // Merge literals (removed commas left empty ones).\n const out: Tok[] = [];\n for (const t of toks) {\n if (t.t === 'lit') pushLit(out, t.s);\n else out.push(t);\n }\n sec.toks = out;\n return sec;\n}\n\n/** Parses a whole format code into its sections. */\nexport function parseSections(code: string): Section[] {\n return splitSections(code).map((s) => analyze(tokenize(s)));\n}\n","// Portions ported from gridcraft/crates/numfmt/src/render.rs and gridcraft/crates/core/src/date.rs\n// (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/** Rendering a value with a parsed section. */\nimport { decimalDigits, fixed, fixedFromDigits, formatGeneralFit } from './decimal';\nimport { isCalendarPart, literalText, type Section, type Tok } from './parse';\n\n/** Last serial that is a valid date in the 1900 date system: 9999-12-31. */\nexport const MAX_DATE_SERIAL = 2_958_465;\n\nconst MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];\nconst WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\nconst UNIX_1899_12_30 = -25569;\n\n/** Civil date for days since 1970-01-01 (Howard Hinnant's algorithm). */\nfunction civilFromDays(z0: number): { y: number; m: number; d: number } {\n const z = z0 + 719468;\n const era = Math.floor(z / 146097);\n const doe = z - era * 146097;\n const yoe = Math.floor((doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / 365);\n const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100));\n const mp = Math.floor((5 * doy + 2) / 153);\n const d = doy - Math.floor((153 * mp + 2) / 5) + 1;\n const m = mp < 10 ? mp + 3 : mp - 9;\n return { y: yoe + era * 400 + (m <= 2 ? 1 : 0), m, d };\n}\n\n/**\n * Year, month, day and weekday (0 = Sunday) of a whole 1900-system serial.\n * Serial 0 is the fictitious 1900-01-00 and serial 60 the fictitious\n * 1900-02-29; weekdays continue that calendar (serial 1 is a Sunday).\n */\nexport function dateFromSerial(day: number): { year: number; month: number; day: number; weekday: number } {\n const weekday = (((day + 6) % 7) + 7) % 7;\n if (day === 60) return { year: 1900, month: 2, day: 29, weekday };\n if (day === 0) return { year: 1900, month: 1, day: 0, weekday };\n const real = day < 60 ? day + 1 : day;\n const { y, m, d } = civilFromDays(real + UNIX_1899_12_30);\n return { year: y, month: m, day: d, weekday };\n}\n\n/** Text being built plus the first repeat-fill position. */\nexport class Out {\n text = '';\n fill: { char: string; index: number } | undefined;\n /** True when every digit shown is zero (a number section that rounded to zero). */\n zero = false;\n push(s: string): void {\n this.text += s;\n }\n setFill(c: string): void {\n if (!this.fill) this.fill = { char: c, index: this.text.length };\n }\n prepend(s: string): void {\n this.text = s + this.text;\n if (this.fill) this.fill.index += s.length;\n }\n}\n\n/** Renders a text value through a text section (`@` is replaced by the text). */\nexport function renderText(sec: Section, text: string): Out {\n const out = new Out();\n for (const t of sec.toks) {\n if (t.t === 'at') out.push(text);\n else if (t.t === 'fill') out.setFill(t.c);\n else out.push(literalText(t));\n }\n return out;\n}\n\n/** Renders a number through a General section (literals around the General keyword). */\nexport function renderGeneral(sec: Section, v: number, width: number): Out {\n const out = new Out();\n for (const t of sec.toks) {\n if (t.t === 'general') out.push(formatGeneralFit(v, width) ?? '#'.repeat(Math.min(Math.max(width, 1), 11)));\n else if (t.t === 'fill') out.setFill(t.c);\n else out.push(literalText(t));\n }\n return out;\n}\n\nconst padNum = (n: number, w: number) => (w >= 2 ? String(n).padStart(Math.min(w, 20), '0') : String(n));\n\n/** Renders a date/time. `null` when the serial is outside the supported range. */\nexport function renderDate(sec: Section, v: number): Out | null {\n if (!Number.isFinite(v) || v < 0 || v >= MAX_DATE_SERIAL + 1) return null;\n let subDigits = 0;\n for (const t of sec.toks) if (t.t === 'date' && t.d.k === 'subsec') subDigits = Math.max(subDigits, t.d.n);\n subDigits = Math.min(subDigits, 3);\n const unit = 10 ** (3 - subDigits);\n let day = Math.floor(v);\n let ms = Math.round(((v - Math.floor(v)) * 86_400_000) / unit) * unit;\n if (ms >= 86_400_000) {\n ms -= 86_400_000;\n day += 1;\n }\n const totalMs = day * 86_400_000 + ms;\n const needsDate = sec.toks.some((t) => t.t === 'date' && isCalendarPart(t.d));\n if (needsDate && day > MAX_DATE_SERIAL) return null;\n const dt = needsDate ? dateFromSerial(day) : dateFromSerial(0);\n const ampm = sec.toks.some((t) => t.t === 'date' && (t.d.k === 'ampm' || t.d.k === 'ap'));\n const hour = Math.floor(ms / 3_600_000);\n const minute = Math.floor(ms / 60_000) % 60;\n const second = Math.floor(ms / 1000) % 60;\n const milli = ms % 1000;\n const monthName = MONTHS[dt.month - 1] ?? '';\n const dayName = WEEKDAYS[dt.weekday] ?? '';\n const out = new Out();\n for (const t of sec.toks) {\n if (t.t === 'fill') {\n out.setFill(t.c);\n continue;\n }\n if (t.t !== 'date') {\n out.push(literalText(t));\n continue;\n }\n const d = t.d;\n let s: string;\n switch (d.k) {\n case 'year':\n s = d.n <= 2 ? String(((dt.year % 100) + 100) % 100).padStart(2, '0') : String(dt.year).padStart(4, '0');\n break;\n case 'month':\n s = d.n <= 2 ? padNum(dt.month, d.n) : d.n === 3 ? monthName.slice(0, 3) : d.n === 5 ? monthName.slice(0, 1) : monthName;\n break;\n case 'day':\n s = d.n <= 2 ? padNum(dt.day, d.n) : d.n === 3 ? dayName.slice(0, 3) : dayName;\n break;\n case 'hour':\n s = padNum(ampm ? hour % 12 || 12 : hour, d.n);\n break;\n case 'minute':\n s = padNum(minute, d.n);\n break;\n case 'second':\n s = padNum(second, d.n);\n break;\n case 'subsec': {\n s = `.${String(milli).padStart(3, '0')}`.slice(0, 1 + Math.min(d.n, 3));\n if (d.n > 3) s += '0'.repeat(d.n - 3);\n break;\n }\n case 'eh':\n s = padNum(Math.floor(totalMs / 3_600_000), d.n);\n break;\n case 'em':\n s = padNum(Math.floor(totalMs / 60_000), d.n);\n break;\n case 'es':\n s = padNum(Math.floor(totalMs / 1000), d.n);\n break;\n case 'ampm':\n s = hour >= 12 ? 'PM' : 'AM';\n break;\n case 'ap':\n s = hour >= 12 ? d.p : d.a;\n break;\n }\n out.push(s);\n }\n return out;\n}\n\nconst digitChar = (toks: Tok[], i: number): string => {\n const t = toks[i];\n return t?.t === 'digit' ? t.c : '0';\n};\n\n/**\n * Assigns integer digits right-to-left to the placeholders at `idxs`; the\n * leftmost placeholder takes all remaining digits. `forceZero` shows `0` in\n * the units placeholder when there are no digits. A blank `?` pads with a\n * space, and so does the thousands separator it would carry.\n */\nfunction fillInt(toks: Tok[], idxs: number[], digits: string, thousands: boolean, forceZero: boolean, pieces: string[]): void {\n let rem = digits.length;\n let pos = 0;\n const count = idxs.length;\n for (let k = 0; k < count; k++) {\n const ti = idxs[count - 1 - k];\n const leftmost = k + 1 === count;\n const ph = digitChar(toks, ti);\n const rev: string[] = [];\n const pushDigit = (c: string) => {\n if (thousands && pos > 0 && pos % 3 === 0) rev.push(',');\n rev.push(c);\n pos += 1;\n };\n const take = leftmost ? rem : Math.min(rem, 1);\n if (take === 0) {\n if (ph === '0') pushDigit('0');\n else if (forceZero && k === 0) pushDigit('0');\n else if (ph === '?') {\n if (thousands && pos > 0 && pos % 3 === 0) rev.push(' ');\n rev.push(' ');\n pos += 1;\n }\n } else {\n for (let n = 0; n < take; n++) {\n rem -= 1;\n pushDigit(digits[rem] ?? '0');\n }\n }\n pieces[ti] = rev.reverse().join('');\n }\n}\n\nfunction withCommas(digits: string, thousands: boolean): string {\n if (!thousands) return digits;\n const n = digits.length;\n let s = '';\n for (let i = 0; i < n; i++) {\n if (i > 0 && (n - i) % 3 === 0) s += ',';\n s += digits[i];\n }\n return s;\n}\n\n/** Fraction-digit placeholders: trailing zeros show as `0`, nothing (`#`) or a space (`?`). */\nfunction fillFrac(toks: Tok[], idxs: number[], frac: string, pieces: string[]): void {\n let lastSig = -1;\n for (let i = frac.length - 1; i >= 0; i--) {\n if (frac[i] !== '0') {\n lastSig = i;\n break;\n }\n }\n idxs.forEach((ti, i) => {\n const ph = digitChar(toks, ti);\n pieces[ti] = i <= lastSig ? (frac[i] ?? '0') : ph === '0' ? '0' : ph === '?' ? ' ' : '';\n });\n}\n\nfunction digitIndices(toks: Tok[], start: number, end: number): number[] {\n const out: number[] = [];\n for (let i = start; i < end; i++) if (toks[i]?.t === 'digit') out.push(i);\n return out;\n}\n\n/** Renders a non-negative number through a number section. */\nexport function renderNumber(sec: Section, v: number): Out {\n let x = Math.abs(v);\n // Observed: `%` in a scientific section is only a literal (`#%e+#` shows 123456.789 as `1%e+5`).\n if (sec.percent > 0 && !sec.exp) x *= 100 ** Math.min(sec.percent, 20);\n if (sec.scale > 0) x /= 1000 ** Math.min(sec.scale, 110);\n if (!Number.isFinite(x)) x = Number.MAX_VALUE;\n const toks = sec.toks;\n const n = toks.length;\n // Pieces replace the token's own output; `before` is inserted ahead of a\n // token; `lits` replaces a literal's text (fractions blank hidden parts).\n const pieces: string[] = new Array(n).fill('');\n const before: string[] = new Array(n + 1).fill('');\n const lits: Array<string | undefined> = new Array(n);\n let zero = false;\n if (sec.fraction) {\n renderFraction(sec, x, pieces, lits);\n } else {\n const pointIdx = toks.findIndex((t) => t.t === 'point');\n const expIdx = toks.findIndex((t) => t.t === 'exp');\n const point = pointIdx === -1 ? undefined : pointIdx;\n const exp = expIdx === -1 ? undefined : expIdx;\n const intEnd = point ?? exp ?? n;\n const fracEnd = exp ?? n;\n const intIdx = digitIndices(toks, 0, intEnd);\n const fracIdx = point === undefined ? [] : digitIndices(toks, point + 1, fracEnd);\n let intS: string;\n let fracS: string;\n if (exp !== undefined) {\n const expDigitIdx = digitIndices(toks, exp + 1, n);\n const step = Math.max(intIdx.length, 1);\n let e: number;\n if (x === 0) {\n // Observed: zero shows `0` in every integer placeholder, `#` and `?` too.\n e = 0;\n intS = '0'.repeat(intIdx.length);\n fracS = '0'.repeat(fracIdx.length);\n } else {\n const { digits, exp: xe } = decimalDigits(x);\n e = xe - (((xe % step) + step) % step);\n [intS, fracS] = fixedFromDigits(digits, xe - e, fracIdx.length);\n if (intS.length > step) {\n e += step;\n [intS, fracS] = fixedFromDigits(digits, xe - e, fracIdx.length);\n }\n }\n const et = toks[exp];\n const upper = et.t === 'exp' ? et.upper : true;\n const plus = et.t === 'exp' ? et.plus : true;\n if (intS === '' && /^0*$/.test(fracS)) e = 0;\n const head = upper ? 'E' : 'e';\n const sign = e < 0 ? '-' : plus ? '+' : '';\n const expDigits = String(Math.abs(e));\n // The sign sits right before the exponent digits, after any literal that\n // follows the `E`; `?` pads the digits with spaces.\n if (expDigitIdx.length > 0) {\n pieces[exp] = head;\n before[expDigitIdx[0]] = sign;\n fillInt(toks, expDigitIdx, expDigits, false, false, pieces);\n } else {\n pieces[exp] = `${head}${sign}${expDigits}`;\n }\n } else {\n [intS, fracS] = fixed(x, fracIdx.length);\n }\n zero = /^0*$/.test(intS + fracS);\n fillInt(toks, intIdx, intS, sec.thousands, false, pieces);\n const anchor = point ?? exp;\n if (intIdx.length === 0 && intS !== '' && anchor !== undefined) before[anchor] = withCommas(intS, sec.thousands);\n fillFrac(toks, fracIdx, fracS, pieces);\n if (point !== undefined) pieces[point] = '.';\n }\n const out = new Out();\n for (let i = 0; i < n; i++) {\n out.push(before[i]);\n const t = toks[i];\n if (t.t === 'lit') out.push(lits[i] ?? t.s);\n else if (t.t === 'percent') out.push('%');\n else if (t.t === 'fill') out.setFill(t.c);\n else if (t.t === 'at' || t.t === 'general' || t.t === 'date' || t.t === 'comma') continue;\n else out.push(pieces[i]);\n }\n out.zero = zero;\n return out;\n}\n\n/** Best rational approximation of `x` with denominator at most `maxDen` (continued fractions). */\nexport function approximate(x: number, maxDen: number): [number, number] {\n if (Number.isNaN(x) || x <= 0 || !Number.isFinite(x)) return [0, 1];\n if (x >= 1e15) return [Math.round(x), 1];\n let [p0, q0, p1, q1] = [0, 1, 1, 0];\n let r = x;\n for (let iter = 0; iter < 64; iter++) {\n const aF = Math.floor(r);\n if (aF > 1e15) break;\n const q2 = aF * q1 + q0;\n if (q2 > maxDen) break;\n const p2 = aF * p1 + p0;\n [p0, q0, p1, q1] = [p1, q1, p2, q2];\n const frac = r - aF;\n if (frac < 1e-12) break;\n r = 1 / frac;\n }\n if (q1 === 0) return [Math.round(x), 1];\n // Semiconvergent bound.\n const k = Math.floor((maxDen - q0) / q1);\n const pb = p0 + k * p1;\n const qb = q0 + k * q1;\n const errA = Math.abs(p1 / q1 - x);\n const errB = qb > 0 ? Math.abs(pb / qb - x) : Infinity;\n return errB < errA ? [pb, qb] : [p1, q1];\n}\n\n/** Display width of a token in a fraction section, for blanking a hidden part with spaces. */\nfunction shownWidth(t: Tok): number {\n if (t.t === 'lit' || t.t === 'denom') return Array.from(t.s).length;\n return t.t === 'digit' || t.t === 'slash' ? 1 : 0;\n}\n\n/** Replaces the literals, placeholders and bar in `[start, end)` with spaces (`spaces`) or nothing. */\nfunction blankRange(toks: Tok[], start: number, end: number, spaces: boolean, pieces: string[], lits: Array<string | undefined>): void {\n for (let i = start; i < end && i < toks.length; i++) {\n const t = toks[i];\n const s = spaces ? ' '.repeat(shownWidth(t)) : '';\n if (t.t === 'lit') lits[i] = s;\n else if (t.t === 'digit' || t.t === 'slash' || t.t === 'denom') pieces[i] = s;\n }\n}\n\n/**\n * A fraction section is `[whole] [sep] num [lit] / [lit] den`: the numerator\n * is the run of placeholders ending at the last one before the bar, the whole\n * part is every placeholder before it, `sep` the literals between them.\n *\n * Observed Excel behaviour: when the whole part shows no digit its separator\n * disappears (or becomes spaces when the whole part or the numerator has a\n * `?`); when the fraction is zero and the numerator has no `0` placeholder,\n * the separator, numerator, bar and denominator disappear (or become spaces\n * when any part has a `?`) and the whole part shows at least `0`.\n */\nfunction renderFraction(sec: Section, x: number, pieces: string[], lits: Array<string | undefined>): void {\n const toks = sec.toks;\n const isDigit = (i: number) => toks[i]?.t === 'digit';\n const slash = toks.findIndex((t) => t.t === 'slash');\n if (slash === -1) return;\n let numLast = -1;\n for (let i = slash - 1; i >= 0; i--) {\n if (isDigit(i)) {\n numLast = i;\n break;\n }\n }\n if (numLast === -1) return;\n let numStart = numLast;\n while (numStart > 0 && isDigit(numStart - 1)) numStart -= 1;\n const wholeIdx = digitIndices(toks, 0, numStart);\n const numIdx = digitIndices(toks, numStart, numLast + 1);\n const sepStart = wholeIdx.length > 0 ? wholeIdx[wholeIdx.length - 1] + 1 : numStart;\n let fixedDen: number | undefined;\n let denStart: number;\n const afterSlash = toks[slash + 1];\n if (afterSlash?.t === 'denom') {\n const parsed = Number(afterSlash.s);\n fixedDen = Math.min(Math.max(Number.isFinite(parsed) ? parsed : 1e9, 1), 1_000_000_000);\n denStart = slash + 1;\n } else {\n denStart = slash + 1;\n while (toks[denStart]?.t === 'lit') denStart += 1;\n }\n let denEnd = denStart;\n if (fixedDen !== undefined) denEnd += 1;\n else while (isDigit(denEnd)) denEnd += 1;\n const denIdx = digitIndices(toks, denStart, denEnd);\n const hasQ = (idx: number[]) => idx.some((i) => isDigitTok(toks[i], '?'));\n const hasWhole = wholeIdx.length > 0;\n let whole = hasWhole ? Math.floor(x) : 0;\n const f = hasWhole ? x - Math.floor(x) : x;\n let num: number;\n let den: number;\n if (fixedDen !== undefined) {\n num = Math.round(f * fixedDen);\n den = fixedDen;\n } else {\n const digits = Math.min(Math.max(denIdx.length, 1), 7);\n [num, den] = approximate(f, 10 ** digits - 1);\n }\n if (hasWhole && num >= den && den > 0) {\n whole += Math.floor(num / den);\n num %= den;\n }\n const wholeS = whole >= 1 ? fixed(whole, 0)[0] : '';\n const numZeroPh = numIdx.some((i) => isDigitTok(toks[i], '0'));\n if (hasWhole && num === 0 && !numZeroPh) {\n fillInt(toks, wholeIdx, wholeS, sec.thousands, true, pieces);\n const spaces = hasQ(wholeIdx) || hasQ(numIdx) || hasQ(denIdx);\n blankRange(toks, sepStart, denEnd, spaces, pieces, lits);\n return;\n }\n if (hasWhole) {\n // A zero value shows `0` in a `?` or `0` units place, nothing in a `#` one.\n const unitsHash = isDigitTok(toks[wholeIdx[wholeIdx.length - 1]], '#');\n const force = wholeS === '' && num === 0 && !unitsHash;\n fillInt(toks, wholeIdx, wholeS, sec.thousands, force, pieces);\n const wholeShown = wholeIdx.some((i) => /[0-9]/.test(pieces[i] ?? ''));\n if (!wholeShown) {\n const spaces = hasQ(wholeIdx) || hasQ(numIdx);\n blankRange(toks, sepStart, numStart, spaces, pieces, lits);\n }\n }\n fillInt(toks, numIdx, fixed(num, 0)[0] || '0', false, true, pieces);\n pieces[slash] = '/';\n const denS = fixed(den, 0)[0] || '0';\n if (fixedDen !== undefined) {\n pieces[denStart] = denS;\n } else {\n // Denominator digits are zero padded to the `0` placeholders, then left\n // aligned; `?` pads with spaces after them.\n const zeros = denIdx.filter((i) => isDigitTok(toks[i], '0')).length;\n const padded = '0'.repeat(Math.max(zeros - denS.length, 0)) + denS;\n const count = denIdx.length;\n denIdx.forEach((ti, k) => {\n let s = k + 1 === count ? padded.slice(k) : (padded[k] ?? '');\n if (s === '') s = isDigitTok(toks[ti], '?') ? ' ' : isDigitTok(toks[ti], '0') ? '0' : '';\n pieces[ti] = s;\n });\n }\n}\n\nfunction isDigitTok(t: Tok | undefined, c: string): boolean {\n return t?.t === 'digit' && t.c === c;\n}\n","// Portions ported from gridcraft/crates/numfmt/src/lib.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/**\n * Excel number formats: format codes (`#,##0.00;[Red](#,##0.00)`, dates,\n * fractions, scientific…), the General format and the `TEXT()` worksheet\n * function. en-US only, 1900 date system.\n *\n * Our engine keeps dates as ISO text (`2026-03-04`, `2026-03-04 18:00:00`) and\n * times as day fractions; ISO text is converted to a 1900-system serial number\n * before formatting.\n */\nimport { parseNumberText, serialFromYmd, daysInMonth, timeFraction } from '../formula/parse-input';\nimport { FORMULA_ERRORS } from '../formula/values';\nimport { formatGeneralFit } from './decimal';\nimport { parseSections, testCond, type NumberFormatColor, type Section } from './parse';\nimport { Out, renderDate, renderGeneral, renderNumber, renderText } from './render';\n\nexport { formatGeneralFit } from './decimal';\nexport { builtinNumberFormat, builtinNumberFormatId } from './builtin';\nexport type { NumberFormatColor } from './parse';\n\n/** Width (in characters) of General output, as in a default-width cell. */\nconst GENERAL_WIDTH = 11;\n/** What Excel paints when a date is out of range. */\nconst OVERFLOW = '########';\n\n/**\n * Longest number format code, in characters, that is parsed (Excel's own\n * limit). A longer code displays its value as General and makes `TEXT`\n * return `#VALUE!`. The cap also bounds work that scales with the code.\n */\nexport const MAX_NUMBER_FORMAT_CODE_CHARS = 255;\n\n/** Best guess at the ribbon's number-format category. */\nexport type NumberFormatKind =\n | 'general'\n | 'number'\n | 'currency'\n | 'accounting'\n | 'date'\n | 'time'\n | 'percentage'\n | 'fraction'\n | 'scientific'\n | 'text'\n /** Zip codes, phone numbers, social security numbers. */\n | 'special'\n | 'custom';\n\n/** A value formatted with a number format, ready to paint. */\nexport interface FormattedNumberValue {\n text: string;\n /** Colour from a `[Red]` / `[ColorN]` tag of the section used. */\n color?: NumberFormatColor;\n /**\n * Repeat-fill character from `*x` and its position (UTF-16 index) in\n * `text`; a renderer may repeat it to fill the cell width.\n */\n fill?: { char: string; index: number };\n /** True for numbers (right-aligned by default); text, booleans and errors are false. */\n numeric: boolean;\n}\n\n/** True when `code` is longer than `MAX_NUMBER_FORMAT_CODE_CHARS` characters. */\nfunction tooLong(code: string): boolean {\n // A code of more than 255 UTF-16 units can still be short in characters; count only when needed.\n return code.length > MAX_NUMBER_FORMAT_CODE_CHARS && Array.from(code).length > MAX_NUMBER_FORMAT_CODE_CHARS;\n}\n\nconst SPECIAL_CODES = ['00000', '00000-0000', '[<=9999999]###-####;(###) ###-####', '000-00-0000'];\n\n/** A parsed number format code. Parsing never fails: malformed parts become literal text. */\nexport class NumberFormat {\n readonly code: string;\n private readonly general: boolean;\n private readonly sections: Section[];\n /** Sections that apply to numbers (a trailing text section is excluded). */\n private readonly numSections: number;\n /** Index of the section that applies to text, if any. */\n private readonly textSection: number | undefined;\n\n private constructor(code: string, uncapped: boolean) {\n this.code = code;\n const trimmed = code.trim();\n // `checkbox` marks a checkbox cell: any value displays as General.\n if (trimmed === '' || trimmed.toLowerCase() === 'general' || trimmed === 'checkbox' || (!uncapped && tooLong(code))) {\n this.general = true;\n this.sections = [];\n this.numSections = 0;\n this.textSection = undefined;\n return;\n }\n this.general = false;\n this.sections = parseSections(code);\n const len = this.sections.length;\n const lastIsText = this.sections[len - 1]?.kind === 'text';\n this.textSection = len >= 4 ? 3 : lastIsText ? len - 1 : undefined;\n this.numSections = len >= 4 ? 3 : lastIsText ? len - 1 : len;\n }\n\n /**\n * Parses a format code. Never throws: unknown or malformed parts degrade to\n * literal text, and a code longer than 255 characters degrades to General.\n */\n static parse(code: string): NumberFormat {\n return new NumberFormat(code, false);\n }\n\n /** `parse` without the 255-character cap (for tests that reach renderer edge cases). */\n static parseUncapped(code: string): NumberFormat {\n return new NumberFormat(code, true);\n }\n\n /** Like `parse`, but remembers recently parsed codes (bounded), so formatting many values parses a code once. */\n static parseCached(code: string): NumberFormat {\n if (code.length > CACHE_MAX_CODE_LENGTH) return NumberFormat.parse(code);\n let fmt = parseCache.get(code);\n if (!fmt) {\n fmt = NumberFormat.parse(code);\n if (parseCache.size >= CACHE_CAPACITY) parseCache.clear();\n parseCache.set(code, fmt);\n }\n return fmt;\n }\n\n static general(): NumberFormat {\n return NumberFormat.parse('General');\n }\n\n isGeneral(): boolean {\n return this.general;\n }\n\n private numberSections(): Section[] {\n return this.sections.slice(0, this.numSections);\n }\n\n /** True when the code contains date or time tokens. */\n isDate(): boolean {\n return this.numberSections().some((s) => s.kind === 'date');\n }\n\n isPercent(): boolean {\n return this.numberSections().some((s) => s.kind === 'number' && s.percent > 0);\n }\n\n /** True for a pure text format such as `@`. */\n isText(): boolean {\n return this.numSections === 0 && this.textSection !== undefined;\n }\n\n /** Best guess at the format category. */\n kind(): NumberFormatKind {\n if (this.general) return 'general';\n if (this.isText()) return 'text';\n if (SPECIAL_CODES.includes(this.code)) return 'special';\n const s = this.sections[0];\n if (!s) return 'custom';\n if (s.kind === 'general') return 'general';\n if (s.kind === 'text') return 'text';\n if (s.kind === 'date') {\n const calendar = s.toks.some((t) => t.t === 'date' && (t.d.k === 'year' || t.d.k === 'month' || t.d.k === 'day'));\n return calendar ? 'date' : 'time';\n }\n if (s.fraction) return 'fraction';\n if (s.exp) return 'scientific';\n if (s.percent > 0) return 'percentage';\n if (s.fill && s.hasDigits) return 'accounting';\n if (s.currency && s.hasDigits) return 'currency';\n if (s.hasDigits) return 'number';\n return 'custom';\n }\n\n /**\n * Picks the number section for `v`, and whether the minus sign is dropped\n * (a negative section shows the absolute value). Null means General.\n */\n private select(v: number): { sec: Section; dropSign: boolean } | null {\n const secs = this.numberSections();\n const k = secs.length;\n const s0 = secs[0];\n if (!s0) return null;\n const c0 = s0.cond;\n const c1 = secs[1]?.cond;\n if (c0 || c1) {\n let idx: number;\n if (c0 && testCond(c0, v)) idx = 0;\n else if (c1 && testCond(c1, v)) idx = 1;\n else if (c0 && c1) idx = 2;\n else if (!c0) idx = 0;\n else idx = 1;\n // Excel never adds a minus sign in a multi-section conditional format.\n const sec = secs[idx];\n return sec ? { sec, dropSign: k >= 2 } : null;\n }\n if (v < 0 && k >= 2) return { sec: secs[1], dropSign: true };\n if (v === 0 && k >= 3) return { sec: secs[2], dropSign: false };\n return { sec: s0, dropSign: false };\n }\n\n /**\n * Formats a number. `overflow` is true when the value can't be shown (a\n * date out of range, painted as `########`).\n */\n formatNumber(n: number): FormattedNumberValue & { overflow: boolean } {\n const general = (): FormattedNumberValue & { overflow: boolean } => ({\n text: formatGeneralFit(n, GENERAL_WIDTH) ?? OVERFLOW,\n numeric: true,\n overflow: false,\n });\n if (!Number.isFinite(n)) return { text: '#NUM!', numeric: false, overflow: true };\n if (this.general) return general();\n const picked = this.select(n);\n if (!picked) return general();\n const { sec, dropSign } = picked;\n const minus = n < 0 && !dropSign;\n let out: Out | null;\n switch (sec.kind) {\n case 'text':\n out = renderText(sec, formatGeneralFit(n, GENERAL_WIDTH) ?? '');\n break;\n case 'general':\n out = renderGeneral(sec, Math.abs(n), minus ? GENERAL_WIDTH - 1 : GENERAL_WIDTH);\n break;\n case 'date':\n out = minus ? null : renderDate(sec, Math.abs(n));\n break;\n default:\n out = renderNumber(sec, Math.abs(n));\n }\n if (!out) return withColor({ text: OVERFLOW, numeric: true, overflow: true }, sec.color);\n // Excel drops the minus sign when the section shows a negative number as all zeros.\n if (minus && sec.kind !== 'text' && !out.zero) out.prepend('-');\n return withColor({ text: out.text, numeric: true, overflow: false, ...(out.fill ? { fill: out.fill } : {}) }, sec.color);\n }\n\n /** Formats text through the text section (`@`), or returns it unchanged. */\n formatText(text: string): FormattedNumberValue {\n const sec = this.textSection === undefined ? undefined : this.sections[this.textSection];\n if (!sec) return { text, numeric: false };\n const out = renderText(sec, text);\n return withColor({ text: out.text, numeric: false, ...(out.fill ? { fill: out.fill } : {}) }, sec.color);\n }\n\n /**\n * Formats a cell value: numbers through the number sections, ISO date text\n * (`2026-03-04`, `2026-03-04 18:00:00`) and `Date`s as serial numbers, other\n * text through the text section; booleans show TRUE/FALSE and error values\n * themselves.\n */\n format(value: unknown): FormattedNumberValue {\n if (value === '' || value === null || value === undefined) return { text: '', numeric: false };\n if (typeof value === 'boolean') return { text: value ? 'TRUE' : 'FALSE', numeric: false };\n if (typeof value === 'number') {\n const { overflow: _overflow, ...rest } = this.formatNumber(value);\n return rest;\n }\n if (typeof value === 'string' && FORMULA_ERRORS.has(value)) return { text: value, numeric: false };\n const serial = dateValueToSerial(value);\n if (serial !== null) {\n const { overflow: _overflow, ...rest } = this.formatNumber(serial);\n return rest;\n }\n return this.formatText(String(value));\n }\n}\n\nfunction withColor<T extends FormattedNumberValue>(value: T, color: NumberFormatColor | undefined): T {\n if (color) value.color = color;\n return value;\n}\n\n// --- Parse cache ---\n\n/** Most codes kept. A full cache is emptied, so memory stays bounded. */\nconst CACHE_CAPACITY = 64;\n/** Longer codes are parsed every time and never stored. */\nconst CACHE_MAX_CODE_LENGTH = 256;\nconst parseCache = new Map<string, NumberFormat>();\n\n/** Empties the parse cache used by `NumberFormat.parseCached` (mainly for tests). */\nexport function clearNumberFormatCache(): void {\n parseCache.clear();\n}\n\n// --- Dates ---\n\nconst ISO_DATE_TIME = /^(\\d{4})-(\\d{1,2})-(\\d{1,2})(?:[ T](\\d{1,2}):(\\d{2})(?::(\\d{2}(?:\\.\\d+)?))?)?$/;\n\n/**\n * 1900-system serial number of a date value: ISO date text (with an optional\n * time) or a `Date` (its local date and time). Null for anything else.\n */\nexport function dateValueToSerial(value: unknown): number | null {\n if (value instanceof Date) {\n if (isNaN(value.getTime())) return null;\n const serial = serialFromYmd(value.getFullYear(), value.getMonth() + 1, value.getDate());\n return serial === null ? null : serial + timeFraction(value.getHours(), value.getMinutes(), value.getSeconds() + value.getMilliseconds() / 1000);\n }\n if (typeof value !== 'string' || value.length < 8 || value.length > 30) return null;\n const m = ISO_DATE_TIME.exec(value.trim());\n if (!m) return null;\n const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];\n if (mo < 1 || mo > 12 || d < 1 || d > daysInMonth(y, mo)) return null;\n const serial = serialFromYmd(y, mo, d);\n if (serial === null) return null;\n if (m[4] === undefined) return serial;\n const [hh, mm, ss] = [Number(m[4]), Number(m[5]), Number(m[6] ?? 0)];\n if (hh > 23 || mm > 59 || ss >= 60) return null;\n return serial + timeFraction(hh, mm, ss);\n}\n\n// --- Public helpers ---\n\n/** Formats a cell value with a format code (see `NumberFormat.format`). */\nexport function formatNumberFormatValue(value: unknown, code: string): FormattedNumberValue {\n return NumberFormat.parseCached(code).format(value);\n}\n\n/**\n * The `TEXT()` worksheet function: formats a value with a format code.\n * Blanks count as 0; numeric and date text (`\"5\"`, `\"3/4/2026\"`,\n * `\"2026-03-04\"`) is converted to a number first; other text and booleans\n * (as TRUE/FALSE) go through the text section, or are returned as is.\n * Errors: the value's own error, `#VALUE!` for a code over 255 characters or\n * a date out of range, `#NUM!` for a non-finite number.\n */\nexport function formatTextFunction(value: unknown, code: string): { text: string } | { error: string } {\n if (typeof value === 'string' && FORMULA_ERRORS.has(value)) return { error: value };\n if (code === '') return { text: '' };\n if (tooLong(code)) return { error: '#VALUE!' };\n const fmt = NumberFormat.parseCached(code);\n let n: number;\n if (value === '' || value === null || value === undefined) n = 0;\n else if (typeof value === 'number') n = value;\n else if (typeof value === 'boolean') return { text: fmt.formatText(value ? 'TRUE' : 'FALSE').text };\n else {\n const serial = dateValueToSerial(value);\n const parsed = serial ?? (typeof value === 'string' ? parseNumberText(value) : null);\n if (parsed === null) return { text: fmt.formatText(String(value)).text };\n n = parsed;\n }\n if (!Number.isFinite(n)) return { error: '#NUM!' };\n const f = fmt.formatNumber(n);\n return f.overflow ? { error: '#VALUE!' } : { text: f.text };\n}\n\n/** Excel's default 56-colour palette, for `[Color1]`…`[Color56]`. */\nconst INDEXED_COLORS = [\n '#000000', '#FFFFFF', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF',\n '#800000', '#008000', '#000080', '#808000', '#800080', '#008080', '#C0C0C0', '#808080',\n '#9999FF', '#993366', '#FFFFCC', '#CCFFFF', '#660066', '#FF8080', '#0066CC', '#CCCCFF',\n '#000080', '#FF00FF', '#FFFF00', '#00FFFF', '#800080', '#800000', '#008080', '#0000FF',\n '#00CCFF', '#CCFFFF', '#CCFFCC', '#FFFF99', '#99CCFF', '#FF99CC', '#CC99FF', '#FFCC99',\n '#3366FF', '#33CCCC', '#99CC00', '#FFCC00', '#FF9900', '#FF6600', '#666699', '#969696',\n '#003366', '#339966', '#003300', '#333300', '#993300', '#993366', '#333399', '#333333',\n];\n\nconst NAMED_CSS: Record<string, string> = {\n black: '#000000',\n blue: '#0000FF',\n cyan: '#00FFFF',\n green: '#00FF00',\n magenta: '#FF00FF',\n red: '#FF0000',\n white: '#FFFFFF',\n yellow: '#FFFF00',\n};\n\n/** CSS colour for a format colour (`red` → `#FF0000`, `color10` → `#008000`). */\nexport function numberFormatColorToCss(color: NumberFormatColor): string {\n const named = NAMED_CSS[color];\n if (named) return named;\n return INDEXED_COLORS[Number(color.slice(5)) - 1] ?? '#000000';\n}\n"]}
|