@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.
- package/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/modules/excel/cell.d.ts +18 -0
- package/dist/browser/modules/excel/cell.js +21 -0
- package/dist/browser/modules/excel/utils/cell-format.js +85 -13
- package/dist/browser/modules/excel/workbook.browser.d.ts +57 -0
- package/dist/browser/modules/excel/workbook.browser.js +49 -0
- package/dist/browser/modules/excel/xlsx/defaultnumformats.js +3 -3
- package/dist/browser/modules/formula/compile/binder.js +48 -6
- package/dist/browser/modules/formula/compile/bound-ast.d.ts +16 -2
- package/dist/browser/modules/formula/compile/bound-ast.js +1 -0
- package/dist/browser/modules/formula/compile/compiled-formula.js +41 -8
- package/dist/browser/modules/formula/functions/_shared.d.ts +19 -0
- package/dist/browser/modules/formula/functions/_shared.js +47 -0
- package/dist/browser/modules/formula/functions/conditional.js +103 -22
- package/dist/browser/modules/formula/functions/date.js +105 -23
- package/dist/browser/modules/formula/functions/dynamic-array.js +173 -69
- package/dist/browser/modules/formula/functions/engineering.d.ts +2 -2
- package/dist/browser/modules/formula/functions/engineering.js +103 -151
- package/dist/browser/modules/formula/functions/financial.js +210 -184
- package/dist/browser/modules/formula/functions/lookup.js +224 -157
- package/dist/browser/modules/formula/functions/math.d.ts +26 -0
- package/dist/browser/modules/formula/functions/math.js +249 -69
- package/dist/browser/modules/formula/functions/statistical.js +221 -171
- package/dist/browser/modules/formula/functions/text.js +112 -52
- package/dist/browser/modules/formula/integration/calculate-formulas-impl.js +20 -1
- package/dist/browser/modules/formula/materialize/build-writeback-plan.js +10 -6
- package/dist/browser/modules/formula/materialize/types.d.ts +15 -0
- package/dist/browser/modules/formula/runtime/evaluator.d.ts +8 -0
- package/dist/browser/modules/formula/runtime/evaluator.js +582 -162
- package/dist/browser/modules/formula/runtime/function-registry.d.ts +5 -0
- package/dist/browser/modules/formula/runtime/function-registry.js +59 -13
- package/dist/browser/modules/formula/runtime/values.d.ts +13 -0
- package/dist/browser/modules/formula/runtime/values.js +20 -2
- package/dist/browser/modules/formula/syntax/ast.d.ts +14 -2
- package/dist/browser/modules/formula/syntax/ast.js +1 -0
- package/dist/browser/modules/formula/syntax/parser.js +29 -7
- package/dist/browser/modules/formula/syntax/token-types.d.ts +4 -0
- package/dist/browser/modules/formula/syntax/token-types.js +9 -0
- package/dist/browser/modules/formula/syntax/tokenizer.js +76 -19
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/modules/excel/cell.js +21 -0
- package/dist/cjs/modules/excel/utils/cell-format.js +85 -13
- package/dist/cjs/modules/excel/workbook.browser.js +49 -0
- package/dist/cjs/modules/excel/xlsx/defaultnumformats.js +3 -3
- package/dist/cjs/modules/formula/compile/binder.js +48 -6
- package/dist/cjs/modules/formula/compile/compiled-formula.js +41 -8
- package/dist/cjs/modules/formula/functions/_shared.js +48 -0
- package/dist/cjs/modules/formula/functions/conditional.js +103 -22
- package/dist/cjs/modules/formula/functions/date.js +104 -22
- package/dist/cjs/modules/formula/functions/dynamic-array.js +173 -69
- package/dist/cjs/modules/formula/functions/engineering.js +109 -157
- package/dist/cjs/modules/formula/functions/financial.js +209 -183
- package/dist/cjs/modules/formula/functions/lookup.js +224 -157
- package/dist/cjs/modules/formula/functions/math.js +254 -70
- package/dist/cjs/modules/formula/functions/statistical.js +222 -172
- package/dist/cjs/modules/formula/functions/text.js +112 -52
- package/dist/cjs/modules/formula/integration/calculate-formulas-impl.js +20 -1
- package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +10 -6
- package/dist/cjs/modules/formula/runtime/evaluator.js +581 -161
- package/dist/cjs/modules/formula/runtime/function-registry.js +57 -11
- package/dist/cjs/modules/formula/runtime/values.js +21 -2
- package/dist/cjs/modules/formula/syntax/parser.js +29 -7
- package/dist/cjs/modules/formula/syntax/token-types.js +9 -0
- package/dist/cjs/modules/formula/syntax/tokenizer.js +76 -19
- package/dist/esm/index.js +2 -0
- package/dist/esm/modules/excel/cell.js +21 -0
- package/dist/esm/modules/excel/utils/cell-format.js +85 -13
- package/dist/esm/modules/excel/workbook.browser.js +49 -0
- package/dist/esm/modules/excel/xlsx/defaultnumformats.js +3 -3
- package/dist/esm/modules/formula/compile/binder.js +48 -6
- package/dist/esm/modules/formula/compile/bound-ast.js +1 -0
- package/dist/esm/modules/formula/compile/compiled-formula.js +41 -8
- package/dist/esm/modules/formula/functions/_shared.js +47 -0
- package/dist/esm/modules/formula/functions/conditional.js +103 -22
- package/dist/esm/modules/formula/functions/date.js +105 -23
- package/dist/esm/modules/formula/functions/dynamic-array.js +173 -69
- package/dist/esm/modules/formula/functions/engineering.js +103 -151
- package/dist/esm/modules/formula/functions/financial.js +210 -184
- package/dist/esm/modules/formula/functions/lookup.js +224 -157
- package/dist/esm/modules/formula/functions/math.js +249 -69
- package/dist/esm/modules/formula/functions/statistical.js +221 -171
- package/dist/esm/modules/formula/functions/text.js +112 -52
- package/dist/esm/modules/formula/integration/calculate-formulas-impl.js +20 -1
- package/dist/esm/modules/formula/materialize/build-writeback-plan.js +10 -6
- package/dist/esm/modules/formula/runtime/evaluator.js +582 -162
- package/dist/esm/modules/formula/runtime/function-registry.js +59 -13
- package/dist/esm/modules/formula/runtime/values.js +20 -2
- package/dist/esm/modules/formula/syntax/ast.js +1 -0
- package/dist/esm/modules/formula/syntax/parser.js +29 -7
- package/dist/esm/modules/formula/syntax/token-types.js +9 -0
- package/dist/esm/modules/formula/syntax/tokenizer.js +76 -19
- package/dist/iife/excelts.iife.js +1502 -1379
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +26 -26
- package/dist/types/index.d.ts +1 -0
- package/dist/types/modules/excel/cell.d.ts +18 -0
- package/dist/types/modules/excel/workbook.browser.d.ts +57 -0
- package/dist/types/modules/formula/compile/bound-ast.d.ts +16 -2
- package/dist/types/modules/formula/functions/_shared.d.ts +19 -0
- package/dist/types/modules/formula/functions/engineering.d.ts +2 -2
- package/dist/types/modules/formula/functions/math.d.ts +26 -0
- package/dist/types/modules/formula/materialize/types.d.ts +15 -0
- package/dist/types/modules/formula/runtime/evaluator.d.ts +8 -0
- package/dist/types/modules/formula/runtime/function-registry.d.ts +5 -0
- package/dist/types/modules/formula/runtime/values.d.ts +13 -0
- package/dist/types/modules/formula/syntax/ast.d.ts +14 -2
- package/dist/types/modules/formula/syntax/token-types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @cj-tech-master/excelts v9.
|
|
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: """,
|
|
17520
|
+
38: "&",
|
|
17521
|
+
39: "'",
|
|
17522
|
+
60: "<",
|
|
17523
|
+
62: ">"
|
|
17524
|
+
};
|
|
17525
|
+
/**
|
|
17526
|
+
* Decode XML entities in a string.
|
|
17527
|
+
*
|
|
17528
|
+
* Handles named entities (`<`, `>`, `&`, `"`, `'`)
|
|
17529
|
+
* and numeric character references (`{`, `{`).
|
|
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: """,
|
|
18860
|
-
38: "&",
|
|
18861
|
-
39: "'",
|
|
18862
|
-
60: "<",
|
|
18863
|
-
62: ">"
|
|
18864
|
-
};
|
|
18865
|
-
/**
|
|
18866
|
-
* Decode XML entities in a string.
|
|
18867
|
-
*
|
|
18868
|
-
* Handles named entities (`<`, `>`, `&`, `"`, `'`)
|
|
18869
|
-
* and numeric character references (`{`, `{`).
|
|
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
|
|
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
|
|
22038
|
-
40: { f: "#,##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
|
-
|
|
33763
|
-
this.
|
|
34066
|
+
get filterButton() {
|
|
34067
|
+
return this.column.filterButton;
|
|
33764
34068
|
}
|
|
33765
|
-
|
|
33766
|
-
|
|
33767
|
-
return this.table.style;
|
|
34069
|
+
set filterButton(value) {
|
|
34070
|
+
this.column.filterButton = value;
|
|
33768
34071
|
}
|
|
33769
|
-
get
|
|
33770
|
-
return this.
|
|
34072
|
+
get style() {
|
|
34073
|
+
return this.column.style;
|
|
33771
34074
|
}
|
|
33772
|
-
set
|
|
33773
|
-
this.
|
|
34075
|
+
set style(value) {
|
|
34076
|
+
this.column.style = value;
|
|
33774
34077
|
}
|
|
33775
|
-
get
|
|
33776
|
-
return this.
|
|
34078
|
+
get totalsRowLabel() {
|
|
34079
|
+
return this.column.totalsRowLabel;
|
|
33777
34080
|
}
|
|
33778
|
-
set
|
|
33779
|
-
this.
|
|
34081
|
+
set totalsRowLabel(value) {
|
|
34082
|
+
this._set("totalsRowLabel", value);
|
|
33780
34083
|
}
|
|
33781
|
-
get
|
|
33782
|
-
return this.
|
|
34084
|
+
get totalsRowFunction() {
|
|
34085
|
+
return this.column.totalsRowFunction;
|
|
33783
34086
|
}
|
|
33784
|
-
set
|
|
33785
|
-
this.
|
|
34087
|
+
set totalsRowFunction(value) {
|
|
34088
|
+
this._set("totalsRowFunction", value);
|
|
33786
34089
|
}
|
|
33787
|
-
get
|
|
33788
|
-
return this.
|
|
34090
|
+
get totalsRowResult() {
|
|
34091
|
+
return this.column.totalsRowResult;
|
|
33789
34092
|
}
|
|
33790
|
-
set
|
|
33791
|
-
this.
|
|
34093
|
+
set totalsRowResult(value) {
|
|
34094
|
+
this._set("totalsRowResult", value);
|
|
33792
34095
|
}
|
|
33793
|
-
get
|
|
33794
|
-
return this.
|
|
34096
|
+
get totalsRowFormula() {
|
|
34097
|
+
return this.column.totalsRowFormula;
|
|
33795
34098
|
}
|
|
33796
|
-
set
|
|
33797
|
-
this.
|
|
34099
|
+
set totalsRowFormula(value) {
|
|
34100
|
+
this._set("totalsRowFormula", value);
|
|
33798
34101
|
}
|
|
33799
34102
|
};
|
|
33800
|
-
|
|
33801
|
-
|
|
33802
|
-
|
|
33803
|
-
|
|
33804
|
-
|
|
33805
|
-
|
|
33806
|
-
|
|
33807
|
-
|
|
33808
|
-
|
|
33809
|
-
|
|
33810
|
-
|
|
33811
|
-
|
|
33812
|
-
|
|
33813
|
-
|
|
33814
|
-
|
|
33815
|
-
|
|
33816
|
-
|
|
33817
|
-
|
|
33818
|
-
|
|
33819
|
-
|
|
33820
|
-
|
|
33821
|
-
|
|
33822
|
-
|
|
33823
|
-
|
|
33824
|
-
|
|
33825
|
-
|
|
33826
|
-
|
|
33827
|
-
|
|
33828
|
-
|
|
33829
|
-
|
|
33830
|
-
|
|
33831
|
-
|
|
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
|
-
|
|
33878
|
-
|
|
33879
|
-
|
|
33880
|
-
|
|
33881
|
-
|
|
33882
|
-
|
|
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
|
-
|
|
33888
|
-
|
|
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
|
-
|
|
34030
|
-
|
|
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
|
-
|
|
34084
|
-
|
|
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
|
-
|
|
34122
|
-
|
|
34123
|
-
p = p1;
|
|
34153
|
+
get tableHeight() {
|
|
34154
|
+
return this.filterHeight + (this.table.totalsRow ? 1 : 0);
|
|
34124
34155
|
}
|
|
34125
|
-
|
|
34126
|
-
|
|
34127
|
-
|
|
34128
|
-
|
|
34129
|
-
|
|
34130
|
-
|
|
34131
|
-
|
|
34132
|
-
|
|
34133
|
-
|
|
34134
|
-
|
|
34135
|
-
|
|
34136
|
-
|
|
34137
|
-
|
|
34138
|
-
|
|
34139
|
-
|
|
34140
|
-
|
|
34141
|
-
const
|
|
34142
|
-
|
|
34143
|
-
|
|
34144
|
-
|
|
34145
|
-
|
|
34146
|
-
|
|
34147
|
-
|
|
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
|
-
|
|
34152
|
-
|
|
34153
|
-
|
|
34154
|
-
|
|
34155
|
-
if (
|
|
34156
|
-
|
|
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
|
-
|
|
34159
|
-
|
|
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
|
-
|
|
34193
|
-
|
|
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
|
-
|
|
34201
|
-
|
|
34202
|
-
|
|
34203
|
-
|
|
34204
|
-
|
|
34205
|
-
|
|
34206
|
-
|
|
34207
|
-
|
|
34208
|
-
|
|
34209
|
-
|
|
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
|
-
|
|
34213
|
-
|
|
34214
|
-
|
|
34215
|
-
|
|
34216
|
-
|
|
34217
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
34233
|
-
|
|
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
|
-
|
|
34242
|
-
|
|
34243
|
-
|
|
34244
|
-
|
|
34245
|
-
|
|
34246
|
-
|
|
34247
|
-
|
|
34248
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
34257
|
-
*
|
|
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
|
-
*
|
|
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
|
|
34283
|
-
|
|
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
|
-
*
|
|
34299
|
-
*
|
|
34300
|
-
*
|
|
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
|
|
34303
|
-
|
|
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
|
-
*
|
|
34455
|
+
* Decode row string to 0-indexed number
|
|
34456
|
+
* @example decodeRow("1") // => 0
|
|
34457
|
+
* @example decodeRow("10") // => 9
|
|
34326
34458
|
*/
|
|
34327
|
-
function
|
|
34328
|
-
|
|
34329
|
-
return sections.length >= 2 && sections[1] === selectedFmt;
|
|
34459
|
+
function decodeRow(rowstr) {
|
|
34460
|
+
return parseInt(rowstr, 10) - 1;
|
|
34330
34461
|
}
|
|
34331
34462
|
/**
|
|
34332
|
-
*
|
|
34333
|
-
* @
|
|
34334
|
-
* @
|
|
34463
|
+
* Encode 0-indexed row number to string
|
|
34464
|
+
* @example encodeRow(0) // => "1"
|
|
34465
|
+
* @example encodeRow(9) // => "10"
|
|
34335
34466
|
*/
|
|
34336
|
-
function
|
|
34337
|
-
|
|
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
|
-
*
|
|
34376
|
-
*
|
|
34377
|
-
*
|
|
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
|
|
34380
|
-
const
|
|
34381
|
-
|
|
34382
|
-
|
|
34383
|
-
|
|
34384
|
-
|
|
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
|
-
*
|
|
34390
|
-
*
|
|
34391
|
-
*
|
|
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
|
|
34394
|
-
|
|
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
|
-
*
|
|
34405
|
-
*
|
|
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
|
|
34408
|
-
|
|
34409
|
-
|
|
34410
|
-
|
|
34411
|
-
|
|
34412
|
-
|
|
34413
|
-
|
|
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
|
|
34503
|
+
return {
|
|
34504
|
+
s: decodeCell(range.slice(0, idx)),
|
|
34505
|
+
e: decodeCell(range.slice(idx + 1))
|
|
34506
|
+
};
|
|
34418
34507
|
}
|
|
34419
|
-
|
|
34420
|
-
|
|
34421
|
-
|
|
34422
|
-
|
|
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
|
-
|
|
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
|
}
|