@cj-tech-master/excelts 9.3.1 → 9.4.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.
Files changed (109) hide show
  1. package/dist/browser/index.d.ts +1 -0
  2. package/dist/browser/index.js +2 -0
  3. package/dist/browser/modules/excel/cell.d.ts +18 -0
  4. package/dist/browser/modules/excel/cell.js +21 -0
  5. package/dist/browser/modules/excel/utils/cell-format.js +85 -13
  6. package/dist/browser/modules/excel/workbook.browser.d.ts +57 -0
  7. package/dist/browser/modules/excel/workbook.browser.js +49 -0
  8. package/dist/browser/modules/excel/xlsx/defaultnumformats.js +3 -3
  9. package/dist/browser/modules/formula/compile/binder.js +48 -6
  10. package/dist/browser/modules/formula/compile/bound-ast.d.ts +16 -2
  11. package/dist/browser/modules/formula/compile/bound-ast.js +1 -0
  12. package/dist/browser/modules/formula/compile/compiled-formula.js +41 -8
  13. package/dist/browser/modules/formula/functions/_shared.d.ts +19 -0
  14. package/dist/browser/modules/formula/functions/_shared.js +47 -0
  15. package/dist/browser/modules/formula/functions/conditional.js +103 -22
  16. package/dist/browser/modules/formula/functions/date.js +105 -23
  17. package/dist/browser/modules/formula/functions/dynamic-array.js +173 -69
  18. package/dist/browser/modules/formula/functions/engineering.d.ts +2 -2
  19. package/dist/browser/modules/formula/functions/engineering.js +103 -151
  20. package/dist/browser/modules/formula/functions/financial.js +210 -184
  21. package/dist/browser/modules/formula/functions/lookup.js +224 -157
  22. package/dist/browser/modules/formula/functions/math.d.ts +26 -0
  23. package/dist/browser/modules/formula/functions/math.js +249 -69
  24. package/dist/browser/modules/formula/functions/statistical.js +221 -171
  25. package/dist/browser/modules/formula/functions/text.js +112 -52
  26. package/dist/browser/modules/formula/integration/calculate-formulas-impl.js +20 -1
  27. package/dist/browser/modules/formula/materialize/build-writeback-plan.js +10 -6
  28. package/dist/browser/modules/formula/materialize/types.d.ts +15 -0
  29. package/dist/browser/modules/formula/runtime/evaluator.d.ts +8 -0
  30. package/dist/browser/modules/formula/runtime/evaluator.js +582 -162
  31. package/dist/browser/modules/formula/runtime/function-registry.d.ts +5 -0
  32. package/dist/browser/modules/formula/runtime/function-registry.js +59 -13
  33. package/dist/browser/modules/formula/runtime/values.d.ts +13 -0
  34. package/dist/browser/modules/formula/runtime/values.js +20 -2
  35. package/dist/browser/modules/formula/syntax/ast.d.ts +14 -2
  36. package/dist/browser/modules/formula/syntax/ast.js +1 -0
  37. package/dist/browser/modules/formula/syntax/parser.js +29 -7
  38. package/dist/browser/modules/formula/syntax/token-types.d.ts +4 -0
  39. package/dist/browser/modules/formula/syntax/token-types.js +9 -0
  40. package/dist/browser/modules/formula/syntax/tokenizer.js +76 -19
  41. package/dist/cjs/index.js +7 -2
  42. package/dist/cjs/modules/excel/cell.js +21 -0
  43. package/dist/cjs/modules/excel/utils/cell-format.js +85 -13
  44. package/dist/cjs/modules/excel/workbook.browser.js +49 -0
  45. package/dist/cjs/modules/excel/xlsx/defaultnumformats.js +3 -3
  46. package/dist/cjs/modules/formula/compile/binder.js +48 -6
  47. package/dist/cjs/modules/formula/compile/compiled-formula.js +41 -8
  48. package/dist/cjs/modules/formula/functions/_shared.js +48 -0
  49. package/dist/cjs/modules/formula/functions/conditional.js +103 -22
  50. package/dist/cjs/modules/formula/functions/date.js +104 -22
  51. package/dist/cjs/modules/formula/functions/dynamic-array.js +173 -69
  52. package/dist/cjs/modules/formula/functions/engineering.js +109 -157
  53. package/dist/cjs/modules/formula/functions/financial.js +209 -183
  54. package/dist/cjs/modules/formula/functions/lookup.js +224 -157
  55. package/dist/cjs/modules/formula/functions/math.js +254 -70
  56. package/dist/cjs/modules/formula/functions/statistical.js +222 -172
  57. package/dist/cjs/modules/formula/functions/text.js +112 -52
  58. package/dist/cjs/modules/formula/integration/calculate-formulas-impl.js +20 -1
  59. package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +10 -6
  60. package/dist/cjs/modules/formula/runtime/evaluator.js +581 -161
  61. package/dist/cjs/modules/formula/runtime/function-registry.js +57 -11
  62. package/dist/cjs/modules/formula/runtime/values.js +21 -2
  63. package/dist/cjs/modules/formula/syntax/parser.js +29 -7
  64. package/dist/cjs/modules/formula/syntax/token-types.js +9 -0
  65. package/dist/cjs/modules/formula/syntax/tokenizer.js +76 -19
  66. package/dist/esm/index.js +2 -0
  67. package/dist/esm/modules/excel/cell.js +21 -0
  68. package/dist/esm/modules/excel/utils/cell-format.js +85 -13
  69. package/dist/esm/modules/excel/workbook.browser.js +49 -0
  70. package/dist/esm/modules/excel/xlsx/defaultnumformats.js +3 -3
  71. package/dist/esm/modules/formula/compile/binder.js +48 -6
  72. package/dist/esm/modules/formula/compile/bound-ast.js +1 -0
  73. package/dist/esm/modules/formula/compile/compiled-formula.js +41 -8
  74. package/dist/esm/modules/formula/functions/_shared.js +47 -0
  75. package/dist/esm/modules/formula/functions/conditional.js +103 -22
  76. package/dist/esm/modules/formula/functions/date.js +105 -23
  77. package/dist/esm/modules/formula/functions/dynamic-array.js +173 -69
  78. package/dist/esm/modules/formula/functions/engineering.js +103 -151
  79. package/dist/esm/modules/formula/functions/financial.js +210 -184
  80. package/dist/esm/modules/formula/functions/lookup.js +224 -157
  81. package/dist/esm/modules/formula/functions/math.js +249 -69
  82. package/dist/esm/modules/formula/functions/statistical.js +221 -171
  83. package/dist/esm/modules/formula/functions/text.js +112 -52
  84. package/dist/esm/modules/formula/integration/calculate-formulas-impl.js +20 -1
  85. package/dist/esm/modules/formula/materialize/build-writeback-plan.js +10 -6
  86. package/dist/esm/modules/formula/runtime/evaluator.js +582 -162
  87. package/dist/esm/modules/formula/runtime/function-registry.js +59 -13
  88. package/dist/esm/modules/formula/runtime/values.js +20 -2
  89. package/dist/esm/modules/formula/syntax/ast.js +1 -0
  90. package/dist/esm/modules/formula/syntax/parser.js +29 -7
  91. package/dist/esm/modules/formula/syntax/token-types.js +9 -0
  92. package/dist/esm/modules/formula/syntax/tokenizer.js +76 -19
  93. package/dist/iife/excelts.iife.js +1502 -1379
  94. package/dist/iife/excelts.iife.js.map +1 -1
  95. package/dist/iife/excelts.iife.min.js +26 -26
  96. package/dist/types/index.d.ts +1 -0
  97. package/dist/types/modules/excel/cell.d.ts +18 -0
  98. package/dist/types/modules/excel/workbook.browser.d.ts +57 -0
  99. package/dist/types/modules/formula/compile/bound-ast.d.ts +16 -2
  100. package/dist/types/modules/formula/functions/_shared.d.ts +19 -0
  101. package/dist/types/modules/formula/functions/engineering.d.ts +2 -2
  102. package/dist/types/modules/formula/functions/math.d.ts +26 -0
  103. package/dist/types/modules/formula/materialize/types.d.ts +15 -0
  104. package/dist/types/modules/formula/runtime/evaluator.d.ts +8 -0
  105. package/dist/types/modules/formula/runtime/function-registry.d.ts +5 -0
  106. package/dist/types/modules/formula/runtime/values.d.ts +13 -0
  107. package/dist/types/modules/formula/syntax/ast.d.ts +14 -2
  108. package/dist/types/modules/formula/syntax/token-types.d.ts +4 -0
  109. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @cj-tech-master/excelts v9.3.1
2
+ * @cj-tech-master/excelts v9.4.0
3
3
  * Zero-dependency TypeScript toolkit — Excel (XLSX), PDF, CSV, Markdown, XML, ZIP/TAR, and streaming.
4
4
  * (c) 2026 cjnoname
5
5
  * Released under the MIT License
@@ -17468,6 +17468,1043 @@ onmessage = async (ev) => {
17468
17468
  }
17469
17469
  };
17470
17470
  //#endregion
17471
+ //#region src/utils/env.ts
17472
+ /**
17473
+ * Environment detection utilities
17474
+ * Common functions to detect runtime environment (Node.js vs Browser)
17475
+ */
17476
+ /**
17477
+ * Check if running in Node.js environment
17478
+ * Returns true if process.versions.node exists
17479
+ */
17480
+ function isNode() {
17481
+ return typeof process !== "undefined" && !!process.versions?.node;
17482
+ }
17483
+ //#endregion
17484
+ //#region src/modules/xml/encode.ts
17485
+ /**
17486
+ * XML Encoding / Decoding Utilities
17487
+ *
17488
+ * Self-contained XML entity encoding and decoding functions.
17489
+ */
17490
+ /** Standard XML entity decode map. */
17491
+ const DECODE_MAP = {
17492
+ lt: "<",
17493
+ gt: ">",
17494
+ amp: "&",
17495
+ quot: "\"",
17496
+ apos: "'"
17497
+ };
17498
+ /** Regex for decoding XML entities (named + numeric). */
17499
+ const DECODE_RE = /&(#\d+|#[xX][0-9A-Fa-f]+|\w+);/g;
17500
+ /**
17501
+ * Lookup table for characters that need encoding in the ASCII range (0-127).
17502
+ * 0 = safe, 1 = encode to entity, 2 = strip (invalid control char)
17503
+ */
17504
+ const ENCODE_ACTION = /* @__PURE__ */ (() => {
17505
+ const t = new Uint8Array(128);
17506
+ for (let i = 0; i <= 8; i++) t[i] = 2;
17507
+ t[11] = 2;
17508
+ t[12] = 2;
17509
+ for (let i = 14; i <= 31; i++) t[i] = 2;
17510
+ t[127] = 2;
17511
+ t[34] = 1;
17512
+ t[38] = 1;
17513
+ t[39] = 1;
17514
+ t[60] = 1;
17515
+ t[62] = 1;
17516
+ return t;
17517
+ })();
17518
+ const ENCODE_ENTITIES = {
17519
+ 34: "&quot;",
17520
+ 38: "&amp;",
17521
+ 39: "&apos;",
17522
+ 60: "&lt;",
17523
+ 62: "&gt;"
17524
+ };
17525
+ /**
17526
+ * Decode XML entities in a string.
17527
+ *
17528
+ * Handles named entities (`&lt;`, `&gt;`, `&amp;`, `&quot;`, `&apos;`)
17529
+ * and numeric character references (`&#123;`, `&#x7B;`).
17530
+ *
17531
+ * Security: validates numeric code points are in range [1, 0x10FFFF]
17532
+ * and rejects surrogate halves (0xD800-0xDFFF).
17533
+ *
17534
+ * Fast-path: returns the original string if no `&` is found.
17535
+ */
17536
+ function xmlDecode(text) {
17537
+ if (text.indexOf("&") === -1) return text;
17538
+ return text.replace(DECODE_RE, (match, entity) => {
17539
+ if (entity[0] === "#") {
17540
+ const code = entity[1] === "x" || entity[1] === "X" ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10);
17541
+ if (Number.isNaN(code) || code < 1 || code >= 55296 && code <= 57343 || code > 1114111) return match;
17542
+ return String.fromCodePoint(code);
17543
+ }
17544
+ return DECODE_MAP[entity] ?? match;
17545
+ });
17546
+ }
17547
+ /**
17548
+ * Encode special characters for XML output.
17549
+ *
17550
+ * Escapes `<`, `>`, `&`, `"`, `'` to their entity equivalents.
17551
+ * Strips invalid XML control characters (0x00-0x08, 0x0B-0x0C, 0x0E-0x1F, 0x7F)
17552
+ * and lone surrogates (0xD800-0xDFFF without a pair).
17553
+ *
17554
+ * Optimized: uses a lookup table and manual scan instead of regex for
17555
+ * maximum throughput on the hot path (called per attribute/text value).
17556
+ */
17557
+ function xmlEncode(text) {
17558
+ const len = text.length;
17559
+ let firstBad = -1;
17560
+ for (let i = 0; i < len; i++) {
17561
+ const code = text.charCodeAt(i);
17562
+ if (code < 128) {
17563
+ if (ENCODE_ACTION[code] !== 0) {
17564
+ firstBad = i;
17565
+ break;
17566
+ }
17567
+ } else if (code >= 55296 && code <= 57343) {
17568
+ if (code <= 56319) {
17569
+ const next = text.charCodeAt(i + 1);
17570
+ if (next >= 56320 && next <= 57343) {
17571
+ i++;
17572
+ continue;
17573
+ }
17574
+ }
17575
+ firstBad = i;
17576
+ break;
17577
+ } else if (code === 65534 || code === 65535) {
17578
+ firstBad = i;
17579
+ break;
17580
+ }
17581
+ }
17582
+ if (firstBad === -1) return text;
17583
+ const parts = [];
17584
+ let lastIndex = 0;
17585
+ for (let i = firstBad; i < len; i++) {
17586
+ const code = text.charCodeAt(i);
17587
+ if (code < 128) {
17588
+ const action = ENCODE_ACTION[code];
17589
+ if (action === 0) continue;
17590
+ if (lastIndex < i) parts.push(text.substring(lastIndex, i));
17591
+ if (action === 1) parts.push(ENCODE_ENTITIES[code]);
17592
+ lastIndex = i + 1;
17593
+ } else if (code >= 55296 && code <= 56319) {
17594
+ const next = text.charCodeAt(i + 1);
17595
+ if (next >= 56320 && next <= 57343) {
17596
+ i++;
17597
+ continue;
17598
+ }
17599
+ if (lastIndex < i) parts.push(text.substring(lastIndex, i));
17600
+ lastIndex = i + 1;
17601
+ } else if (code >= 56320 && code <= 57343) {
17602
+ if (lastIndex < i) parts.push(text.substring(lastIndex, i));
17603
+ lastIndex = i + 1;
17604
+ } else if (code === 65534 || code === 65535) {
17605
+ if (lastIndex < i) parts.push(text.substring(lastIndex, i));
17606
+ lastIndex = i + 1;
17607
+ }
17608
+ }
17609
+ if (lastIndex < len) parts.push(text.substring(lastIndex));
17610
+ return parts.length === 1 ? parts[0] : parts.join("");
17611
+ }
17612
+ /**
17613
+ * Encode a value for use in an XML attribute.
17614
+ *
17615
+ * Same as {@link xmlEncode} — provided as a semantic alias.
17616
+ * In the future this could apply attribute-specific normalisation
17617
+ * (e.g. collapsing whitespace per XML 1.0 §3.3.3).
17618
+ */
17619
+ function xmlEncodeAttr(value) {
17620
+ return xmlEncode(value);
17621
+ }
17622
+ /**
17623
+ * Characters that must NEVER appear in XML element or attribute names.
17624
+ * This is a fast security check to prevent markup injection via names,
17625
+ * not a full XML NameChar validation (which would require Unicode tables).
17626
+ */
17627
+ const INVALID_NAME_CHARS = /[\s<>"'/=&]/;
17628
+ /**
17629
+ * Validate an XML element or attribute name against injection attacks.
17630
+ *
17631
+ * Rejects:
17632
+ * - Empty names
17633
+ * - Names containing whitespace, `<`, `>`, `"`, `'`, `/`, `=`, `&`
17634
+ * - Names starting with a digit, `-`, or `.`
17635
+ *
17636
+ * This is NOT a full XML Name validation (which requires Unicode NameStartChar
17637
+ * tables). It is a focused security check to prevent markup injection.
17638
+ */
17639
+ function validateXmlName(name) {
17640
+ if (!name) throw new XmlError("XML name must not be empty");
17641
+ if (INVALID_NAME_CHARS.test(name)) throw new XmlError(`Invalid XML name: contains forbidden character in "${name}"`);
17642
+ const first = name.charCodeAt(0);
17643
+ if (first >= 48 && first <= 57 || first === 45 || first === 46) throw new XmlError(`Invalid XML name: "${name}" starts with forbidden character`);
17644
+ }
17645
+ /**
17646
+ * Encode text for a CDATA section, splitting on `]]>` to produce valid output.
17647
+ *
17648
+ * The sequence `]]>` cannot appear inside CDATA, so each occurrence is split
17649
+ * into adjacent CDATA sections: `<![CDATA[...]]]]><![CDATA[>...]]>`.
17650
+ */
17651
+ function encodeCData(text) {
17652
+ return "<![CDATA[" + text.split("]]>").join("]]]]><![CDATA[>") + "]]>";
17653
+ }
17654
+ /**
17655
+ * Validate that text is legal for an XML comment.
17656
+ *
17657
+ * XML spec: comments must not contain `--` and must not end with `-`.
17658
+ * @throws {XmlError} if the text is invalid.
17659
+ */
17660
+ function validateCommentText(text) {
17661
+ if (text.includes("--") || text.endsWith("-")) throw new XmlError("Invalid comment: must not contain \"--\" or end with \"-\"");
17662
+ }
17663
+ /** Default XML declaration attributes (`version`, `encoding`, `standalone`). */
17664
+ const StdDocAttributes = {
17665
+ version: "1.0",
17666
+ encoding: "UTF-8",
17667
+ standalone: "yes"
17668
+ };
17669
+ //#endregion
17670
+ //#region src/utils/utils.base.ts
17671
+ /**
17672
+ * Base utility functions shared between Node.js and Browser
17673
+ * All functions use standard Web APIs that work in both environments
17674
+ * (Node.js 16+ supports atob/btoa/TextEncoder/TextDecoder globally)
17675
+ */
17676
+ /**
17677
+ * Convert base64 string to Uint8Array
17678
+ * Uses native Buffer in Node.js for better performance
17679
+ */
17680
+ function base64ToUint8Array(base64) {
17681
+ if (isNode()) return Buffer.from(base64, "base64");
17682
+ const binary = atob(base64);
17683
+ const bytes = new Uint8Array(binary.length);
17684
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
17685
+ return bytes;
17686
+ }
17687
+ function dateToExcel(d, date1904) {
17688
+ return 25569 + d.getTime() / (24 * 3600 * 1e3) - (date1904 ? 1462 : 0);
17689
+ }
17690
+ function excelToDate(v, date1904) {
17691
+ const millisecondSinceEpoch = Math.round((v - 25569 + (date1904 ? 1462 : 0)) * 24 * 3600 * 1e3);
17692
+ return new Date(millisecondSinceEpoch);
17693
+ }
17694
+ /**
17695
+ * Parse an OOXML date string into a Date object.
17696
+ * OOXML dates like "2024-01-15T00:00:00" lack a timezone suffix,
17697
+ * which some JS engines parse as local time. Appending "Z" forces UTC.
17698
+ */
17699
+ function parseOoxmlDate(raw) {
17700
+ return new Date(raw.endsWith("Z") ? raw : raw + "Z");
17701
+ }
17702
+ /**
17703
+ * Pattern matching OOXML `_xHHHH_` escape sequences (case-insensitive hex).
17704
+ *
17705
+ * Per the OOXML spec, `_xHHHH_` encodes a Unicode code point where HHHH is
17706
+ * a 4-digit hexadecimal number. The spec uses uppercase, but real-world files
17707
+ * from third-party tools (Google Sheets, LibreOffice, etc.) may use lowercase.
17708
+ */
17709
+ const ooxmlEscapeRegex = /_x([0-9A-Fa-f]{4})_/g;
17710
+ /**
17711
+ * Decode OOXML `_xHHHH_` escape sequences in a string.
17712
+ *
17713
+ * Used when reading text content from `<t>` elements in shared strings,
17714
+ * rich text runs, and inline strings. The replacement works left-to-right,
17715
+ * so `_x005F_x000D_` correctly decodes to the literal string `_x000D_`
17716
+ * (the `_x005F_` decodes to `_`, consuming the match).
17717
+ */
17718
+ function decodeOoxmlEscape(text) {
17719
+ return text.replace(ooxmlEscapeRegex, (match, $1) => {
17720
+ const code = parseInt($1, 16);
17721
+ if (code === 0 || code >= 1 && code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31 || code === 127 || code >= 55296 && code <= 57343 || code === 65534 || code === 65535) return match;
17722
+ return String.fromCharCode(code);
17723
+ });
17724
+ }
17725
+ /**
17726
+ * Encode literal `_xHHHH_` patterns in a string for OOXML output.
17727
+ *
17728
+ * If a string naturally contains the pattern `_xHHHH_` (e.g., the user typed
17729
+ * `_x000D_`), the leading underscore must be escaped as `_x005F_` to prevent
17730
+ * readers from misinterpreting it as an escape sequence.
17731
+ *
17732
+ * Roundtrip guarantee: `decodeOoxmlEscape(encodeOoxmlEscape(s)) === s`
17733
+ */
17734
+ function encodeOoxmlEscape(text) {
17735
+ return text.replace(ooxmlEscapeRegex, "_x005F_x$1_");
17736
+ }
17737
+ /**
17738
+ * Characters that XML attribute-value normalisation replaces with spaces
17739
+ * (XML 1.0 §3.3.3). When writing OOXML attribute values we must encode
17740
+ * these as `_xHHHH_` so that the original characters survive a round-trip.
17741
+ */
17742
+ const xmlAttrUnsafeRe = /[\t\n\r]/g;
17743
+ const xmlAttrUnsafeMap = {
17744
+ " ": "_x0009_",
17745
+ "\n": "_x000A_",
17746
+ "\r": "_x000D_"
17747
+ };
17748
+ /**
17749
+ * Encode a string for safe use in an OOXML **XML attribute** value.
17750
+ *
17751
+ * Two transformations are applied (order matters):
17752
+ * 1. Literal `_xHHHH_` patterns are escaped (`_x005F_xHHHH_`) so readers
17753
+ * do not misinterpret them as escape sequences.
17754
+ * 2. Characters that XML attribute-value normalisation would mangle
17755
+ * (`\t`, `\n`, `\r`) are encoded as `_x0009_`, `_x000A_`, `_x000D_`.
17756
+ *
17757
+ * This is the write-side counterpart of {@link decodeOoxmlEscape}.
17758
+ * Use `encodeOoxmlEscape` for element **text** content and this function
17759
+ * for **attribute** values.
17760
+ */
17761
+ function encodeOoxmlAttr(text) {
17762
+ let result = text.replace(ooxmlEscapeRegex, "_x005F_x$1_");
17763
+ result = result.replace(xmlAttrUnsafeRe, (ch) => xmlAttrUnsafeMap[ch]);
17764
+ return result;
17765
+ }
17766
+ function validInt(value) {
17767
+ const i = typeof value === "number" ? value : parseInt(value, 10);
17768
+ return Number.isNaN(i) ? 0 : i;
17769
+ }
17770
+ /**
17771
+ * Split an Excel numFmt string by semicolons, respecting quoted strings and brackets.
17772
+ *
17773
+ * Excel numFmt can have up to 4 sections: `positive ; negative ; zero ; text`.
17774
+ * Semicolons inside `"..."` (literal text) or `[...]` (locale/color tags) must NOT
17775
+ * be treated as section separators.
17776
+ */
17777
+ function splitFormatSections(fmt) {
17778
+ const sections = [];
17779
+ let current = "";
17780
+ let inQuote = false;
17781
+ let inBracket = false;
17782
+ for (let i = 0; i < fmt.length; i++) {
17783
+ const char = fmt[i];
17784
+ if (char === "\"" && !inBracket) {
17785
+ inQuote = !inQuote;
17786
+ current += char;
17787
+ } else if (char === "[" && !inQuote) {
17788
+ inBracket = true;
17789
+ current += char;
17790
+ } else if (char === "]" && !inQuote) {
17791
+ inBracket = false;
17792
+ current += char;
17793
+ } else if (char === ";" && !inQuote && !inBracket) {
17794
+ sections.push(current);
17795
+ current = "";
17796
+ } else current += char;
17797
+ }
17798
+ sections.push(current);
17799
+ return sections;
17800
+ }
17801
+ /** Reusable regex — no capture groups, so safe for `test()`. */
17802
+ const DATE_FMT_RE = /[ymdhMsb]/;
17803
+ /** Strips bracket expressions `[...]` and quoted literals `"..."` from a format string. */
17804
+ const STRIP_BRACKETS_QUOTES_RE = /\[[^\]]*\]|"[^"]*"/g;
17805
+ /** Cache for isDateFmt results — typically only 5-20 unique formats per workbook,
17806
+ * but each may be tested hundreds of thousands of times during reconcile. */
17807
+ const _isDateFmtCache = /* @__PURE__ */ new Map();
17808
+ function isDateFmt(fmt) {
17809
+ if (!fmt) return false;
17810
+ const cached = _isDateFmtCache.get(fmt);
17811
+ if (cached !== void 0) return cached;
17812
+ const clean = splitFormatSections(fmt)[0].replace(STRIP_BRACKETS_QUOTES_RE, "");
17813
+ let result;
17814
+ if (clean.indexOf("@") > -1) result = false;
17815
+ else result = DATE_FMT_RE.test(clean);
17816
+ _isDateFmtCache.set(fmt, result);
17817
+ return result;
17818
+ }
17819
+ function parseBoolean(value) {
17820
+ return value === true || value === "true" || value === 1 || value === "1";
17821
+ }
17822
+ function* range(start, stop, step = 1) {
17823
+ const compareOrder = step > 0 ? (a, b) => a < b : (a, b) => a > b;
17824
+ for (let value = start; compareOrder(value, stop); value += step) yield value;
17825
+ }
17826
+ function toSortedArray(values) {
17827
+ const result = Array.from(values);
17828
+ if (result.length <= 1) return result;
17829
+ if (result.every((item) => Number.isFinite(item))) return result.sort((a, b) => a - b);
17830
+ if (result.every((item) => item instanceof Date)) return result.sort((a, b) => a.getTime() - b.getTime());
17831
+ return result.sort((a, b) => {
17832
+ const ta = sortTypeRank(a);
17833
+ const tb = sortTypeRank(b);
17834
+ if (ta !== tb) return ta - tb;
17835
+ if (ta === 0) return a - b;
17836
+ if (ta === 1) return a.getTime() - b.getTime();
17837
+ return String(a).localeCompare(String(b));
17838
+ });
17839
+ }
17840
+ /** Rank for mixed-type sort: numbers=0, dates=1, everything else=2 */
17841
+ function sortTypeRank(v) {
17842
+ if (Number.isFinite(v)) return 0;
17843
+ if (v instanceof Date) return 1;
17844
+ return 2;
17845
+ }
17846
+ const textDecoder = new TextDecoder("utf-8");
17847
+ let latin1Decoder;
17848
+ let _latin1DecoderResolved = false;
17849
+ function getLatin1Decoder() {
17850
+ if (!_latin1DecoderResolved) {
17851
+ _latin1DecoderResolved = true;
17852
+ try {
17853
+ latin1Decoder = new TextDecoder("latin1");
17854
+ } catch {
17855
+ latin1Decoder = void 0;
17856
+ }
17857
+ }
17858
+ return latin1Decoder;
17859
+ }
17860
+ /**
17861
+ * Convert a Buffer, ArrayBuffer, or Uint8Array to a UTF-8 string
17862
+ * Works in both Node.js and browser environments
17863
+ */
17864
+ function bufferToString(chunk) {
17865
+ if (typeof chunk === "string") return chunk;
17866
+ return textDecoder.decode(chunk);
17867
+ }
17868
+ /**
17869
+ * Convert Uint8Array to base64 string
17870
+ * Uses native Buffer in Node.js, optimized chunked conversion in browser
17871
+ */
17872
+ function uint8ArrayToBase64(bytes) {
17873
+ if (isNode()) return Buffer.from(bytes).toString("base64");
17874
+ if (getLatin1Decoder()) try {
17875
+ return btoa(latin1Decoder.decode(bytes));
17876
+ } catch {}
17877
+ const CHUNK_SIZE = 32768;
17878
+ const chunks = [];
17879
+ for (let i = 0; i < bytes.length; i += CHUNK_SIZE) chunks.push(String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK_SIZE)));
17880
+ return btoa(chunks.join(""));
17881
+ }
17882
+ /**
17883
+ * Convert string to UTF-16LE Uint8Array (used for Excel password hashing)
17884
+ */
17885
+ function stringToUtf16Le(str) {
17886
+ const bytes = new Uint8Array(str.length * 2);
17887
+ for (let i = 0; i < str.length; i++) {
17888
+ const code = str.charCodeAt(i);
17889
+ bytes[i * 2] = code & 255;
17890
+ bytes[i * 2 + 1] = code >> 8 & 255;
17891
+ }
17892
+ return bytes;
17893
+ }
17894
+ /**
17895
+ * Yield to the event loop via a macrotask.
17896
+ * Uses `setTimeout(0)` which works in both Node.js and browsers.
17897
+ */
17898
+ function yieldToEventLoop() {
17899
+ return new Promise((resolve) => setTimeout(resolve, 0));
17900
+ }
17901
+ //#endregion
17902
+ //#region src/modules/excel/utils/cell-format.ts
17903
+ /**
17904
+ * Pad number with leading zeros
17905
+ */
17906
+ function pad0(num, len) {
17907
+ let s = Math.round(num).toString();
17908
+ while (s.length < len) s = "0" + s;
17909
+ return s;
17910
+ }
17911
+ /**
17912
+ * Add thousand separators to a number string
17913
+ */
17914
+ function commaify(s) {
17915
+ const w = 3;
17916
+ if (s.length <= w) return s;
17917
+ const j = s.length % w;
17918
+ let o = s.substring(0, j);
17919
+ for (let i = j; i < s.length; i += w) o += (o.length > 0 ? "," : "") + s.substring(i, i + w);
17920
+ return o;
17921
+ }
17922
+ /**
17923
+ * Round a number to specified decimal places
17924
+ */
17925
+ function roundTo(val, decimals) {
17926
+ const factor = Math.pow(10, decimals);
17927
+ return Math.round(val * factor) / factor;
17928
+ }
17929
+ /**
17930
+ * Process _ (underscore) placeholder - adds space with width of next character
17931
+ * Process * (asterisk) placeholder - repeats next character to fill width (simplified to single char)
17932
+ */
17933
+ function processPlaceholders(fmt) {
17934
+ let result = fmt.replace(/_./g, " ");
17935
+ result = result.replace(/\*./g, "");
17936
+ return result;
17937
+ }
17938
+ /**
17939
+ * Check if format is "General"
17940
+ */
17941
+ function isGeneral(fmt) {
17942
+ return /^General$/i.test(fmt.trim());
17943
+ }
17944
+ /**
17945
+ * Check if format is a date format
17946
+ */
17947
+ function isDateFormat(fmt) {
17948
+ const cleaned = fmt.replace(/\[[^\]]*\]/g, "");
17949
+ return /[ymdhs]/i.test(cleaned) && !/^[#0.,E%$\s()\-+]+$/i.test(cleaned);
17950
+ }
17951
+ const MONTHS_SHORT = [
17952
+ "Jan",
17953
+ "Feb",
17954
+ "Mar",
17955
+ "Apr",
17956
+ "May",
17957
+ "Jun",
17958
+ "Jul",
17959
+ "Aug",
17960
+ "Sep",
17961
+ "Oct",
17962
+ "Nov",
17963
+ "Dec"
17964
+ ];
17965
+ const MONTHS_LONG = [
17966
+ "January",
17967
+ "February",
17968
+ "March",
17969
+ "April",
17970
+ "May",
17971
+ "June",
17972
+ "July",
17973
+ "August",
17974
+ "September",
17975
+ "October",
17976
+ "November",
17977
+ "December"
17978
+ ];
17979
+ const MONTHS_LETTER = [
17980
+ "J",
17981
+ "F",
17982
+ "M",
17983
+ "A",
17984
+ "M",
17985
+ "J",
17986
+ "J",
17987
+ "A",
17988
+ "S",
17989
+ "O",
17990
+ "N",
17991
+ "D"
17992
+ ];
17993
+ const DAYS_SHORT = [
17994
+ "Sun",
17995
+ "Mon",
17996
+ "Tue",
17997
+ "Wed",
17998
+ "Thu",
17999
+ "Fri",
18000
+ "Sat"
18001
+ ];
18002
+ const DAYS_LONG = [
18003
+ "Sunday",
18004
+ "Monday",
18005
+ "Tuesday",
18006
+ "Wednesday",
18007
+ "Thursday",
18008
+ "Friday",
18009
+ "Saturday"
18010
+ ];
18011
+ /**
18012
+ * Disambiguate each `mm` occurrence in a format string that has already been
18013
+ * placeholder-substituted for the other date/time tokens.
18014
+ *
18015
+ * Excel's rule: `mm` is minutes when it's adjacent to an hour or seconds
18016
+ * token (with no intervening date tokens); otherwise it's a zero-padded
18017
+ * month. This must be decided per occurrence — a single format string can
18018
+ * contain both roles (e.g. `"yyyy-mm-dd hh:mm:ss"`).
18019
+ *
18020
+ * The caller has already replaced `yyyy`/`yy` → `Y4/Y2`, month-name tokens
18021
+ * `mmmmm/mmmm/mmm` → `MN5/MN4/MN3`, `dd`/`d` → `D2/D1`, `hh`/`h` → `H2/H1`,
18022
+ * `ss`/`s` → `S2/S1`. So any remaining literal `mm` substrings here are
18023
+ * ambiguous between minute and month.
18024
+ *
18025
+ * Returns the input with each `mm` replaced by either `\x00MI2\x00` (minutes)
18026
+ * or `\x00M2\x00` (month, zero-padded).
18027
+ */
18028
+ function resolveMonthOrMinute(s) {
18029
+ const DATE_TOKEN = /\x00(?:Y[24]|D[12]|MN[345])\x00/;
18030
+ const HOUR_TOKEN = /\x00H[12]\x00/g;
18031
+ const SEC_TOKEN = /\x00S[12]\x00/g;
18032
+ let out = "";
18033
+ let work = s;
18034
+ let idx = work.search(/mm/i);
18035
+ while (idx !== -1) {
18036
+ const before = work.slice(0, idx);
18037
+ const after = work.slice(idx + 2);
18038
+ let nearestHourIdx = -1;
18039
+ let m;
18040
+ HOUR_TOKEN.lastIndex = 0;
18041
+ while ((m = HOUR_TOKEN.exec(before)) !== null) nearestHourIdx = m.index;
18042
+ SEC_TOKEN.lastIndex = 0;
18043
+ const secMatch = SEC_TOKEN.exec(after);
18044
+ const nearestSecIdx = secMatch ? secMatch.index : -1;
18045
+ const hourInRange = nearestHourIdx !== -1 && !DATE_TOKEN.test(before.slice(nearestHourIdx));
18046
+ const secInRange = nearestSecIdx !== -1 && !DATE_TOKEN.test(after.slice(0, nearestSecIdx));
18047
+ out += before + (hourInRange || secInRange ? "\0MI2\0" : "\0M2\0");
18048
+ work = after;
18049
+ idx = work.search(/mm/i);
18050
+ }
18051
+ out += work;
18052
+ return out;
18053
+ }
18054
+ /**
18055
+ * Format a date value using Excel date format
18056
+ * @param serial Excel serial number (days since 1900-01-01)
18057
+ * @param fmt Format string
18058
+ */
18059
+ function formatDate(serial, fmt) {
18060
+ const timeOfDay = Math.round(serial * 86400) % 86400;
18061
+ const hours = Math.floor(timeOfDay / 3600);
18062
+ const minutes = Math.floor(timeOfDay % 3600 / 60);
18063
+ const seconds = timeOfDay % 60;
18064
+ const date = excelToDate(serial, false);
18065
+ const year = date.getUTCFullYear();
18066
+ const month = date.getUTCMonth();
18067
+ const day = date.getUTCDate();
18068
+ const dayOfWeek = date.getUTCDay();
18069
+ const fractionalSeconds = serial * 86400 - Math.floor(serial * 86400);
18070
+ const hasAmPm = /AM\/PM|A\/P/i.test(fmt);
18071
+ const isPm = hours >= 12;
18072
+ const hours12 = hours % 12 || 12;
18073
+ let result = fmt.replace(/\[(Red|Green|Blue|Yellow|Magenta|Cyan|White|Black|Color\d+)\]/gi, "");
18074
+ result = processPlaceholders(result);
18075
+ const fracSecMatch = result.match(/ss\.(0+)/i);
18076
+ let fracSecStr = "";
18077
+ if (fracSecMatch) {
18078
+ const decPlaces = fracSecMatch[1].length;
18079
+ fracSecStr = Math.round(fractionalSeconds * Math.pow(10, decPlaces)).toString().padStart(decPlaces, "0");
18080
+ result = result.replace(/ss\.0+/gi, "\0SF\0");
18081
+ }
18082
+ result = result.replace(/yyyy/gi, "\0Y4\0");
18083
+ result = result.replace(/yy/gi, "\0Y2\0");
18084
+ result = result.replace(/mmmmm/gi, "\0MN5\0");
18085
+ result = result.replace(/mmmm/gi, "\0MN4\0");
18086
+ result = result.replace(/mmm/gi, "\0MN3\0");
18087
+ result = result.replace(/dddd/gi, "\0DN4\0");
18088
+ result = result.replace(/ddd/gi, "\0DN3\0");
18089
+ result = result.replace(/dd/gi, "\0D2\0");
18090
+ result = result.replace(/\bd\b/gi, "\0D1\0");
18091
+ result = result.replace(/hh/gi, "\0H2\0");
18092
+ result = result.replace(/\bh\b/gi, "\0H1\0");
18093
+ result = result.replace(/ss/gi, "\0S2\0");
18094
+ result = result.replace(/\bs\b/gi, "\0S1\0");
18095
+ result = resolveMonthOrMinute(result);
18096
+ result = result.replace(/\bm\b/gi, "\0M1\0");
18097
+ result = result.replace(/AM\/PM/gi, "\0AMPM\0");
18098
+ result = result.replace(/A\/P/gi, "\0AP\0");
18099
+ const hourVal = hasAmPm ? hours12 : hours;
18100
+ result = result.replace(/\x00Y4\x00/g, year.toString()).replace(/\x00Y2\x00/g, (year % 100).toString().padStart(2, "0")).replace(/\x00MN5\x00/g, MONTHS_LETTER[month]).replace(/\x00MN4\x00/g, MONTHS_LONG[month]).replace(/\x00MN3\x00/g, MONTHS_SHORT[month]).replace(/\x00M2\x00/g, (month + 1).toString().padStart(2, "0")).replace(/\x00M1\x00/g, (month + 1).toString()).replace(/\x00DN4\x00/g, DAYS_LONG[dayOfWeek]).replace(/\x00DN3\x00/g, DAYS_SHORT[dayOfWeek]).replace(/\x00D2\x00/g, day.toString().padStart(2, "0")).replace(/\x00D1\x00/g, day.toString()).replace(/\x00H2\x00/g, hourVal.toString().padStart(2, "0")).replace(/\x00H1\x00/g, hourVal.toString()).replace(/\x00MI2\x00/g, minutes.toString().padStart(2, "0")).replace(/\x00S2\x00/g, seconds.toString().padStart(2, "0")).replace(/\x00S1\x00/g, seconds.toString()).replace(/\x00SF\x00/g, seconds.toString().padStart(2, "0") + "." + fracSecStr).replace(/\x00AMPM\x00/g, isPm ? "PM" : "AM").replace(/\x00AP\x00/g, isPm ? "P" : "A");
18101
+ result = result.replace(/\\/g, "");
18102
+ return result;
18103
+ }
18104
+ /**
18105
+ * Format a number using "General" format
18106
+ */
18107
+ function formatGeneral(val) {
18108
+ if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
18109
+ if (typeof val === "string") return val;
18110
+ if (Number.isInteger(val)) return val.toString();
18111
+ return val.toPrecision(11).replace(/\.?0+$/, "").replace(/\.?0+e/, "e");
18112
+ }
18113
+ /**
18114
+ * Format a percentage value
18115
+ * @param val The decimal value (e.g., 0.25 for 25%)
18116
+ * @param fmt The format string containing %
18117
+ */
18118
+ function formatPercentage(val, fmt) {
18119
+ const percentCount = (fmt.match(/%/g) ?? []).length;
18120
+ return formatNumberPattern(val * Math.pow(100, percentCount), fmt.replace(/%/g, "") || "0") + "%".repeat(percentCount);
18121
+ }
18122
+ /**
18123
+ * Format a number in scientific notation
18124
+ * @param val The number to format
18125
+ * @param fmt The format string (e.g., "0.00E+00")
18126
+ */
18127
+ function formatScientific(val, fmt) {
18128
+ const sign = val < 0 ? "-" : "";
18129
+ const absVal = Math.abs(val);
18130
+ if (absVal === 0) {
18131
+ const decMatch = fmt.match(/\.([0#]+)E/i);
18132
+ const decPlaces = decMatch ? decMatch[1].length : 2;
18133
+ return "0." + "0".repeat(decPlaces) + "E+00";
18134
+ }
18135
+ const decMatch = fmt.match(/\.([0#]+)E/i);
18136
+ const decPlaces = decMatch ? decMatch[1].length : 2;
18137
+ const hasPlus = fmt.includes("E+");
18138
+ const exp = Math.floor(Math.log10(absVal));
18139
+ const mantissaStr = roundTo(absVal / Math.pow(10, exp), decPlaces).toFixed(decPlaces);
18140
+ const expSign = exp >= 0 ? hasPlus ? "+" : "" : "-";
18141
+ const expStr = pad0(Math.abs(exp), 2);
18142
+ return sign + mantissaStr + "E" + expSign + expStr;
18143
+ }
18144
+ /**
18145
+ * Convert decimal to fraction using continued fraction algorithm
18146
+ */
18147
+ function toFraction(val, maxDenom) {
18148
+ const sign = val < 0 ? -1 : 1;
18149
+ let absVal = Math.abs(val);
18150
+ const whole = Math.floor(absVal);
18151
+ absVal -= whole;
18152
+ if (absVal < 1e-10) return [
18153
+ sign * whole,
18154
+ 0,
18155
+ 1
18156
+ ];
18157
+ let p0 = 0, p1 = 1;
18158
+ let q0 = 1, q1 = 0;
18159
+ let a = Math.floor(absVal);
18160
+ let p = a;
18161
+ let q = 1;
18162
+ while (q1 < maxDenom) {
18163
+ a = Math.floor(absVal);
18164
+ p = a * p1 + p0;
18165
+ q = a * q1 + q0;
18166
+ if (absVal - a < 1e-10) break;
18167
+ absVal = 1 / (absVal - a);
18168
+ p0 = p1;
18169
+ p1 = p;
18170
+ q0 = q1;
18171
+ q1 = q;
18172
+ }
18173
+ if (q > maxDenom) {
18174
+ q = q1;
18175
+ p = p1;
18176
+ }
18177
+ return [
18178
+ sign * whole,
18179
+ sign * p,
18180
+ q
18181
+ ];
18182
+ }
18183
+ /**
18184
+ * Format a number as a fraction
18185
+ * @param val The number to format
18186
+ * @param fmt The format string (e.g., "# ?/?", "# ??/??")
18187
+ */
18188
+ function formatFraction(val, fmt) {
18189
+ const sign = val < 0 ? "-" : "";
18190
+ const absVal = Math.abs(val);
18191
+ const fixedDenomMatch = fmt.match(/\?+\s*\/\s*(\d+)/);
18192
+ if (fixedDenomMatch) {
18193
+ const denom = parseInt(fixedDenomMatch[1], 10);
18194
+ const whole = Math.floor(absVal);
18195
+ const frac = absVal - whole;
18196
+ const numer = Math.round(frac * denom);
18197
+ if (fmt.includes("#") || fmt.includes("0")) {
18198
+ if (numer === 0) return sign + whole.toString();
18199
+ return sign + (whole > 0 ? whole + " " : "") + numer + "/" + denom;
18200
+ }
18201
+ return sign + (whole * denom + numer) + "/" + denom;
18202
+ }
18203
+ const denomMatch = fmt.match(/\/\s*(\?+)/);
18204
+ const maxDigits = denomMatch ? denomMatch[1].length : 2;
18205
+ const [whole, numer, denom] = toFraction(absVal, Math.pow(10, maxDigits) - 1);
18206
+ if (fmt.includes("#") && whole !== 0) {
18207
+ if (numer === 0) return sign + Math.abs(whole).toString();
18208
+ return sign + Math.abs(whole) + " " + Math.abs(numer) + "/" + denom;
18209
+ }
18210
+ if (numer === 0) return whole === 0 ? "0" : sign + Math.abs(whole).toString();
18211
+ return sign + (Math.abs(whole) * denom + Math.abs(numer)) + "/" + denom;
18212
+ }
18213
+ /**
18214
+ * Format elapsed time (e.g., [h]:mm:ss for durations > 24 hours)
18215
+ */
18216
+ function formatElapsedTime(serial, fmt) {
18217
+ const totalSeconds = Math.round(serial * 86400);
18218
+ const totalMinutes = Math.floor(totalSeconds / 60);
18219
+ const totalHours = Math.floor(totalMinutes / 60);
18220
+ const seconds = totalSeconds % 60;
18221
+ const minutes = totalMinutes % 60;
18222
+ const hours = totalHours;
18223
+ let result = fmt;
18224
+ if (/\[h+\]/i.test(result)) result = result.replace(/\[h+\]/gi, hours.toString());
18225
+ if (/\[m+\]/i.test(result)) result = result.replace(/\[m+\]/gi, totalMinutes.toString());
18226
+ if (/\[s+\]/i.test(result)) result = result.replace(/\[s+\]/gi, totalSeconds.toString());
18227
+ result = result.replace(/mm/gi, minutes.toString().padStart(2, "0"));
18228
+ result = result.replace(/ss/gi, seconds.toString().padStart(2, "0"));
18229
+ return result;
18230
+ }
18231
+ /**
18232
+ * Format a number with the given pattern
18233
+ * Handles patterns like "0", "00", "#,##0", "0-0", "000-0000" etc.
18234
+ */
18235
+ function formatNumberPattern(val, fmt) {
18236
+ const absVal = Math.abs(val);
18237
+ const sign = val < 0 ? "-" : "";
18238
+ let trailingCommas = 0;
18239
+ let workFmt = fmt;
18240
+ while (workFmt.endsWith(",")) {
18241
+ trailingCommas++;
18242
+ workFmt = workFmt.slice(0, -1);
18243
+ }
18244
+ const scaledVal = absVal / Math.pow(1e3, trailingCommas);
18245
+ const decimalIdx = workFmt.indexOf(".");
18246
+ let intFmt = workFmt;
18247
+ let decFmt = "";
18248
+ if (decimalIdx !== -1) {
18249
+ intFmt = workFmt.substring(0, decimalIdx);
18250
+ decFmt = workFmt.substring(decimalIdx + 1);
18251
+ }
18252
+ const decimalPlaces = decFmt.replace(/[^0#?]/g, "").length;
18253
+ const roundedVal = roundTo(scaledVal, decimalPlaces);
18254
+ if (roundedVal === 0 && !intFmt.includes("0") && !decFmt.includes("0")) {
18255
+ let result = "";
18256
+ for (const ch of intFmt) if (ch === "?") result += " ";
18257
+ else if (ch !== "#" && ch !== ",") result += ch;
18258
+ if (decimalPlaces > 0) {
18259
+ if (/[0?]/.test(decFmt)) {
18260
+ result += ".";
18261
+ for (const ch of decFmt) if (ch === "?") result += " ";
18262
+ }
18263
+ }
18264
+ return sign + result;
18265
+ }
18266
+ const [intPart, decPart = ""] = roundedVal.toString().split(".");
18267
+ const hasLiteralInFormat = /[0#?][^0#?,.\s][0#?]/.test(intFmt);
18268
+ let formattedInt;
18269
+ if (hasLiteralInFormat) {
18270
+ const digitPlaceholders = intFmt.replace(/[^0#?]/g, "").length;
18271
+ let digits = intPart;
18272
+ if (digits.length < digitPlaceholders) digits = "0".repeat(digitPlaceholders - digits.length) + digits;
18273
+ formattedInt = "";
18274
+ let digitIndex = digits.length - digitPlaceholders;
18275
+ for (let i = 0; i < intFmt.length; i++) {
18276
+ const char = intFmt[i];
18277
+ if (char === "0" || char === "#" || char === "?") {
18278
+ if (digitIndex < digits.length) {
18279
+ formattedInt += digits[digitIndex];
18280
+ digitIndex++;
18281
+ }
18282
+ } else if (char !== ",") formattedInt += char;
18283
+ }
18284
+ } else {
18285
+ formattedInt = intPart;
18286
+ if (intFmt.includes(",")) formattedInt = commaify(intPart);
18287
+ const minIntDigits = (intFmt.match(/0/g) ?? []).length;
18288
+ const totalIntSlots = (intFmt.match(/[0?]/g) ?? []).length;
18289
+ if (formattedInt.length < minIntDigits) formattedInt = "0".repeat(minIntDigits - formattedInt.length) + formattedInt;
18290
+ if (formattedInt.length < totalIntSlots) formattedInt = " ".repeat(totalIntSlots - formattedInt.length) + formattedInt;
18291
+ if (formattedInt === "0" && minIntDigits === 0 && totalIntSlots === 0) formattedInt = "";
18292
+ }
18293
+ let formattedDec = "";
18294
+ if (decimalPlaces > 0) {
18295
+ const decChars = (decPart + "0".repeat(decimalPlaces)).substring(0, decimalPlaces).split("");
18296
+ for (let i = decFmt.length - 1; i >= 0; i--) {
18297
+ if (i >= decChars.length) continue;
18298
+ if (decFmt[i] === "#" && decChars[i] === "0") decChars[i] = "";
18299
+ else if (decFmt[i] === "?" && decChars[i] === "0") decChars[i] = " ";
18300
+ else break;
18301
+ }
18302
+ const decStr = decChars.join("");
18303
+ if (decStr.length > 0) formattedDec = "." + decStr;
18304
+ }
18305
+ return sign + formattedInt + formattedDec;
18306
+ }
18307
+ /**
18308
+ * Remove quoted literal text markers and return the literal characters
18309
+ * Also handles backslash escape sequences
18310
+ */
18311
+ function processQuotedText(fmt) {
18312
+ let result = "";
18313
+ let i = 0;
18314
+ while (i < fmt.length) if (fmt[i] === "\"") {
18315
+ i++;
18316
+ while (i < fmt.length && fmt[i] !== "\"") {
18317
+ result += fmt[i];
18318
+ i++;
18319
+ }
18320
+ i++;
18321
+ } else if (fmt[i] === "\\" && i + 1 < fmt.length) {
18322
+ i++;
18323
+ result += fmt[i];
18324
+ i++;
18325
+ } else {
18326
+ result += fmt[i];
18327
+ i++;
18328
+ }
18329
+ return result;
18330
+ }
18331
+ /**
18332
+ * Check if a condition matches (e.g., [>100], [<=50])
18333
+ */
18334
+ function checkCondition(val, condition) {
18335
+ const match = condition.match(/\[(=|>|<|>=|<=|<>)(-?\d+(?:\.\d*)?)\]/);
18336
+ if (!match) return false;
18337
+ const op = match[1];
18338
+ const threshold = parseFloat(match[2]);
18339
+ switch (op) {
18340
+ case "=": return val === threshold;
18341
+ case ">": return val > threshold;
18342
+ case "<": return val < threshold;
18343
+ case ">=": return val >= threshold;
18344
+ case "<=": return val <= threshold;
18345
+ case "<>": return val !== threshold;
18346
+ default: return false;
18347
+ }
18348
+ }
18349
+ /**
18350
+ * Parse format string and handle positive/negative/zero/text sections
18351
+ * Excel format: positive;negative;zero;text
18352
+ * Also handles conditional formats like [>100]
18353
+ */
18354
+ function chooseFormat(fmt, val) {
18355
+ if (typeof val === "string") {
18356
+ const sections = splitFormatSections(fmt);
18357
+ if (sections.length >= 4 && sections[3]) return processQuotedText(sections[3]).replace(/@/g, val);
18358
+ return val;
18359
+ }
18360
+ if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
18361
+ const sections = splitFormatSections(fmt);
18362
+ const condRegex = /\[(=|>|<|>=|<=|<>)-?\d+(?:\.\d*)?\]/;
18363
+ if ((sections[0] && condRegex.test(sections[0]) || sections[1] && condRegex.test(sections[1])) && sections.length >= 2) {
18364
+ for (let i = 0; i < Math.min(sections.length, 2); i++) {
18365
+ const condMatch = sections[i].match(/\[(=|>|<|>=|<=|<>)-?\d+(?:\.\d*)?\]/);
18366
+ if (condMatch && checkCondition(val, condMatch[0])) return sections[i];
18367
+ }
18368
+ return sections[sections.length > 2 ? 2 : 1];
18369
+ }
18370
+ if (sections.length === 1) return sections[0];
18371
+ if (sections.length === 2) return val >= 0 ? sections[0] : sections[1];
18372
+ if (val > 0) return sections[0];
18373
+ if (val < 0) return sections[1];
18374
+ return sections[2] || sections[0];
18375
+ }
18376
+ /**
18377
+ * Check if format section is for negative values (2nd section in multi-section format)
18378
+ */
18379
+ function isNegativeSection(fmt, selectedFmt) {
18380
+ const sections = splitFormatSections(fmt);
18381
+ return sections.length >= 2 && sections[1] === selectedFmt;
18382
+ }
18383
+ /**
18384
+ * Main format function - formats a value according to Excel numFmt
18385
+ * @param fmt The Excel number format string (e.g., "0.00%", "#,##0", "yyyy-mm-dd")
18386
+ * @param val The value to format
18387
+ */
18388
+ function format(fmt, val) {
18389
+ if (val == null) return "";
18390
+ if (isGeneral(fmt)) return formatGeneral(val);
18391
+ if (typeof val === "string") return chooseFormat(fmt, val);
18392
+ if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
18393
+ let numVal = val;
18394
+ const selectedFmt = chooseFormat(fmt, numVal);
18395
+ if (numVal < 0 && isNegativeSection(fmt, selectedFmt)) numVal = Math.abs(numVal);
18396
+ let cleanFmt = selectedFmt.replace(/\[(Red|Green|Blue|Yellow|Magenta|Cyan|White|Black|Color\d+)\]/gi, "");
18397
+ cleanFmt = cleanFmt.replace(/\[(>|<|>=|<=|=|<>)-?\d+(\.\d+)?\]/g, "");
18398
+ cleanFmt = cleanFmt.replace(/\[\$[^\]]*\]/g, "");
18399
+ cleanFmt = processPlaceholders(cleanFmt);
18400
+ cleanFmt = processQuotedText(cleanFmt);
18401
+ if (/\[[hms]+\]/i.test(cleanFmt)) return formatElapsedTime(numVal, cleanFmt);
18402
+ if (isDateFormat(cleanFmt)) return formatDate(numVal, cleanFmt);
18403
+ if (cleanFmt.includes("%")) return formatPercentage(numVal, cleanFmt);
18404
+ if (/E[+-]?/i.test(cleanFmt)) return formatScientific(numVal, cleanFmt);
18405
+ if (/\?+\s*\/\s*[\d?]+/.test(cleanFmt)) return formatFraction(numVal, cleanFmt);
18406
+ if (cleanFmt.includes("(") && cleanFmt.includes(")") && numVal < 0) {
18407
+ const innerFmt = cleanFmt.replace(/\(|\)/g, "");
18408
+ return "(" + formatNumberPattern(-numVal, innerFmt) + ")";
18409
+ }
18410
+ if (cleanFmt === "@") return numVal.toString();
18411
+ let prefix = "";
18412
+ let suffix = "";
18413
+ const prefixMatch = cleanFmt.match(/^([^#0?.,]+)/);
18414
+ if (prefixMatch) {
18415
+ prefix = prefixMatch[1];
18416
+ cleanFmt = cleanFmt.substring(prefixMatch[0].length);
18417
+ }
18418
+ const suffixMatch = cleanFmt.match(/([^#0?.,]+)$/);
18419
+ if (suffixMatch && !suffixMatch[1].includes("%")) {
18420
+ suffix = suffixMatch[1];
18421
+ cleanFmt = cleanFmt.substring(0, cleanFmt.length - suffixMatch[0].length);
18422
+ }
18423
+ const formattedNum = formatNumberPattern(numVal, cleanFmt);
18424
+ return prefix + formattedNum + suffix;
18425
+ }
18426
+ /**
18427
+ * Check if format is a pure time format (no date components like y, m for month, d).
18428
+ * Time formats only contain: h, m (minutes in time context), s, AM/PM.
18429
+ * Excludes elapsed time formats like [h]:mm:ss which need the full serial number.
18430
+ */
18431
+ function isTimeOnlyFormat(fmt) {
18432
+ const cleaned = fmt.replace(/"[^"]*"/g, "");
18433
+ if (/\[[hms]\]/i.test(cleaned)) return false;
18434
+ const withoutBrackets = cleaned.replace(/\[[^\]]*\]/g, "");
18435
+ const hasTimeComponents = /[hs]/i.test(withoutBrackets) || /AM\/PM|A\/P/i.test(withoutBrackets);
18436
+ if (/[yd]/i.test(withoutBrackets)) return false;
18437
+ if (/m/i.test(withoutBrackets) && !hasTimeComponents) return false;
18438
+ return hasTimeComponents;
18439
+ }
18440
+ /**
18441
+ * Check if format is a date format (contains y, d, or month-m).
18442
+ * More precise than the internal isDateFormat — correctly handles elapsed time
18443
+ * formats like [h]:mm:ss (not a date format) and distinguishes month-m from minute-m.
18444
+ */
18445
+ function isDateDisplayFormat(fmt) {
18446
+ const cleaned = fmt.replace(/"[^"]*"/g, "");
18447
+ if (/\[[hms]\]/i.test(cleaned)) return false;
18448
+ const withoutBrackets = cleaned.replace(/\[[^\]]*\]/g, "");
18449
+ if (/[yd]/i.test(withoutBrackets)) return true;
18450
+ if (/m/i.test(withoutBrackets)) {
18451
+ if (!(/[hs]/i.test(withoutBrackets) || /AM\/PM|A\/P/i.test(withoutBrackets))) return true;
18452
+ }
18453
+ return false;
18454
+ }
18455
+ /**
18456
+ * Default format applied to Date values whose numFmt is `General` or empty.
18457
+ *
18458
+ * Excel itself substitutes a locale-dependent short date in this case (US:
18459
+ * `m/d/yyyy`). We pick an ISO-like `yyyy-mm-dd` so consumers who never set a
18460
+ * `numFmt` still get a sensible, unambiguous rendering instead of the raw
18461
+ * Excel serial number.
18462
+ */
18463
+ const DEFAULT_DATE_FORMAT = "yyyy-mm-dd";
18464
+ const DEFAULT_DATETIME_FORMAT = "yyyy-mm-dd hh:mm:ss";
18465
+ /**
18466
+ * Format a value according to the given format string.
18467
+ * Handles Date objects with timezone-independent Excel serial conversion.
18468
+ */
18469
+ function formatCellValue(value, fmt, dateFormat) {
18470
+ if (value instanceof Date) {
18471
+ let serial = dateToExcel(value);
18472
+ if (isTimeOnlyFormat(fmt)) {
18473
+ serial = serial % 1;
18474
+ if (serial < 0) serial += 1;
18475
+ return format(fmt, serial);
18476
+ }
18477
+ let effectiveFmt;
18478
+ if (dateFormat && isDateDisplayFormat(fmt)) effectiveFmt = dateFormat;
18479
+ else if (!fmt || isGeneral(fmt)) effectiveFmt = serial % 1 === 0 ? DEFAULT_DATE_FORMAT : DEFAULT_DATETIME_FORMAT;
18480
+ else effectiveFmt = fmt;
18481
+ return format(effectiveFmt, serial);
18482
+ }
18483
+ return format(fmt, value);
18484
+ }
18485
+ /**
18486
+ * Get the formatted display text for a cell value.
18487
+ *
18488
+ * Handles primitive values, Date objects, formula results, and falls back to
18489
+ * `cell.text` for complex types (rich text, hyperlinks, errors, etc.).
18490
+ *
18491
+ * @param cell - A cell (or cell-like object) with `.value`, `.numFmt`, and `.text`
18492
+ * @param dateFormat - Optional custom date format override
18493
+ */
18494
+ function getCellDisplayText$1(cell, dateFormat) {
18495
+ const value = cell.value;
18496
+ const numFmt = cell.numFmt;
18497
+ const fmt = typeof numFmt === "string" ? numFmt : numFmt?.formatCode ?? "General";
18498
+ if (value == null) return "";
18499
+ if (value instanceof Date || typeof value === "number" || typeof value === "boolean" || typeof value === "string") return formatCellValue(value, fmt, dateFormat);
18500
+ if (typeof value === "object" && "formula" in value) {
18501
+ const result = value.result;
18502
+ if (result == null) return "";
18503
+ if (result instanceof Date || typeof result === "number" || typeof result === "boolean" || typeof result === "string") return formatCellValue(result, fmt, dateFormat);
18504
+ }
18505
+ return cell.text;
18506
+ }
18507
+ //#endregion
17471
18508
  //#region src/modules/excel/utils/shared-formula.ts
17472
18509
  const replacementCandidateRx = /(([a-z_\-0-9]*)!)?([a-z0-9_$]{2,})([(])?/gi;
17473
18510
  const CRrx = /^([$])?([a-z]+)([$])?([1-9][0-9]*)$/i;
@@ -17692,6 +18729,26 @@ onmessage = async (ev) => {
17692
18729
  get text() {
17693
18730
  return this._value.toString();
17694
18731
  }
18732
+ /**
18733
+ * The cell's display text — the value formatted the way Excel would render
18734
+ * it, applying the cell's `numFmt`. For a Date cell with `numFmt` `"mm-dd-yy"`,
18735
+ * this returns e.g. `"04-12-19"` rather than the JS `Date.prototype.toString()`
18736
+ * output you'd get from `cell.text`.
18737
+ *
18738
+ * Handles primitive values, dates, and formula results. For rich text,
18739
+ * hyperlinks, errors, and other complex types, falls back to `cell.text`.
18740
+ *
18741
+ * Note: numFmt codes that are locale-dependent in Excel (e.g. built-in
18742
+ * numFmtId 14 renders as `dd.mm.yyyy` under German locale but is stored
18743
+ * as `mm-dd-yy`) are applied literally — excelts does not perform
18744
+ * Excel's locale-based format substitution. If you need a specific date
18745
+ * style across cells regardless of per-cell numFmts, call the exported
18746
+ * {@link getCellDisplayText} helper with a `dateFormat` argument, or use
18747
+ * `worksheet.toJSON({ dateFormat })`.
18748
+ */
18749
+ get displayText() {
18750
+ return getCellDisplayText$1(this);
18751
+ }
17695
18752
  get html() {
17696
18753
  return escapeHtml(this.text);
17697
18754
  }
@@ -18808,437 +19865,6 @@ onmessage = async (ev) => {
18808
19865
  }
18809
19866
  };
18810
19867
  //#endregion
18811
- //#region src/utils/env.ts
18812
- /**
18813
- * Environment detection utilities
18814
- * Common functions to detect runtime environment (Node.js vs Browser)
18815
- */
18816
- /**
18817
- * Check if running in Node.js environment
18818
- * Returns true if process.versions.node exists
18819
- */
18820
- function isNode() {
18821
- return typeof process !== "undefined" && !!process.versions?.node;
18822
- }
18823
- //#endregion
18824
- //#region src/modules/xml/encode.ts
18825
- /**
18826
- * XML Encoding / Decoding Utilities
18827
- *
18828
- * Self-contained XML entity encoding and decoding functions.
18829
- */
18830
- /** Standard XML entity decode map. */
18831
- const DECODE_MAP = {
18832
- lt: "<",
18833
- gt: ">",
18834
- amp: "&",
18835
- quot: "\"",
18836
- apos: "'"
18837
- };
18838
- /** Regex for decoding XML entities (named + numeric). */
18839
- const DECODE_RE = /&(#\d+|#[xX][0-9A-Fa-f]+|\w+);/g;
18840
- /**
18841
- * Lookup table for characters that need encoding in the ASCII range (0-127).
18842
- * 0 = safe, 1 = encode to entity, 2 = strip (invalid control char)
18843
- */
18844
- const ENCODE_ACTION = /* @__PURE__ */ (() => {
18845
- const t = new Uint8Array(128);
18846
- for (let i = 0; i <= 8; i++) t[i] = 2;
18847
- t[11] = 2;
18848
- t[12] = 2;
18849
- for (let i = 14; i <= 31; i++) t[i] = 2;
18850
- t[127] = 2;
18851
- t[34] = 1;
18852
- t[38] = 1;
18853
- t[39] = 1;
18854
- t[60] = 1;
18855
- t[62] = 1;
18856
- return t;
18857
- })();
18858
- const ENCODE_ENTITIES = {
18859
- 34: "&quot;",
18860
- 38: "&amp;",
18861
- 39: "&apos;",
18862
- 60: "&lt;",
18863
- 62: "&gt;"
18864
- };
18865
- /**
18866
- * Decode XML entities in a string.
18867
- *
18868
- * Handles named entities (`&lt;`, `&gt;`, `&amp;`, `&quot;`, `&apos;`)
18869
- * and numeric character references (`&#123;`, `&#x7B;`).
18870
- *
18871
- * Security: validates numeric code points are in range [1, 0x10FFFF]
18872
- * and rejects surrogate halves (0xD800-0xDFFF).
18873
- *
18874
- * Fast-path: returns the original string if no `&` is found.
18875
- */
18876
- function xmlDecode(text) {
18877
- if (text.indexOf("&") === -1) return text;
18878
- return text.replace(DECODE_RE, (match, entity) => {
18879
- if (entity[0] === "#") {
18880
- const code = entity[1] === "x" || entity[1] === "X" ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10);
18881
- if (Number.isNaN(code) || code < 1 || code >= 55296 && code <= 57343 || code > 1114111) return match;
18882
- return String.fromCodePoint(code);
18883
- }
18884
- return DECODE_MAP[entity] ?? match;
18885
- });
18886
- }
18887
- /**
18888
- * Encode special characters for XML output.
18889
- *
18890
- * Escapes `<`, `>`, `&`, `"`, `'` to their entity equivalents.
18891
- * Strips invalid XML control characters (0x00-0x08, 0x0B-0x0C, 0x0E-0x1F, 0x7F)
18892
- * and lone surrogates (0xD800-0xDFFF without a pair).
18893
- *
18894
- * Optimized: uses a lookup table and manual scan instead of regex for
18895
- * maximum throughput on the hot path (called per attribute/text value).
18896
- */
18897
- function xmlEncode(text) {
18898
- const len = text.length;
18899
- let firstBad = -1;
18900
- for (let i = 0; i < len; i++) {
18901
- const code = text.charCodeAt(i);
18902
- if (code < 128) {
18903
- if (ENCODE_ACTION[code] !== 0) {
18904
- firstBad = i;
18905
- break;
18906
- }
18907
- } else if (code >= 55296 && code <= 57343) {
18908
- if (code <= 56319) {
18909
- const next = text.charCodeAt(i + 1);
18910
- if (next >= 56320 && next <= 57343) {
18911
- i++;
18912
- continue;
18913
- }
18914
- }
18915
- firstBad = i;
18916
- break;
18917
- } else if (code === 65534 || code === 65535) {
18918
- firstBad = i;
18919
- break;
18920
- }
18921
- }
18922
- if (firstBad === -1) return text;
18923
- const parts = [];
18924
- let lastIndex = 0;
18925
- for (let i = firstBad; i < len; i++) {
18926
- const code = text.charCodeAt(i);
18927
- if (code < 128) {
18928
- const action = ENCODE_ACTION[code];
18929
- if (action === 0) continue;
18930
- if (lastIndex < i) parts.push(text.substring(lastIndex, i));
18931
- if (action === 1) parts.push(ENCODE_ENTITIES[code]);
18932
- lastIndex = i + 1;
18933
- } else if (code >= 55296 && code <= 56319) {
18934
- const next = text.charCodeAt(i + 1);
18935
- if (next >= 56320 && next <= 57343) {
18936
- i++;
18937
- continue;
18938
- }
18939
- if (lastIndex < i) parts.push(text.substring(lastIndex, i));
18940
- lastIndex = i + 1;
18941
- } else if (code >= 56320 && code <= 57343) {
18942
- if (lastIndex < i) parts.push(text.substring(lastIndex, i));
18943
- lastIndex = i + 1;
18944
- } else if (code === 65534 || code === 65535) {
18945
- if (lastIndex < i) parts.push(text.substring(lastIndex, i));
18946
- lastIndex = i + 1;
18947
- }
18948
- }
18949
- if (lastIndex < len) parts.push(text.substring(lastIndex));
18950
- return parts.length === 1 ? parts[0] : parts.join("");
18951
- }
18952
- /**
18953
- * Encode a value for use in an XML attribute.
18954
- *
18955
- * Same as {@link xmlEncode} — provided as a semantic alias.
18956
- * In the future this could apply attribute-specific normalisation
18957
- * (e.g. collapsing whitespace per XML 1.0 §3.3.3).
18958
- */
18959
- function xmlEncodeAttr(value) {
18960
- return xmlEncode(value);
18961
- }
18962
- /**
18963
- * Characters that must NEVER appear in XML element or attribute names.
18964
- * This is a fast security check to prevent markup injection via names,
18965
- * not a full XML NameChar validation (which would require Unicode tables).
18966
- */
18967
- const INVALID_NAME_CHARS = /[\s<>"'/=&]/;
18968
- /**
18969
- * Validate an XML element or attribute name against injection attacks.
18970
- *
18971
- * Rejects:
18972
- * - Empty names
18973
- * - Names containing whitespace, `<`, `>`, `"`, `'`, `/`, `=`, `&`
18974
- * - Names starting with a digit, `-`, or `.`
18975
- *
18976
- * This is NOT a full XML Name validation (which requires Unicode NameStartChar
18977
- * tables). It is a focused security check to prevent markup injection.
18978
- */
18979
- function validateXmlName(name) {
18980
- if (!name) throw new XmlError("XML name must not be empty");
18981
- if (INVALID_NAME_CHARS.test(name)) throw new XmlError(`Invalid XML name: contains forbidden character in "${name}"`);
18982
- const first = name.charCodeAt(0);
18983
- if (first >= 48 && first <= 57 || first === 45 || first === 46) throw new XmlError(`Invalid XML name: "${name}" starts with forbidden character`);
18984
- }
18985
- /**
18986
- * Encode text for a CDATA section, splitting on `]]>` to produce valid output.
18987
- *
18988
- * The sequence `]]>` cannot appear inside CDATA, so each occurrence is split
18989
- * into adjacent CDATA sections: `<![CDATA[...]]]]><![CDATA[>...]]>`.
18990
- */
18991
- function encodeCData(text) {
18992
- return "<![CDATA[" + text.split("]]>").join("]]]]><![CDATA[>") + "]]>";
18993
- }
18994
- /**
18995
- * Validate that text is legal for an XML comment.
18996
- *
18997
- * XML spec: comments must not contain `--` and must not end with `-`.
18998
- * @throws {XmlError} if the text is invalid.
18999
- */
19000
- function validateCommentText(text) {
19001
- if (text.includes("--") || text.endsWith("-")) throw new XmlError("Invalid comment: must not contain \"--\" or end with \"-\"");
19002
- }
19003
- /** Default XML declaration attributes (`version`, `encoding`, `standalone`). */
19004
- const StdDocAttributes = {
19005
- version: "1.0",
19006
- encoding: "UTF-8",
19007
- standalone: "yes"
19008
- };
19009
- //#endregion
19010
- //#region src/utils/utils.base.ts
19011
- /**
19012
- * Base utility functions shared between Node.js and Browser
19013
- * All functions use standard Web APIs that work in both environments
19014
- * (Node.js 16+ supports atob/btoa/TextEncoder/TextDecoder globally)
19015
- */
19016
- /**
19017
- * Convert base64 string to Uint8Array
19018
- * Uses native Buffer in Node.js for better performance
19019
- */
19020
- function base64ToUint8Array(base64) {
19021
- if (isNode()) return Buffer.from(base64, "base64");
19022
- const binary = atob(base64);
19023
- const bytes = new Uint8Array(binary.length);
19024
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
19025
- return bytes;
19026
- }
19027
- function dateToExcel(d, date1904) {
19028
- return 25569 + d.getTime() / (24 * 3600 * 1e3) - (date1904 ? 1462 : 0);
19029
- }
19030
- function excelToDate(v, date1904) {
19031
- const millisecondSinceEpoch = Math.round((v - 25569 + (date1904 ? 1462 : 0)) * 24 * 3600 * 1e3);
19032
- return new Date(millisecondSinceEpoch);
19033
- }
19034
- /**
19035
- * Parse an OOXML date string into a Date object.
19036
- * OOXML dates like "2024-01-15T00:00:00" lack a timezone suffix,
19037
- * which some JS engines parse as local time. Appending "Z" forces UTC.
19038
- */
19039
- function parseOoxmlDate(raw) {
19040
- return new Date(raw.endsWith("Z") ? raw : raw + "Z");
19041
- }
19042
- /**
19043
- * Pattern matching OOXML `_xHHHH_` escape sequences (case-insensitive hex).
19044
- *
19045
- * Per the OOXML spec, `_xHHHH_` encodes a Unicode code point where HHHH is
19046
- * a 4-digit hexadecimal number. The spec uses uppercase, but real-world files
19047
- * from third-party tools (Google Sheets, LibreOffice, etc.) may use lowercase.
19048
- */
19049
- const ooxmlEscapeRegex = /_x([0-9A-Fa-f]{4})_/g;
19050
- /**
19051
- * Decode OOXML `_xHHHH_` escape sequences in a string.
19052
- *
19053
- * Used when reading text content from `<t>` elements in shared strings,
19054
- * rich text runs, and inline strings. The replacement works left-to-right,
19055
- * so `_x005F_x000D_` correctly decodes to the literal string `_x000D_`
19056
- * (the `_x005F_` decodes to `_`, consuming the match).
19057
- */
19058
- function decodeOoxmlEscape(text) {
19059
- return text.replace(ooxmlEscapeRegex, (match, $1) => {
19060
- const code = parseInt($1, 16);
19061
- if (code === 0 || code >= 1 && code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31 || code === 127 || code >= 55296 && code <= 57343 || code === 65534 || code === 65535) return match;
19062
- return String.fromCharCode(code);
19063
- });
19064
- }
19065
- /**
19066
- * Encode literal `_xHHHH_` patterns in a string for OOXML output.
19067
- *
19068
- * If a string naturally contains the pattern `_xHHHH_` (e.g., the user typed
19069
- * `_x000D_`), the leading underscore must be escaped as `_x005F_` to prevent
19070
- * readers from misinterpreting it as an escape sequence.
19071
- *
19072
- * Roundtrip guarantee: `decodeOoxmlEscape(encodeOoxmlEscape(s)) === s`
19073
- */
19074
- function encodeOoxmlEscape(text) {
19075
- return text.replace(ooxmlEscapeRegex, "_x005F_x$1_");
19076
- }
19077
- /**
19078
- * Characters that XML attribute-value normalisation replaces with spaces
19079
- * (XML 1.0 §3.3.3). When writing OOXML attribute values we must encode
19080
- * these as `_xHHHH_` so that the original characters survive a round-trip.
19081
- */
19082
- const xmlAttrUnsafeRe = /[\t\n\r]/g;
19083
- const xmlAttrUnsafeMap = {
19084
- " ": "_x0009_",
19085
- "\n": "_x000A_",
19086
- "\r": "_x000D_"
19087
- };
19088
- /**
19089
- * Encode a string for safe use in an OOXML **XML attribute** value.
19090
- *
19091
- * Two transformations are applied (order matters):
19092
- * 1. Literal `_xHHHH_` patterns are escaped (`_x005F_xHHHH_`) so readers
19093
- * do not misinterpret them as escape sequences.
19094
- * 2. Characters that XML attribute-value normalisation would mangle
19095
- * (`\t`, `\n`, `\r`) are encoded as `_x0009_`, `_x000A_`, `_x000D_`.
19096
- *
19097
- * This is the write-side counterpart of {@link decodeOoxmlEscape}.
19098
- * Use `encodeOoxmlEscape` for element **text** content and this function
19099
- * for **attribute** values.
19100
- */
19101
- function encodeOoxmlAttr(text) {
19102
- let result = text.replace(ooxmlEscapeRegex, "_x005F_x$1_");
19103
- result = result.replace(xmlAttrUnsafeRe, (ch) => xmlAttrUnsafeMap[ch]);
19104
- return result;
19105
- }
19106
- function validInt(value) {
19107
- const i = typeof value === "number" ? value : parseInt(value, 10);
19108
- return Number.isNaN(i) ? 0 : i;
19109
- }
19110
- /**
19111
- * Split an Excel numFmt string by semicolons, respecting quoted strings and brackets.
19112
- *
19113
- * Excel numFmt can have up to 4 sections: `positive ; negative ; zero ; text`.
19114
- * Semicolons inside `"..."` (literal text) or `[...]` (locale/color tags) must NOT
19115
- * be treated as section separators.
19116
- */
19117
- function splitFormatSections(fmt) {
19118
- const sections = [];
19119
- let current = "";
19120
- let inQuote = false;
19121
- let inBracket = false;
19122
- for (let i = 0; i < fmt.length; i++) {
19123
- const char = fmt[i];
19124
- if (char === "\"" && !inBracket) {
19125
- inQuote = !inQuote;
19126
- current += char;
19127
- } else if (char === "[" && !inQuote) {
19128
- inBracket = true;
19129
- current += char;
19130
- } else if (char === "]" && !inQuote) {
19131
- inBracket = false;
19132
- current += char;
19133
- } else if (char === ";" && !inQuote && !inBracket) {
19134
- sections.push(current);
19135
- current = "";
19136
- } else current += char;
19137
- }
19138
- sections.push(current);
19139
- return sections;
19140
- }
19141
- /** Reusable regex — no capture groups, so safe for `test()`. */
19142
- const DATE_FMT_RE = /[ymdhMsb]/;
19143
- /** Strips bracket expressions `[...]` and quoted literals `"..."` from a format string. */
19144
- const STRIP_BRACKETS_QUOTES_RE = /\[[^\]]*\]|"[^"]*"/g;
19145
- /** Cache for isDateFmt results — typically only 5-20 unique formats per workbook,
19146
- * but each may be tested hundreds of thousands of times during reconcile. */
19147
- const _isDateFmtCache = /* @__PURE__ */ new Map();
19148
- function isDateFmt(fmt) {
19149
- if (!fmt) return false;
19150
- const cached = _isDateFmtCache.get(fmt);
19151
- if (cached !== void 0) return cached;
19152
- const clean = splitFormatSections(fmt)[0].replace(STRIP_BRACKETS_QUOTES_RE, "");
19153
- let result;
19154
- if (clean.indexOf("@") > -1) result = false;
19155
- else result = DATE_FMT_RE.test(clean);
19156
- _isDateFmtCache.set(fmt, result);
19157
- return result;
19158
- }
19159
- function parseBoolean(value) {
19160
- return value === true || value === "true" || value === 1 || value === "1";
19161
- }
19162
- function* range(start, stop, step = 1) {
19163
- const compareOrder = step > 0 ? (a, b) => a < b : (a, b) => a > b;
19164
- for (let value = start; compareOrder(value, stop); value += step) yield value;
19165
- }
19166
- function toSortedArray(values) {
19167
- const result = Array.from(values);
19168
- if (result.length <= 1) return result;
19169
- if (result.every((item) => Number.isFinite(item))) return result.sort((a, b) => a - b);
19170
- if (result.every((item) => item instanceof Date)) return result.sort((a, b) => a.getTime() - b.getTime());
19171
- return result.sort((a, b) => {
19172
- const ta = sortTypeRank(a);
19173
- const tb = sortTypeRank(b);
19174
- if (ta !== tb) return ta - tb;
19175
- if (ta === 0) return a - b;
19176
- if (ta === 1) return a.getTime() - b.getTime();
19177
- return String(a).localeCompare(String(b));
19178
- });
19179
- }
19180
- /** Rank for mixed-type sort: numbers=0, dates=1, everything else=2 */
19181
- function sortTypeRank(v) {
19182
- if (Number.isFinite(v)) return 0;
19183
- if (v instanceof Date) return 1;
19184
- return 2;
19185
- }
19186
- const textDecoder = new TextDecoder("utf-8");
19187
- let latin1Decoder;
19188
- let _latin1DecoderResolved = false;
19189
- function getLatin1Decoder() {
19190
- if (!_latin1DecoderResolved) {
19191
- _latin1DecoderResolved = true;
19192
- try {
19193
- latin1Decoder = new TextDecoder("latin1");
19194
- } catch {
19195
- latin1Decoder = void 0;
19196
- }
19197
- }
19198
- return latin1Decoder;
19199
- }
19200
- /**
19201
- * Convert a Buffer, ArrayBuffer, or Uint8Array to a UTF-8 string
19202
- * Works in both Node.js and browser environments
19203
- */
19204
- function bufferToString(chunk) {
19205
- if (typeof chunk === "string") return chunk;
19206
- return textDecoder.decode(chunk);
19207
- }
19208
- /**
19209
- * Convert Uint8Array to base64 string
19210
- * Uses native Buffer in Node.js, optimized chunked conversion in browser
19211
- */
19212
- function uint8ArrayToBase64(bytes) {
19213
- if (isNode()) return Buffer.from(bytes).toString("base64");
19214
- if (getLatin1Decoder()) try {
19215
- return btoa(latin1Decoder.decode(bytes));
19216
- } catch {}
19217
- const CHUNK_SIZE = 32768;
19218
- const chunks = [];
19219
- for (let i = 0; i < bytes.length; i += CHUNK_SIZE) chunks.push(String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK_SIZE)));
19220
- return btoa(chunks.join(""));
19221
- }
19222
- /**
19223
- * Convert string to UTF-16LE Uint8Array (used for Excel password hashing)
19224
- */
19225
- function stringToUtf16Le(str) {
19226
- const bytes = new Uint8Array(str.length * 2);
19227
- for (let i = 0; i < str.length; i++) {
19228
- const code = str.charCodeAt(i);
19229
- bytes[i * 2] = code & 255;
19230
- bytes[i * 2 + 1] = code >> 8 & 255;
19231
- }
19232
- return bytes;
19233
- }
19234
- /**
19235
- * Yield to the event loop via a macrotask.
19236
- * Uses `setTimeout(0)` which works in both Node.js and browsers.
19237
- */
19238
- function yieldToEventLoop() {
19239
- return new Promise((resolve) => setTimeout(resolve, 0));
19240
- }
19241
- //#endregion
19242
19868
  //#region src/modules/excel/stream/worksheet-reader.ts
19243
19869
  /**
19244
19870
  * WorksheetReader - Cross-Platform Streaming Worksheet Reader
@@ -21971,7 +22597,7 @@ onmessage = async (ev) => {
21971
22597
  19: { f: "h:mm:ss AM/PM" },
21972
22598
  20: { f: "h:mm" },
21973
22599
  21: { f: "h:mm:ss" },
21974
- 22: { f: "m/d/yy \"h\":mm" },
22600
+ 22: { f: "m/d/yy h:mm" },
21975
22601
  27: {
21976
22602
  "zh-tw": "[$-404]e/m/d",
21977
22603
  "zh-cn": "yyyy\"年\"m\"月\"",
@@ -22034,8 +22660,8 @@ onmessage = async (ev) => {
22034
22660
  },
22035
22661
  37: { f: "#,##0 ;(#,##0)" },
22036
22662
  38: { f: "#,##0 ;[Red](#,##0)" },
22037
- 39: { f: "#,##0.00 ;(#,##0.00)" },
22038
- 40: { f: "#,##0.00 ;[Red](#,##0.00)" },
22663
+ 39: { f: "#,##0.00;(#,##0.00)" },
22664
+ 40: { f: "#,##0.00;[Red](#,##0.00)" },
22039
22665
  45: { f: "mm:ss" },
22040
22666
  46: { f: "[h]:mm:ss" },
22041
22667
  47: { f: "mmss.0" },
@@ -33435,1008 +34061,458 @@ self.onmessage = async function(event) {
33435
34061
  return this.column.name;
33436
34062
  }
33437
34063
  set name(value) {
33438
- this._set("name", value);
33439
- }
33440
- get filterButton() {
33441
- return this.column.filterButton;
33442
- }
33443
- set filterButton(value) {
33444
- this.column.filterButton = value;
33445
- }
33446
- get style() {
33447
- return this.column.style;
33448
- }
33449
- set style(value) {
33450
- this.column.style = value;
33451
- }
33452
- get totalsRowLabel() {
33453
- return this.column.totalsRowLabel;
33454
- }
33455
- set totalsRowLabel(value) {
33456
- this._set("totalsRowLabel", value);
33457
- }
33458
- get totalsRowFunction() {
33459
- return this.column.totalsRowFunction;
33460
- }
33461
- set totalsRowFunction(value) {
33462
- this._set("totalsRowFunction", value);
33463
- }
33464
- get totalsRowResult() {
33465
- return this.column.totalsRowResult;
33466
- }
33467
- set totalsRowResult(value) {
33468
- this._set("totalsRowResult", value);
33469
- }
33470
- get totalsRowFormula() {
33471
- return this.column.totalsRowFormula;
33472
- }
33473
- set totalsRowFormula(value) {
33474
- this._set("totalsRowFormula", value);
33475
- }
33476
- };
33477
- var Table = class Table {
33478
- constructor(worksheet, table) {
33479
- this.worksheet = worksheet;
33480
- if (table) {
33481
- this.table = table;
33482
- if (Array.isArray(table.rows) && table.rows.length === 0 && table.tableRef) {
33483
- const decoded = colCache.decode(table.tableRef);
33484
- if ("dimensions" in decoded) {
33485
- const startRow = decoded.top + (table.headerRow === false ? 0 : 1);
33486
- const endRow = decoded.bottom - (table.totalsRow === true ? 1 : 0);
33487
- if (endRow >= startRow) for (let r = startRow; r <= endRow; r++) {
33488
- const row = worksheet.getRow(r);
33489
- const values = [];
33490
- for (let c = decoded.left; c <= decoded.right; c++) values.push(row.getCell(c).value);
33491
- table.rows.push(values);
33492
- }
33493
- }
33494
- }
33495
- this.validate();
33496
- this.store();
33497
- }
33498
- }
33499
- static {
33500
- this.SUBTOTAL_FUNCTIONS = {
33501
- average: 101,
33502
- countNums: 102,
33503
- count: 103,
33504
- max: 104,
33505
- min: 105,
33506
- stdDev: 107,
33507
- var: 110,
33508
- sum: 109
33509
- };
33510
- }
33511
- getFormula(column) {
33512
- if (column.totalsRowFunction === "none") return null;
33513
- if (column.totalsRowFunction === "custom") return column.totalsRowFormula ?? null;
33514
- const fnNum = column.totalsRowFunction ? Table.SUBTOTAL_FUNCTIONS[column.totalsRowFunction] : void 0;
33515
- if (fnNum !== void 0) return `SUBTOTAL(${fnNum},${this.table.name}[${column.name}])`;
33516
- throw new TableError(`Invalid Totals Row Function: ${column.totalsRowFunction}`);
33517
- }
33518
- get width() {
33519
- return this.table.columns.length;
33520
- }
33521
- get height() {
33522
- return this.table.rows.length;
33523
- }
33524
- get filterHeight() {
33525
- return this.height + (this.table.headerRow ? 1 : 0);
33526
- }
33527
- get tableHeight() {
33528
- return this.filterHeight + (this.table.totalsRow ? 1 : 0);
33529
- }
33530
- validate() {
33531
- const { table } = this;
33532
- const assign = (o, name, dflt) => {
33533
- if (o[name] === void 0) o[name] = dflt;
33534
- };
33535
- assign(table, "headerRow", true);
33536
- assign(table, "totalsRow", false);
33537
- assign(table, "style", {});
33538
- const style = table.style;
33539
- assign(style, "theme", "TableStyleMedium2");
33540
- assign(style, "showFirstColumn", false);
33541
- assign(style, "showLastColumn", false);
33542
- assign(style, "showRowStripes", false);
33543
- assign(style, "showColumnStripes", false);
33544
- if (table.name) table.name = sanitizeTableName(table.name);
33545
- if (table.displayName) table.displayName = sanitizeTableName(table.displayName);
33546
- const assert = (test, message) => {
33547
- if (!test) throw new TableError(message);
33548
- };
33549
- assert(!!table.name, "Table must have a name");
33550
- assert(!!table.ref, "Table must have ref");
33551
- assert(!!table.columns, "Table must have column definitions");
33552
- assert(!!table.rows, "Table must have row definitions");
33553
- table.tl = colCache.decodeAddress(table.ref);
33554
- const { row, col } = table.tl;
33555
- assert(row > 0, "Table must be on valid row");
33556
- assert(col > 0, "Table must be on valid col");
33557
- const { width, tableHeight } = this;
33558
- table.autoFilterRef = colCache.encode(row, col, row, col + width - 1);
33559
- table.tableRef = colCache.encode(row, col, row + tableHeight - 1, col + width - 1);
33560
- table.columns.forEach((column, i) => {
33561
- assert(!!column.name, `Column ${i} must have a name`);
33562
- if (i === 0) assign(column, "totalsRowLabel", "Total");
33563
- else {
33564
- assign(column, "totalsRowFunction", "none");
33565
- column.totalsRowFormula = this.getFormula(column) ?? void 0;
33566
- }
33567
- });
33568
- }
33569
- store() {
33570
- const assignStyle = (cell, style) => {
33571
- if (style) Object.assign(cell.style, style);
33572
- };
33573
- const { worksheet, table } = this;
33574
- const { row, col } = table.tl;
33575
- let count = 0;
33576
- if (table.headerRow) {
33577
- const r = worksheet.getRow(row + count++);
33578
- table.columns.forEach((column, j) => {
33579
- const { style, name } = column;
33580
- const cell = r.getCell(col + j);
33581
- cell.value = name;
33582
- assignStyle(cell, style);
33583
- });
33584
- }
33585
- table.rows.forEach((data) => {
33586
- const r = worksheet.getRow(row + count++);
33587
- data.forEach((value, j) => {
33588
- const cell = r.getCell(col + j);
33589
- if (typeof value === "object" && value !== null && "formula" in value && typeof value.formula === "string") {
33590
- const formulaValue = value;
33591
- const shouldQualify = table.qualifyImplicitStructuredReferences === true;
33592
- cell.value = {
33593
- ...formulaValue,
33594
- formula: shouldQualify ? formulaValue.formula.replace(/(^|[^A-Za-z0-9_])\[@\[?([^[\]]+?)\]?\]/g, `$1${table.name}[[#This Row],[$2]]`) : formulaValue.formula
33595
- };
33596
- } else cell.value = value;
33597
- assignStyle(cell, table.columns[j]?.style);
33598
- });
33599
- });
33600
- if (table.totalsRow) {
33601
- const r = worksheet.getRow(row + count++);
33602
- table.columns.forEach((column, j) => {
33603
- const cell = r.getCell(col + j);
33604
- if (j === 0) cell.value = column.totalsRowLabel;
33605
- else {
33606
- const formula = this.getFormula(column);
33607
- if (formula) cell.value = {
33608
- formula,
33609
- result: column.totalsRowResult
33610
- };
33611
- else cell.value = null;
33612
- }
33613
- assignStyle(cell, column.style);
33614
- });
33615
- }
33616
- }
33617
- load(worksheet) {
33618
- const { table } = this;
33619
- const { row, col } = table.tl;
33620
- let count = 0;
33621
- if (table.headerRow) {
33622
- const r = worksheet.getRow(row + count++);
33623
- table.columns.forEach((column, j) => {
33624
- const cell = r.getCell(col + j);
33625
- cell.value = column.name;
33626
- });
33627
- }
33628
- table.rows.forEach((data) => {
33629
- const r = worksheet.getRow(row + count++);
33630
- data.forEach((value, j) => {
33631
- const cell = r.getCell(col + j);
33632
- cell.value = value;
33633
- });
33634
- });
33635
- if (table.totalsRow) {
33636
- const r = worksheet.getRow(row + count++);
33637
- table.columns.forEach((column, j) => {
33638
- const cell = r.getCell(col + j);
33639
- if (j === 0) cell.value = column.totalsRowLabel;
33640
- else {
33641
- const formula = this.getFormula(column);
33642
- if (formula) cell.value = {
33643
- formula,
33644
- result: column.totalsRowResult
33645
- };
33646
- }
33647
- });
33648
- }
33649
- }
33650
- get model() {
33651
- return this.table;
33652
- }
33653
- set model(value) {
33654
- this.table = value;
33655
- }
33656
- cacheState() {
33657
- if (!this._cache) this._cache = {
33658
- ref: this.ref,
33659
- width: this.width,
33660
- tableHeight: this.tableHeight
33661
- };
33662
- }
33663
- commit() {
33664
- if (!this._cache) return;
33665
- this.validate();
33666
- const ref = colCache.decodeAddress(this._cache.ref);
33667
- if (this.ref !== this._cache.ref) for (let i = 0; i < this._cache.tableHeight; i++) {
33668
- const row = this.worksheet.getRow(ref.row + i);
33669
- for (let j = 0; j < this._cache.width; j++) {
33670
- const cell = row.getCell(ref.col + j);
33671
- cell.value = null;
33672
- }
33673
- }
33674
- else {
33675
- for (let i = this.tableHeight; i < this._cache.tableHeight; i++) {
33676
- const row = this.worksheet.getRow(ref.row + i);
33677
- for (let j = 0; j < this._cache.width; j++) {
33678
- const cell = row.getCell(ref.col + j);
33679
- cell.value = null;
33680
- }
33681
- }
33682
- for (let i = 0; i < this.tableHeight; i++) {
33683
- const row = this.worksheet.getRow(ref.row + i);
33684
- for (let j = this.width; j < this._cache.width; j++) {
33685
- const cell = row.getCell(ref.col + j);
33686
- cell.value = null;
33687
- }
33688
- }
33689
- }
33690
- this.store();
33691
- this._cache = void 0;
33692
- }
33693
- addRow(values, rowNumber, options) {
33694
- this.cacheState();
33695
- if (rowNumber === void 0) this.table.rows.push(values);
33696
- else this.table.rows.splice(rowNumber, 0, values);
33697
- if (options?.commit !== false) this.commit();
33698
- }
33699
- removeRows(rowIndex, count = 1, options) {
33700
- this.cacheState();
33701
- this.table.rows.splice(rowIndex, count);
33702
- if (options?.commit !== false) this.commit();
33703
- }
33704
- getColumn(colIndex) {
33705
- const column = this.table.columns[colIndex];
33706
- return new Column$1(this, column, colIndex);
33707
- }
33708
- addColumn(column, values, colIndex) {
33709
- this.cacheState();
33710
- if (colIndex === void 0) {
33711
- this.table.columns.push(column);
33712
- this.table.rows.forEach((row, i) => {
33713
- row.push(values[i]);
33714
- });
33715
- } else {
33716
- this.table.columns.splice(colIndex, 0, column);
33717
- this.table.rows.forEach((row, i) => {
33718
- row.splice(colIndex, 0, values[i]);
33719
- });
33720
- }
33721
- }
33722
- removeColumns(colIndex, count = 1) {
33723
- this.cacheState();
33724
- this.table.columns.splice(colIndex, count);
33725
- this.table.rows.forEach((row) => {
33726
- row.splice(colIndex, count);
33727
- });
33728
- }
33729
- _assign(target, prop, value) {
33730
- this.cacheState();
33731
- target[prop] = value;
33732
- }
33733
- get ref() {
33734
- return this.table.ref;
33735
- }
33736
- set ref(value) {
33737
- this._assign(this.table, "ref", value);
33738
- }
33739
- get name() {
33740
- return this.table.name;
33741
- }
33742
- set name(value) {
33743
- this.cacheState();
33744
- this.table.name = sanitizeTableName(value);
33745
- }
33746
- get displayName() {
33747
- return this.table.displayName || this.table.name;
33748
- }
33749
- set displayName(value) {
33750
- this.cacheState();
33751
- this.table.displayName = sanitizeTableName(value);
33752
- }
33753
- get headerRow() {
33754
- return this.table.headerRow;
33755
- }
33756
- set headerRow(value) {
33757
- this._assign(this.table, "headerRow", value);
33758
- }
33759
- get totalsRow() {
33760
- return this.table.totalsRow;
34064
+ this._set("name", value);
33761
34065
  }
33762
- set totalsRow(value) {
33763
- this._assign(this.table, "totalsRow", value);
34066
+ get filterButton() {
34067
+ return this.column.filterButton;
33764
34068
  }
33765
- _ensureStyle() {
33766
- if (!this.table.style) this.table.style = {};
33767
- return this.table.style;
34069
+ set filterButton(value) {
34070
+ this.column.filterButton = value;
33768
34071
  }
33769
- get theme() {
33770
- return this.table.style?.theme;
34072
+ get style() {
34073
+ return this.column.style;
33771
34074
  }
33772
- set theme(value) {
33773
- this._ensureStyle().theme = value;
34075
+ set style(value) {
34076
+ this.column.style = value;
33774
34077
  }
33775
- get showFirstColumn() {
33776
- return this.table.style?.showFirstColumn;
34078
+ get totalsRowLabel() {
34079
+ return this.column.totalsRowLabel;
33777
34080
  }
33778
- set showFirstColumn(value) {
33779
- this._ensureStyle().showFirstColumn = value;
34081
+ set totalsRowLabel(value) {
34082
+ this._set("totalsRowLabel", value);
33780
34083
  }
33781
- get showLastColumn() {
33782
- return this.table.style?.showLastColumn;
34084
+ get totalsRowFunction() {
34085
+ return this.column.totalsRowFunction;
33783
34086
  }
33784
- set showLastColumn(value) {
33785
- this._ensureStyle().showLastColumn = value;
34087
+ set totalsRowFunction(value) {
34088
+ this._set("totalsRowFunction", value);
33786
34089
  }
33787
- get showRowStripes() {
33788
- return this.table.style?.showRowStripes;
34090
+ get totalsRowResult() {
34091
+ return this.column.totalsRowResult;
33789
34092
  }
33790
- set showRowStripes(value) {
33791
- this._ensureStyle().showRowStripes = value;
34093
+ set totalsRowResult(value) {
34094
+ this._set("totalsRowResult", value);
33792
34095
  }
33793
- get showColumnStripes() {
33794
- return this.table.style?.showColumnStripes;
34096
+ get totalsRowFormula() {
34097
+ return this.column.totalsRowFormula;
33795
34098
  }
33796
- set showColumnStripes(value) {
33797
- this._ensureStyle().showColumnStripes = value;
34099
+ set totalsRowFormula(value) {
34100
+ this._set("totalsRowFormula", value);
33798
34101
  }
33799
34102
  };
33800
- //#endregion
33801
- //#region src/modules/excel/utils/address.ts
33802
- /**
33803
- * Cell address encoding/decoding utilities (0-indexed)
33804
- *
33805
- * These functions use 0-indexed coordinates (column A = 0, row 1 = 0),
33806
- * matching the convention used by SheetJS and most spreadsheet APIs.
33807
- *
33808
- * @module
33809
- */
33810
- /**
33811
- * Decode column string to 0-indexed number
33812
- * @example decodeCol("A") // => 0
33813
- * @example decodeCol("Z") // => 25
33814
- * @example decodeCol("AA") // => 26
33815
- */
33816
- function decodeCol(colstr) {
33817
- return colCache.l2n(colstr.toUpperCase()) - 1;
33818
- }
33819
- /**
33820
- * Encode 0-indexed column number to string
33821
- * @example encodeCol(0) // => "A"
33822
- * @example encodeCol(25) // => "Z"
33823
- * @example encodeCol(26) // => "AA"
33824
- */
33825
- function encodeCol(col) {
33826
- return colCache.n2l(col + 1);
33827
- }
33828
- /**
33829
- * Decode row string to 0-indexed number
33830
- * @example decodeRow("1") // => 0
33831
- * @example decodeRow("10") // => 9
33832
- */
33833
- function decodeRow(rowstr) {
33834
- return parseInt(rowstr, 10) - 1;
33835
- }
33836
- /**
33837
- * Encode 0-indexed row number to string
33838
- * @example encodeRow(0) // => "1"
33839
- * @example encodeRow(9) // => "10"
33840
- */
33841
- function encodeRow(row) {
33842
- return String(row + 1);
33843
- }
33844
- /**
33845
- * Decode cell address string to CellAddress object (0-indexed)
33846
- * @example decodeCell("A1") // => { c: 0, r: 0 }
33847
- * @example decodeCell("B2") // => { c: 1, r: 1 }
33848
- */
33849
- function decodeCell(cstr) {
33850
- const addr = colCache.decodeAddress(cstr.toUpperCase());
33851
- return {
33852
- c: addr.col - 1,
33853
- r: addr.row - 1
33854
- };
33855
- }
33856
- /**
33857
- * Encode CellAddress object (0-indexed) to cell address string
33858
- * @example encodeCell({ c: 0, r: 0 }) // => "A1"
33859
- * @example encodeCell({ c: 1, r: 1 }) // => "B2"
33860
- */
33861
- function encodeCell(cell) {
33862
- return colCache.encodeAddress(cell.r + 1, cell.c + 1);
33863
- }
33864
- /**
33865
- * Decode range string to SheetRange object (0-indexed)
33866
- * @example decodeRange("A1:B2") // => { s: { c: 0, r: 0 }, e: { c: 1, r: 1 } }
33867
- */
33868
- function decodeRange(range) {
33869
- const idx = range.indexOf(":");
33870
- if (idx === -1) {
33871
- const cell = decodeCell(range);
33872
- return {
33873
- s: cell,
33874
- e: { ...cell }
34103
+ var Table = class Table {
34104
+ constructor(worksheet, table) {
34105
+ this.worksheet = worksheet;
34106
+ if (table) {
34107
+ this.table = table;
34108
+ if (Array.isArray(table.rows) && table.rows.length === 0 && table.tableRef) {
34109
+ const decoded = colCache.decode(table.tableRef);
34110
+ if ("dimensions" in decoded) {
34111
+ const startRow = decoded.top + (table.headerRow === false ? 0 : 1);
34112
+ const endRow = decoded.bottom - (table.totalsRow === true ? 1 : 0);
34113
+ if (endRow >= startRow) for (let r = startRow; r <= endRow; r++) {
34114
+ const row = worksheet.getRow(r);
34115
+ const values = [];
34116
+ for (let c = decoded.left; c <= decoded.right; c++) values.push(row.getCell(c).value);
34117
+ table.rows.push(values);
34118
+ }
34119
+ }
34120
+ }
34121
+ this.validate();
34122
+ this.store();
34123
+ }
34124
+ }
34125
+ static {
34126
+ this.SUBTOTAL_FUNCTIONS = {
34127
+ average: 101,
34128
+ countNums: 102,
34129
+ count: 103,
34130
+ max: 104,
34131
+ min: 105,
34132
+ stdDev: 107,
34133
+ var: 110,
34134
+ sum: 109
33875
34135
  };
33876
34136
  }
33877
- return {
33878
- s: decodeCell(range.slice(0, idx)),
33879
- e: decodeCell(range.slice(idx + 1))
33880
- };
33881
- }
33882
- function encodeRange(startOrRange, end) {
33883
- if (end === void 0) {
33884
- const range = startOrRange;
33885
- return encodeRange(range.s, range.e);
34137
+ getFormula(column) {
34138
+ if (column.totalsRowFunction === "none") return null;
34139
+ if (column.totalsRowFunction === "custom") return column.totalsRowFormula ?? null;
34140
+ const fnNum = column.totalsRowFunction ? Table.SUBTOTAL_FUNCTIONS[column.totalsRowFunction] : void 0;
34141
+ if (fnNum !== void 0) return `SUBTOTAL(${fnNum},${this.table.name}[${column.name}])`;
34142
+ throw new TableError(`Invalid Totals Row Function: ${column.totalsRowFunction}`);
33886
34143
  }
33887
- const startStr = encodeCell(startOrRange);
33888
- const endStr = encodeCell(end);
33889
- return startStr === endStr ? startStr : `${startStr}:${endStr}`;
33890
- }
33891
- //#endregion
33892
- //#region src/modules/excel/utils/cell-format.ts
33893
- /**
33894
- * Pad number with leading zeros
33895
- */
33896
- function pad0(num, len) {
33897
- let s = Math.round(num).toString();
33898
- while (s.length < len) s = "0" + s;
33899
- return s;
33900
- }
33901
- /**
33902
- * Add thousand separators to a number string
33903
- */
33904
- function commaify(s) {
33905
- const w = 3;
33906
- if (s.length <= w) return s;
33907
- const j = s.length % w;
33908
- let o = s.substring(0, j);
33909
- for (let i = j; i < s.length; i += w) o += (o.length > 0 ? "," : "") + s.substring(i, i + w);
33910
- return o;
33911
- }
33912
- /**
33913
- * Round a number to specified decimal places
33914
- */
33915
- function roundTo(val, decimals) {
33916
- const factor = Math.pow(10, decimals);
33917
- return Math.round(val * factor) / factor;
33918
- }
33919
- /**
33920
- * Process _ (underscore) placeholder - adds space with width of next character
33921
- * Process * (asterisk) placeholder - repeats next character to fill width (simplified to single char)
33922
- */
33923
- function processPlaceholders(fmt) {
33924
- let result = fmt.replace(/_./g, " ");
33925
- result = result.replace(/\*./g, "");
33926
- return result;
33927
- }
33928
- /**
33929
- * Check if format is "General"
33930
- */
33931
- function isGeneral(fmt) {
33932
- return /^General$/i.test(fmt.trim());
33933
- }
33934
- /**
33935
- * Check if format is a date format
33936
- */
33937
- function isDateFormat(fmt) {
33938
- const cleaned = fmt.replace(/\[[^\]]*\]/g, "");
33939
- return /[ymdhs]/i.test(cleaned) && !/^[#0.,E%$\s()\-+]+$/i.test(cleaned);
33940
- }
33941
- const MONTHS_SHORT = [
33942
- "Jan",
33943
- "Feb",
33944
- "Mar",
33945
- "Apr",
33946
- "May",
33947
- "Jun",
33948
- "Jul",
33949
- "Aug",
33950
- "Sep",
33951
- "Oct",
33952
- "Nov",
33953
- "Dec"
33954
- ];
33955
- const MONTHS_LONG = [
33956
- "January",
33957
- "February",
33958
- "March",
33959
- "April",
33960
- "May",
33961
- "June",
33962
- "July",
33963
- "August",
33964
- "September",
33965
- "October",
33966
- "November",
33967
- "December"
33968
- ];
33969
- const MONTHS_LETTER = [
33970
- "J",
33971
- "F",
33972
- "M",
33973
- "A",
33974
- "M",
33975
- "J",
33976
- "J",
33977
- "A",
33978
- "S",
33979
- "O",
33980
- "N",
33981
- "D"
33982
- ];
33983
- const DAYS_SHORT = [
33984
- "Sun",
33985
- "Mon",
33986
- "Tue",
33987
- "Wed",
33988
- "Thu",
33989
- "Fri",
33990
- "Sat"
33991
- ];
33992
- const DAYS_LONG = [
33993
- "Sunday",
33994
- "Monday",
33995
- "Tuesday",
33996
- "Wednesday",
33997
- "Thursday",
33998
- "Friday",
33999
- "Saturday"
34000
- ];
34001
- /**
34002
- * Format a date value using Excel date format
34003
- * @param serial Excel serial number (days since 1900-01-01)
34004
- * @param fmt Format string
34005
- */
34006
- function formatDate(serial, fmt) {
34007
- const timeOfDay = Math.round(serial * 86400) % 86400;
34008
- const hours = Math.floor(timeOfDay / 3600);
34009
- const minutes = Math.floor(timeOfDay % 3600 / 60);
34010
- const seconds = timeOfDay % 60;
34011
- const date = excelToDate(serial, false);
34012
- const year = date.getUTCFullYear();
34013
- const month = date.getUTCMonth();
34014
- const day = date.getUTCDate();
34015
- const dayOfWeek = date.getUTCDay();
34016
- const fractionalSeconds = serial * 86400 - Math.floor(serial * 86400);
34017
- const hasAmPm = /AM\/PM|A\/P/i.test(fmt);
34018
- const isPm = hours >= 12;
34019
- const hours12 = hours % 12 || 12;
34020
- let result = fmt.replace(/\[(Red|Green|Blue|Yellow|Magenta|Cyan|White|Black|Color\d+)\]/gi, "");
34021
- result = processPlaceholders(result);
34022
- const fracSecMatch = result.match(/ss\.(0+)/i);
34023
- let fracSecStr = "";
34024
- if (fracSecMatch) {
34025
- const decPlaces = fracSecMatch[1].length;
34026
- fracSecStr = Math.round(fractionalSeconds * Math.pow(10, decPlaces)).toString().padStart(decPlaces, "0");
34027
- result = result.replace(/ss\.0+/gi, "\0SF\0");
34144
+ get width() {
34145
+ return this.table.columns.length;
34028
34146
  }
34029
- result = result.replace(/yyyy/gi, "\0Y4\0");
34030
- result = result.replace(/yy/gi, "\0Y2\0");
34031
- result = result.replace(/mmmmm/gi, "\0MN5\0");
34032
- result = result.replace(/mmmm/gi, "\0MN4\0");
34033
- result = result.replace(/mmm/gi, "\0MN3\0");
34034
- result = result.replace(/dddd/gi, "\0DN4\0");
34035
- result = result.replace(/ddd/gi, "\0DN3\0");
34036
- result = result.replace(/dd/gi, "\0D2\0");
34037
- result = result.replace(/\bd\b/gi, "\0D1\0");
34038
- result = result.replace(/hh/gi, "\0H2\0");
34039
- result = result.replace(/\bh\b/gi, "\0H1\0");
34040
- result = result.replace(/ss/gi, "\0S2\0");
34041
- result = result.replace(/\bs\b/gi, "\0S1\0");
34042
- if (/\x00H[12]\x00.*mm|mm.*\x00S[12]\x00/i.test(result)) result = result.replace(/mm/gi, "\0MI2\0");
34043
- else result = result.replace(/mm/gi, "\0M2\0");
34044
- result = result.replace(/\bm\b/gi, "\0M1\0");
34045
- result = result.replace(/AM\/PM/gi, "\0AMPM\0");
34046
- result = result.replace(/A\/P/gi, "\0AP\0");
34047
- const hourVal = hasAmPm ? hours12 : hours;
34048
- result = result.replace(/\x00Y4\x00/g, year.toString()).replace(/\x00Y2\x00/g, (year % 100).toString().padStart(2, "0")).replace(/\x00MN5\x00/g, MONTHS_LETTER[month]).replace(/\x00MN4\x00/g, MONTHS_LONG[month]).replace(/\x00MN3\x00/g, MONTHS_SHORT[month]).replace(/\x00M2\x00/g, (month + 1).toString().padStart(2, "0")).replace(/\x00M1\x00/g, (month + 1).toString()).replace(/\x00DN4\x00/g, DAYS_LONG[dayOfWeek]).replace(/\x00DN3\x00/g, DAYS_SHORT[dayOfWeek]).replace(/\x00D2\x00/g, day.toString().padStart(2, "0")).replace(/\x00D1\x00/g, day.toString()).replace(/\x00H2\x00/g, hourVal.toString().padStart(2, "0")).replace(/\x00H1\x00/g, hourVal.toString()).replace(/\x00MI2\x00/g, minutes.toString().padStart(2, "0")).replace(/\x00S2\x00/g, seconds.toString().padStart(2, "0")).replace(/\x00S1\x00/g, seconds.toString()).replace(/\x00SF\x00/g, seconds.toString().padStart(2, "0") + "." + fracSecStr).replace(/\x00AMPM\x00/g, isPm ? "PM" : "AM").replace(/\x00AP\x00/g, isPm ? "P" : "A");
34049
- result = result.replace(/\\/g, "");
34050
- return result;
34051
- }
34052
- /**
34053
- * Format a number using "General" format
34054
- */
34055
- function formatGeneral(val) {
34056
- if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
34057
- if (typeof val === "string") return val;
34058
- if (Number.isInteger(val)) return val.toString();
34059
- return val.toPrecision(11).replace(/\.?0+$/, "").replace(/\.?0+e/, "e");
34060
- }
34061
- /**
34062
- * Format a percentage value
34063
- * @param val The decimal value (e.g., 0.25 for 25%)
34064
- * @param fmt The format string containing %
34065
- */
34066
- function formatPercentage(val, fmt) {
34067
- const percentCount = (fmt.match(/%/g) ?? []).length;
34068
- return formatNumberPattern(val * Math.pow(100, percentCount), fmt.replace(/%/g, "") || "0") + "%".repeat(percentCount);
34069
- }
34070
- /**
34071
- * Format a number in scientific notation
34072
- * @param val The number to format
34073
- * @param fmt The format string (e.g., "0.00E+00")
34074
- */
34075
- function formatScientific(val, fmt) {
34076
- const sign = val < 0 ? "-" : "";
34077
- const absVal = Math.abs(val);
34078
- if (absVal === 0) {
34079
- const decMatch = fmt.match(/\.([0#]+)E/i);
34080
- const decPlaces = decMatch ? decMatch[1].length : 2;
34081
- return "0." + "0".repeat(decPlaces) + "E+00";
34147
+ get height() {
34148
+ return this.table.rows.length;
34082
34149
  }
34083
- const decMatch = fmt.match(/\.([0#]+)E/i);
34084
- const decPlaces = decMatch ? decMatch[1].length : 2;
34085
- const hasPlus = fmt.includes("E+");
34086
- const exp = Math.floor(Math.log10(absVal));
34087
- const mantissaStr = roundTo(absVal / Math.pow(10, exp), decPlaces).toFixed(decPlaces);
34088
- const expSign = exp >= 0 ? hasPlus ? "+" : "" : "-";
34089
- const expStr = pad0(Math.abs(exp), 2);
34090
- return sign + mantissaStr + "E" + expSign + expStr;
34091
- }
34092
- /**
34093
- * Convert decimal to fraction using continued fraction algorithm
34094
- */
34095
- function toFraction(val, maxDenom) {
34096
- const sign = val < 0 ? -1 : 1;
34097
- let absVal = Math.abs(val);
34098
- const whole = Math.floor(absVal);
34099
- absVal -= whole;
34100
- if (absVal < 1e-10) return [
34101
- sign * whole,
34102
- 0,
34103
- 1
34104
- ];
34105
- let p0 = 0, p1 = 1;
34106
- let q0 = 1, q1 = 0;
34107
- let a = Math.floor(absVal);
34108
- let p = a;
34109
- let q = 1;
34110
- while (q1 < maxDenom) {
34111
- a = Math.floor(absVal);
34112
- p = a * p1 + p0;
34113
- q = a * q1 + q0;
34114
- if (absVal - a < 1e-10) break;
34115
- absVal = 1 / (absVal - a);
34116
- p0 = p1;
34117
- p1 = p;
34118
- q0 = q1;
34119
- q1 = q;
34150
+ get filterHeight() {
34151
+ return this.height + (this.table.headerRow ? 1 : 0);
34120
34152
  }
34121
- if (q > maxDenom) {
34122
- q = q1;
34123
- p = p1;
34153
+ get tableHeight() {
34154
+ return this.filterHeight + (this.table.totalsRow ? 1 : 0);
34124
34155
  }
34125
- return [
34126
- sign * whole,
34127
- sign * p,
34128
- q
34129
- ];
34130
- }
34131
- /**
34132
- * Format a number as a fraction
34133
- * @param val The number to format
34134
- * @param fmt The format string (e.g., "# ?/?", "# ??/??")
34135
- */
34136
- function formatFraction(val, fmt) {
34137
- const sign = val < 0 ? "-" : "";
34138
- const absVal = Math.abs(val);
34139
- const fixedDenomMatch = fmt.match(/\?+\s*\/\s*(\d+)/);
34140
- if (fixedDenomMatch) {
34141
- const denom = parseInt(fixedDenomMatch[1], 10);
34142
- const whole = Math.floor(absVal);
34143
- const frac = absVal - whole;
34144
- const numer = Math.round(frac * denom);
34145
- if (fmt.includes("#") || fmt.includes("0")) {
34146
- if (numer === 0) return sign + whole.toString();
34147
- return sign + (whole > 0 ? whole + " " : "") + numer + "/" + denom;
34156
+ validate() {
34157
+ const { table } = this;
34158
+ const assign = (o, name, dflt) => {
34159
+ if (o[name] === void 0) o[name] = dflt;
34160
+ };
34161
+ assign(table, "headerRow", true);
34162
+ assign(table, "totalsRow", false);
34163
+ assign(table, "style", {});
34164
+ const style = table.style;
34165
+ assign(style, "theme", "TableStyleMedium2");
34166
+ assign(style, "showFirstColumn", false);
34167
+ assign(style, "showLastColumn", false);
34168
+ assign(style, "showRowStripes", false);
34169
+ assign(style, "showColumnStripes", false);
34170
+ if (table.name) table.name = sanitizeTableName(table.name);
34171
+ if (table.displayName) table.displayName = sanitizeTableName(table.displayName);
34172
+ const assert = (test, message) => {
34173
+ if (!test) throw new TableError(message);
34174
+ };
34175
+ assert(!!table.name, "Table must have a name");
34176
+ assert(!!table.ref, "Table must have ref");
34177
+ assert(!!table.columns, "Table must have column definitions");
34178
+ assert(!!table.rows, "Table must have row definitions");
34179
+ table.tl = colCache.decodeAddress(table.ref);
34180
+ const { row, col } = table.tl;
34181
+ assert(row > 0, "Table must be on valid row");
34182
+ assert(col > 0, "Table must be on valid col");
34183
+ const { width, tableHeight } = this;
34184
+ table.autoFilterRef = colCache.encode(row, col, row, col + width - 1);
34185
+ table.tableRef = colCache.encode(row, col, row + tableHeight - 1, col + width - 1);
34186
+ table.columns.forEach((column, i) => {
34187
+ assert(!!column.name, `Column ${i} must have a name`);
34188
+ if (i === 0) assign(column, "totalsRowLabel", "Total");
34189
+ else {
34190
+ assign(column, "totalsRowFunction", "none");
34191
+ column.totalsRowFormula = this.getFormula(column) ?? void 0;
34192
+ }
34193
+ });
34194
+ }
34195
+ store() {
34196
+ const assignStyle = (cell, style) => {
34197
+ if (style) Object.assign(cell.style, style);
34198
+ };
34199
+ const { worksheet, table } = this;
34200
+ const { row, col } = table.tl;
34201
+ let count = 0;
34202
+ if (table.headerRow) {
34203
+ const r = worksheet.getRow(row + count++);
34204
+ table.columns.forEach((column, j) => {
34205
+ const { style, name } = column;
34206
+ const cell = r.getCell(col + j);
34207
+ cell.value = name;
34208
+ assignStyle(cell, style);
34209
+ });
34210
+ }
34211
+ table.rows.forEach((data) => {
34212
+ const r = worksheet.getRow(row + count++);
34213
+ data.forEach((value, j) => {
34214
+ const cell = r.getCell(col + j);
34215
+ if (typeof value === "object" && value !== null && "formula" in value && typeof value.formula === "string") {
34216
+ const formulaValue = value;
34217
+ const shouldQualify = table.qualifyImplicitStructuredReferences === true;
34218
+ cell.value = {
34219
+ ...formulaValue,
34220
+ formula: shouldQualify ? formulaValue.formula.replace(/(^|[^A-Za-z0-9_])\[@\[?([^[\]]+?)\]?\]/g, `$1${table.name}[[#This Row],[$2]]`) : formulaValue.formula
34221
+ };
34222
+ } else cell.value = value;
34223
+ assignStyle(cell, table.columns[j]?.style);
34224
+ });
34225
+ });
34226
+ if (table.totalsRow) {
34227
+ const r = worksheet.getRow(row + count++);
34228
+ table.columns.forEach((column, j) => {
34229
+ const cell = r.getCell(col + j);
34230
+ if (j === 0) cell.value = column.totalsRowLabel;
34231
+ else {
34232
+ const formula = this.getFormula(column);
34233
+ if (formula) cell.value = {
34234
+ formula,
34235
+ result: column.totalsRowResult
34236
+ };
34237
+ else cell.value = null;
34238
+ }
34239
+ assignStyle(cell, column.style);
34240
+ });
34148
34241
  }
34149
- return sign + (whole * denom + numer) + "/" + denom;
34150
34242
  }
34151
- const denomMatch = fmt.match(/\/\s*(\?+)/);
34152
- const maxDigits = denomMatch ? denomMatch[1].length : 2;
34153
- const [whole, numer, denom] = toFraction(absVal, Math.pow(10, maxDigits) - 1);
34154
- if (fmt.includes("#") && whole !== 0) {
34155
- if (numer === 0) return sign + Math.abs(whole).toString();
34156
- return sign + Math.abs(whole) + " " + Math.abs(numer) + "/" + denom;
34243
+ load(worksheet) {
34244
+ const { table } = this;
34245
+ const { row, col } = table.tl;
34246
+ let count = 0;
34247
+ if (table.headerRow) {
34248
+ const r = worksheet.getRow(row + count++);
34249
+ table.columns.forEach((column, j) => {
34250
+ const cell = r.getCell(col + j);
34251
+ cell.value = column.name;
34252
+ });
34253
+ }
34254
+ table.rows.forEach((data) => {
34255
+ const r = worksheet.getRow(row + count++);
34256
+ data.forEach((value, j) => {
34257
+ const cell = r.getCell(col + j);
34258
+ cell.value = value;
34259
+ });
34260
+ });
34261
+ if (table.totalsRow) {
34262
+ const r = worksheet.getRow(row + count++);
34263
+ table.columns.forEach((column, j) => {
34264
+ const cell = r.getCell(col + j);
34265
+ if (j === 0) cell.value = column.totalsRowLabel;
34266
+ else {
34267
+ const formula = this.getFormula(column);
34268
+ if (formula) cell.value = {
34269
+ formula,
34270
+ result: column.totalsRowResult
34271
+ };
34272
+ }
34273
+ });
34274
+ }
34157
34275
  }
34158
- if (numer === 0) return whole === 0 ? "0" : sign + Math.abs(whole).toString();
34159
- return sign + (Math.abs(whole) * denom + Math.abs(numer)) + "/" + denom;
34160
- }
34161
- /**
34162
- * Format elapsed time (e.g., [h]:mm:ss for durations > 24 hours)
34163
- */
34164
- function formatElapsedTime(serial, fmt) {
34165
- const totalSeconds = Math.round(serial * 86400);
34166
- const totalMinutes = Math.floor(totalSeconds / 60);
34167
- const totalHours = Math.floor(totalMinutes / 60);
34168
- const seconds = totalSeconds % 60;
34169
- const minutes = totalMinutes % 60;
34170
- const hours = totalHours;
34171
- let result = fmt;
34172
- if (/\[h+\]/i.test(result)) result = result.replace(/\[h+\]/gi, hours.toString());
34173
- if (/\[m+\]/i.test(result)) result = result.replace(/\[m+\]/gi, totalMinutes.toString());
34174
- if (/\[s+\]/i.test(result)) result = result.replace(/\[s+\]/gi, totalSeconds.toString());
34175
- result = result.replace(/mm/gi, minutes.toString().padStart(2, "0"));
34176
- result = result.replace(/ss/gi, seconds.toString().padStart(2, "0"));
34177
- return result;
34178
- }
34179
- /**
34180
- * Format a number with the given pattern
34181
- * Handles patterns like "0", "00", "#,##0", "0-0", "000-0000" etc.
34182
- */
34183
- function formatNumberPattern(val, fmt) {
34184
- const absVal = Math.abs(val);
34185
- const sign = val < 0 ? "-" : "";
34186
- let trailingCommas = 0;
34187
- let workFmt = fmt;
34188
- while (workFmt.endsWith(",")) {
34189
- trailingCommas++;
34190
- workFmt = workFmt.slice(0, -1);
34276
+ get model() {
34277
+ return this.table;
34191
34278
  }
34192
- const scaledVal = absVal / Math.pow(1e3, trailingCommas);
34193
- const decimalIdx = workFmt.indexOf(".");
34194
- let intFmt = workFmt;
34195
- let decFmt = "";
34196
- if (decimalIdx !== -1) {
34197
- intFmt = workFmt.substring(0, decimalIdx);
34198
- decFmt = workFmt.substring(decimalIdx + 1);
34279
+ set model(value) {
34280
+ this.table = value;
34199
34281
  }
34200
- const decimalPlaces = decFmt.replace(/[^0#?]/g, "").length;
34201
- const roundedVal = roundTo(scaledVal, decimalPlaces);
34202
- if (roundedVal === 0 && !intFmt.includes("0") && !decFmt.includes("0")) {
34203
- let result = "";
34204
- for (const ch of intFmt) if (ch === "?") result += " ";
34205
- else if (ch !== "#" && ch !== ",") result += ch;
34206
- if (decimalPlaces > 0) {
34207
- if (/[0?]/.test(decFmt)) {
34208
- result += ".";
34209
- for (const ch of decFmt) if (ch === "?") result += " ";
34282
+ cacheState() {
34283
+ if (!this._cache) this._cache = {
34284
+ ref: this.ref,
34285
+ width: this.width,
34286
+ tableHeight: this.tableHeight
34287
+ };
34288
+ }
34289
+ commit() {
34290
+ if (!this._cache) return;
34291
+ this.validate();
34292
+ const ref = colCache.decodeAddress(this._cache.ref);
34293
+ if (this.ref !== this._cache.ref) for (let i = 0; i < this._cache.tableHeight; i++) {
34294
+ const row = this.worksheet.getRow(ref.row + i);
34295
+ for (let j = 0; j < this._cache.width; j++) {
34296
+ const cell = row.getCell(ref.col + j);
34297
+ cell.value = null;
34210
34298
  }
34211
34299
  }
34212
- return sign + result;
34213
- }
34214
- const [intPart, decPart = ""] = roundedVal.toString().split(".");
34215
- const hasLiteralInFormat = /[0#?][^0#?,.\s][0#?]/.test(intFmt);
34216
- let formattedInt;
34217
- if (hasLiteralInFormat) {
34218
- const digitPlaceholders = intFmt.replace(/[^0#?]/g, "").length;
34219
- let digits = intPart;
34220
- if (digits.length < digitPlaceholders) digits = "0".repeat(digitPlaceholders - digits.length) + digits;
34221
- formattedInt = "";
34222
- let digitIndex = digits.length - digitPlaceholders;
34223
- for (let i = 0; i < intFmt.length; i++) {
34224
- const char = intFmt[i];
34225
- if (char === "0" || char === "#" || char === "?") {
34226
- if (digitIndex < digits.length) {
34227
- formattedInt += digits[digitIndex];
34228
- digitIndex++;
34300
+ else {
34301
+ for (let i = this.tableHeight; i < this._cache.tableHeight; i++) {
34302
+ const row = this.worksheet.getRow(ref.row + i);
34303
+ for (let j = 0; j < this._cache.width; j++) {
34304
+ const cell = row.getCell(ref.col + j);
34305
+ cell.value = null;
34229
34306
  }
34230
- } else if (char !== ",") formattedInt += char;
34307
+ }
34308
+ for (let i = 0; i < this.tableHeight; i++) {
34309
+ const row = this.worksheet.getRow(ref.row + i);
34310
+ for (let j = this.width; j < this._cache.width; j++) {
34311
+ const cell = row.getCell(ref.col + j);
34312
+ cell.value = null;
34313
+ }
34314
+ }
34231
34315
  }
34232
- } else {
34233
- formattedInt = intPart;
34234
- if (intFmt.includes(",")) formattedInt = commaify(intPart);
34235
- const minIntDigits = (intFmt.match(/0/g) ?? []).length;
34236
- const totalIntSlots = (intFmt.match(/[0?]/g) ?? []).length;
34237
- if (formattedInt.length < minIntDigits) formattedInt = "0".repeat(minIntDigits - formattedInt.length) + formattedInt;
34238
- if (formattedInt.length < totalIntSlots) formattedInt = " ".repeat(totalIntSlots - formattedInt.length) + formattedInt;
34239
- if (formattedInt === "0" && minIntDigits === 0 && totalIntSlots === 0) formattedInt = "";
34316
+ this.store();
34317
+ this._cache = void 0;
34240
34318
  }
34241
- let formattedDec = "";
34242
- if (decimalPlaces > 0) {
34243
- const decChars = (decPart + "0".repeat(decimalPlaces)).substring(0, decimalPlaces).split("");
34244
- for (let i = decFmt.length - 1; i >= 0; i--) {
34245
- if (i >= decChars.length) continue;
34246
- if (decFmt[i] === "#" && decChars[i] === "0") decChars[i] = "";
34247
- else if (decFmt[i] === "?" && decChars[i] === "0") decChars[i] = " ";
34248
- else break;
34319
+ addRow(values, rowNumber, options) {
34320
+ this.cacheState();
34321
+ if (rowNumber === void 0) this.table.rows.push(values);
34322
+ else this.table.rows.splice(rowNumber, 0, values);
34323
+ if (options?.commit !== false) this.commit();
34324
+ }
34325
+ removeRows(rowIndex, count = 1, options) {
34326
+ this.cacheState();
34327
+ this.table.rows.splice(rowIndex, count);
34328
+ if (options?.commit !== false) this.commit();
34329
+ }
34330
+ getColumn(colIndex) {
34331
+ const column = this.table.columns[colIndex];
34332
+ return new Column$1(this, column, colIndex);
34333
+ }
34334
+ addColumn(column, values, colIndex) {
34335
+ this.cacheState();
34336
+ if (colIndex === void 0) {
34337
+ this.table.columns.push(column);
34338
+ this.table.rows.forEach((row, i) => {
34339
+ row.push(values[i]);
34340
+ });
34341
+ } else {
34342
+ this.table.columns.splice(colIndex, 0, column);
34343
+ this.table.rows.forEach((row, i) => {
34344
+ row.splice(colIndex, 0, values[i]);
34345
+ });
34249
34346
  }
34250
- const decStr = decChars.join("");
34251
- if (decStr.length > 0) formattedDec = "." + decStr;
34252
34347
  }
34253
- return sign + formattedInt + formattedDec;
34254
- }
34348
+ removeColumns(colIndex, count = 1) {
34349
+ this.cacheState();
34350
+ this.table.columns.splice(colIndex, count);
34351
+ this.table.rows.forEach((row) => {
34352
+ row.splice(colIndex, count);
34353
+ });
34354
+ }
34355
+ _assign(target, prop, value) {
34356
+ this.cacheState();
34357
+ target[prop] = value;
34358
+ }
34359
+ get ref() {
34360
+ return this.table.ref;
34361
+ }
34362
+ set ref(value) {
34363
+ this._assign(this.table, "ref", value);
34364
+ }
34365
+ get name() {
34366
+ return this.table.name;
34367
+ }
34368
+ set name(value) {
34369
+ this.cacheState();
34370
+ this.table.name = sanitizeTableName(value);
34371
+ }
34372
+ get displayName() {
34373
+ return this.table.displayName || this.table.name;
34374
+ }
34375
+ set displayName(value) {
34376
+ this.cacheState();
34377
+ this.table.displayName = sanitizeTableName(value);
34378
+ }
34379
+ get headerRow() {
34380
+ return this.table.headerRow;
34381
+ }
34382
+ set headerRow(value) {
34383
+ this._assign(this.table, "headerRow", value);
34384
+ }
34385
+ get totalsRow() {
34386
+ return this.table.totalsRow;
34387
+ }
34388
+ set totalsRow(value) {
34389
+ this._assign(this.table, "totalsRow", value);
34390
+ }
34391
+ _ensureStyle() {
34392
+ if (!this.table.style) this.table.style = {};
34393
+ return this.table.style;
34394
+ }
34395
+ get theme() {
34396
+ return this.table.style?.theme;
34397
+ }
34398
+ set theme(value) {
34399
+ this._ensureStyle().theme = value;
34400
+ }
34401
+ get showFirstColumn() {
34402
+ return this.table.style?.showFirstColumn;
34403
+ }
34404
+ set showFirstColumn(value) {
34405
+ this._ensureStyle().showFirstColumn = value;
34406
+ }
34407
+ get showLastColumn() {
34408
+ return this.table.style?.showLastColumn;
34409
+ }
34410
+ set showLastColumn(value) {
34411
+ this._ensureStyle().showLastColumn = value;
34412
+ }
34413
+ get showRowStripes() {
34414
+ return this.table.style?.showRowStripes;
34415
+ }
34416
+ set showRowStripes(value) {
34417
+ this._ensureStyle().showRowStripes = value;
34418
+ }
34419
+ get showColumnStripes() {
34420
+ return this.table.style?.showColumnStripes;
34421
+ }
34422
+ set showColumnStripes(value) {
34423
+ this._ensureStyle().showColumnStripes = value;
34424
+ }
34425
+ };
34426
+ //#endregion
34427
+ //#region src/modules/excel/utils/address.ts
34255
34428
  /**
34256
- * Remove quoted literal text markers and return the literal characters
34257
- * Also handles backslash escape sequences
34429
+ * Cell address encoding/decoding utilities (0-indexed)
34430
+ *
34431
+ * These functions use 0-indexed coordinates (column A = 0, row 1 = 0),
34432
+ * matching the convention used by SheetJS and most spreadsheet APIs.
34433
+ *
34434
+ * @module
34258
34435
  */
34259
- function processQuotedText(fmt) {
34260
- let result = "";
34261
- let i = 0;
34262
- while (i < fmt.length) if (fmt[i] === "\"") {
34263
- i++;
34264
- while (i < fmt.length && fmt[i] !== "\"") {
34265
- result += fmt[i];
34266
- i++;
34267
- }
34268
- i++;
34269
- } else if (fmt[i] === "\\" && i + 1 < fmt.length) {
34270
- i++;
34271
- result += fmt[i];
34272
- i++;
34273
- } else {
34274
- result += fmt[i];
34275
- i++;
34276
- }
34277
- return result;
34278
- }
34279
34436
  /**
34280
- * Check if a condition matches (e.g., [>100], [<=50])
34437
+ * Decode column string to 0-indexed number
34438
+ * @example decodeCol("A") // => 0
34439
+ * @example decodeCol("Z") // => 25
34440
+ * @example decodeCol("AA") // => 26
34281
34441
  */
34282
- function checkCondition(val, condition) {
34283
- const match = condition.match(/\[(=|>|<|>=|<=|<>)(-?\d+(?:\.\d*)?)\]/);
34284
- if (!match) return false;
34285
- const op = match[1];
34286
- const threshold = parseFloat(match[2]);
34287
- switch (op) {
34288
- case "=": return val === threshold;
34289
- case ">": return val > threshold;
34290
- case "<": return val < threshold;
34291
- case ">=": return val >= threshold;
34292
- case "<=": return val <= threshold;
34293
- case "<>": return val !== threshold;
34294
- default: return false;
34295
- }
34442
+ function decodeCol(colstr) {
34443
+ return colCache.l2n(colstr.toUpperCase()) - 1;
34296
34444
  }
34297
34445
  /**
34298
- * Parse format string and handle positive/negative/zero/text sections
34299
- * Excel format: positive;negative;zero;text
34300
- * Also handles conditional formats like [>100]
34446
+ * Encode 0-indexed column number to string
34447
+ * @example encodeCol(0) // => "A"
34448
+ * @example encodeCol(25) // => "Z"
34449
+ * @example encodeCol(26) // => "AA"
34301
34450
  */
34302
- function chooseFormat(fmt, val) {
34303
- if (typeof val === "string") {
34304
- const sections = splitFormatSections(fmt);
34305
- if (sections.length >= 4 && sections[3]) return processQuotedText(sections[3]).replace(/@/g, val);
34306
- return val;
34307
- }
34308
- if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
34309
- const sections = splitFormatSections(fmt);
34310
- const condRegex = /\[(=|>|<|>=|<=|<>)-?\d+(?:\.\d*)?\]/;
34311
- if ((sections[0] && condRegex.test(sections[0]) || sections[1] && condRegex.test(sections[1])) && sections.length >= 2) {
34312
- for (let i = 0; i < Math.min(sections.length, 2); i++) {
34313
- const condMatch = sections[i].match(/\[(=|>|<|>=|<=|<>)-?\d+(?:\.\d*)?\]/);
34314
- if (condMatch && checkCondition(val, condMatch[0])) return sections[i];
34315
- }
34316
- return sections[sections.length > 2 ? 2 : 1];
34317
- }
34318
- if (sections.length === 1) return sections[0];
34319
- if (sections.length === 2) return val >= 0 ? sections[0] : sections[1];
34320
- if (val > 0) return sections[0];
34321
- if (val < 0) return sections[1];
34322
- return sections[2] || sections[0];
34451
+ function encodeCol(col) {
34452
+ return colCache.n2l(col + 1);
34323
34453
  }
34324
34454
  /**
34325
- * Check if format section is for negative values (2nd section in multi-section format)
34455
+ * Decode row string to 0-indexed number
34456
+ * @example decodeRow("1") // => 0
34457
+ * @example decodeRow("10") // => 9
34326
34458
  */
34327
- function isNegativeSection(fmt, selectedFmt) {
34328
- const sections = splitFormatSections(fmt);
34329
- return sections.length >= 2 && sections[1] === selectedFmt;
34459
+ function decodeRow(rowstr) {
34460
+ return parseInt(rowstr, 10) - 1;
34330
34461
  }
34331
34462
  /**
34332
- * Main format function - formats a value according to Excel numFmt
34333
- * @param fmt The Excel number format string (e.g., "0.00%", "#,##0", "yyyy-mm-dd")
34334
- * @param val The value to format
34463
+ * Encode 0-indexed row number to string
34464
+ * @example encodeRow(0) // => "1"
34465
+ * @example encodeRow(9) // => "10"
34335
34466
  */
34336
- function format(fmt, val) {
34337
- if (val == null) return "";
34338
- if (isGeneral(fmt)) return formatGeneral(val);
34339
- if (typeof val === "string") return chooseFormat(fmt, val);
34340
- if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
34341
- let numVal = val;
34342
- const selectedFmt = chooseFormat(fmt, numVal);
34343
- if (numVal < 0 && isNegativeSection(fmt, selectedFmt)) numVal = Math.abs(numVal);
34344
- let cleanFmt = selectedFmt.replace(/\[(Red|Green|Blue|Yellow|Magenta|Cyan|White|Black|Color\d+)\]/gi, "");
34345
- cleanFmt = cleanFmt.replace(/\[(>|<|>=|<=|=|<>)-?\d+(\.\d+)?\]/g, "");
34346
- cleanFmt = cleanFmt.replace(/\[\$[^\]]*\]/g, "");
34347
- cleanFmt = processPlaceholders(cleanFmt);
34348
- cleanFmt = processQuotedText(cleanFmt);
34349
- if (/\[[hms]+\]/i.test(cleanFmt)) return formatElapsedTime(numVal, cleanFmt);
34350
- if (isDateFormat(cleanFmt)) return formatDate(numVal, cleanFmt);
34351
- if (cleanFmt.includes("%")) return formatPercentage(numVal, cleanFmt);
34352
- if (/E[+-]?/i.test(cleanFmt)) return formatScientific(numVal, cleanFmt);
34353
- if (/\?+\s*\/\s*[\d?]+/.test(cleanFmt)) return formatFraction(numVal, cleanFmt);
34354
- if (cleanFmt.includes("(") && cleanFmt.includes(")") && numVal < 0) {
34355
- const innerFmt = cleanFmt.replace(/\(|\)/g, "");
34356
- return "(" + formatNumberPattern(-numVal, innerFmt) + ")";
34357
- }
34358
- if (cleanFmt === "@") return numVal.toString();
34359
- let prefix = "";
34360
- let suffix = "";
34361
- const prefixMatch = cleanFmt.match(/^([^#0?.,]+)/);
34362
- if (prefixMatch) {
34363
- prefix = prefixMatch[1];
34364
- cleanFmt = cleanFmt.substring(prefixMatch[0].length);
34365
- }
34366
- const suffixMatch = cleanFmt.match(/([^#0?.,]+)$/);
34367
- if (suffixMatch && !suffixMatch[1].includes("%")) {
34368
- suffix = suffixMatch[1];
34369
- cleanFmt = cleanFmt.substring(0, cleanFmt.length - suffixMatch[0].length);
34370
- }
34371
- const formattedNum = formatNumberPattern(numVal, cleanFmt);
34372
- return prefix + formattedNum + suffix;
34467
+ function encodeRow(row) {
34468
+ return String(row + 1);
34373
34469
  }
34374
34470
  /**
34375
- * Check if format is a pure time format (no date components like y, m for month, d).
34376
- * Time formats only contain: h, m (minutes in time context), s, AM/PM.
34377
- * Excludes elapsed time formats like [h]:mm:ss which need the full serial number.
34471
+ * Decode cell address string to CellAddress object (0-indexed)
34472
+ * @example decodeCell("A1") // => { c: 0, r: 0 }
34473
+ * @example decodeCell("B2") // => { c: 1, r: 1 }
34378
34474
  */
34379
- function isTimeOnlyFormat(fmt) {
34380
- const cleaned = fmt.replace(/"[^"]*"/g, "");
34381
- if (/\[[hms]\]/i.test(cleaned)) return false;
34382
- const withoutBrackets = cleaned.replace(/\[[^\]]*\]/g, "");
34383
- const hasTimeComponents = /[hs]/i.test(withoutBrackets) || /AM\/PM|A\/P/i.test(withoutBrackets);
34384
- if (/[yd]/i.test(withoutBrackets)) return false;
34385
- if (/m/i.test(withoutBrackets) && !hasTimeComponents) return false;
34386
- return hasTimeComponents;
34475
+ function decodeCell(cstr) {
34476
+ const addr = colCache.decodeAddress(cstr.toUpperCase());
34477
+ return {
34478
+ c: addr.col - 1,
34479
+ r: addr.row - 1
34480
+ };
34387
34481
  }
34388
34482
  /**
34389
- * Check if format is a date format (contains y, d, or month-m).
34390
- * More precise than the internal isDateFormat correctly handles elapsed time
34391
- * formats like [h]:mm:ss (not a date format) and distinguishes month-m from minute-m.
34483
+ * Encode CellAddress object (0-indexed) to cell address string
34484
+ * @example encodeCell({ c: 0, r: 0 }) // => "A1"
34485
+ * @example encodeCell({ c: 1, r: 1 }) // => "B2"
34392
34486
  */
34393
- function isDateDisplayFormat(fmt) {
34394
- const cleaned = fmt.replace(/"[^"]*"/g, "");
34395
- if (/\[[hms]\]/i.test(cleaned)) return false;
34396
- const withoutBrackets = cleaned.replace(/\[[^\]]*\]/g, "");
34397
- if (/[yd]/i.test(withoutBrackets)) return true;
34398
- if (/m/i.test(withoutBrackets)) {
34399
- if (!(/[hs]/i.test(withoutBrackets) || /AM\/PM|A\/P/i.test(withoutBrackets))) return true;
34400
- }
34401
- return false;
34487
+ function encodeCell(cell) {
34488
+ return colCache.encodeAddress(cell.r + 1, cell.c + 1);
34402
34489
  }
34403
34490
  /**
34404
- * Format a value according to the given format string.
34405
- * Handles Date objects with timezone-independent Excel serial conversion.
34491
+ * Decode range string to SheetRange object (0-indexed)
34492
+ * @example decodeRange("A1:B2") // => { s: { c: 0, r: 0 }, e: { c: 1, r: 1 } }
34406
34493
  */
34407
- function formatCellValue(value, fmt, dateFormat) {
34408
- if (value instanceof Date) {
34409
- let serial = dateToExcel(value);
34410
- if (isTimeOnlyFormat(fmt)) {
34411
- serial = serial % 1;
34412
- if (serial < 0) serial += 1;
34413
- return format(fmt, serial);
34414
- }
34415
- return format(dateFormat && isDateDisplayFormat(fmt) ? dateFormat : fmt, serial);
34494
+ function decodeRange(range) {
34495
+ const idx = range.indexOf(":");
34496
+ if (idx === -1) {
34497
+ const cell = decodeCell(range);
34498
+ return {
34499
+ s: cell,
34500
+ e: { ...cell }
34501
+ };
34416
34502
  }
34417
- return format(fmt, value);
34503
+ return {
34504
+ s: decodeCell(range.slice(0, idx)),
34505
+ e: decodeCell(range.slice(idx + 1))
34506
+ };
34418
34507
  }
34419
- /**
34420
- * Get the formatted display text for a cell value.
34421
- *
34422
- * Handles primitive values, Date objects, formula results, and falls back to
34423
- * `cell.text` for complex types (rich text, hyperlinks, errors, etc.).
34424
- *
34425
- * @param cell - A cell (or cell-like object) with `.value`, `.numFmt`, and `.text`
34426
- * @param dateFormat - Optional custom date format override
34427
- */
34428
- function getCellDisplayText$1(cell, dateFormat) {
34429
- const value = cell.value;
34430
- const numFmt = cell.numFmt;
34431
- const fmt = typeof numFmt === "string" ? numFmt : numFmt?.formatCode ?? "General";
34432
- if (value == null) return "";
34433
- if (value instanceof Date || typeof value === "number" || typeof value === "boolean" || typeof value === "string") return formatCellValue(value, fmt, dateFormat);
34434
- if (typeof value === "object" && "formula" in value) {
34435
- const result = value.result;
34436
- if (result == null) return "";
34437
- if (result instanceof Date || typeof result === "number" || typeof result === "boolean" || typeof result === "string") return formatCellValue(result, fmt, dateFormat);
34508
+ function encodeRange(startOrRange, end) {
34509
+ if (end === void 0) {
34510
+ const range = startOrRange;
34511
+ return encodeRange(range.s, range.e);
34438
34512
  }
34439
- return cell.text;
34513
+ const startStr = encodeCell(startOrRange);
34514
+ const endStr = encodeCell(end);
34515
+ return startStr === endStr ? startStr : `${startStr}:${endStr}`;
34440
34516
  }
34441
34517
  //#endregion
34442
34518
  //#region src/modules/excel/utils/font-data.ts
@@ -47440,6 +47516,53 @@ self.onmessage = async function(event) {
47440
47516
  calculateFormulas() {
47441
47517
  invokeFormulaEngine(this);
47442
47518
  }
47519
+ /**
47520
+ * Register (or replace) a custom formula function on this workbook.
47521
+ *
47522
+ * The function becomes visible to `calculateFormulas()` on this
47523
+ * workbook only — the built-in registry stays untouched. Names are
47524
+ * case-insensitive (normalised to uppercase) and must not include
47525
+ * the `_XLFN.` prefix — the engine strips that automatically.
47526
+ *
47527
+ * @param name Function name (case-insensitive).
47528
+ * @param fn Implementation. Receives already-evaluated RuntimeValue
47529
+ * arguments; return a RuntimeValue. Wrap failures with
47530
+ * `rvError("#VALUE!")` rather than throwing — throws are
47531
+ * caught at the evaluator boundary and surface as
47532
+ * `#VALUE!` so a buggy custom function doesn't tear
47533
+ * down the whole calculation pass.
47534
+ * @param options Optional arity bounds. Defaults to `minArity=0`,
47535
+ * `maxArity=255` (Excel's universal argument cap), so
47536
+ * simple variadic functions work without extra config.
47537
+ * Set `volatile: true` when the function should be
47538
+ * re-evaluated on every calc cycle (analogous to
47539
+ * built-in `RAND`, `NOW`). Currently reserved for
47540
+ * future use; the engine recomputes every formula on
47541
+ * each `calculateFormulas()` call regardless.
47542
+ *
47543
+ * ```ts
47544
+ * import { rvNumber } from "@cj-tech-master/excelts/formula";
47545
+ * workbook.registerFunction("DOUBLE", ([x]) => {
47546
+ * return rvNumber((x as any).value * 2);
47547
+ * }, { minArity: 1, maxArity: 1 });
47548
+ * ```
47549
+ */
47550
+ registerFunction(name, fn, options) {
47551
+ if (!this.userFunctions) this.userFunctions = /* @__PURE__ */ new Map();
47552
+ this.userFunctions.set(name.toUpperCase(), {
47553
+ minArity: options?.minArity ?? 0,
47554
+ maxArity: options?.maxArity ?? 255,
47555
+ invoke: fn,
47556
+ volatile: options?.volatile ?? false
47557
+ });
47558
+ }
47559
+ /**
47560
+ * Remove a user-registered function. No-op when the name isn't
47561
+ * registered; returns `true` when an entry was removed.
47562
+ */
47563
+ unregisterFunction(name) {
47564
+ return this.userFunctions?.delete(name.toUpperCase()) ?? false;
47565
+ }
47443
47566
  clearThemes() {
47444
47567
  this._themes = void 0;
47445
47568
  }