@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
|
@@ -40,8 +40,17 @@ function registerFunction(desc) {
|
|
|
40
40
|
* `_XLFN._XLWS.` prefixed variants by stripping the prefix before lookup
|
|
41
41
|
* (a no-op for plain names, so plain lookups also go through a single
|
|
42
42
|
* Map.get call — avoiding the double-lookup pattern used previously).
|
|
43
|
+
*
|
|
44
|
+
* Fast-path: the overwhelming majority of lookups use plain names
|
|
45
|
+
* (`SUM`, `IF`, `VLOOKUP`, …). Checking the prefix sentinel byte up
|
|
46
|
+
* front lets those callers skip the `slice`/`startsWith` machinery in
|
|
47
|
+
* `stripFunctionPrefix` entirely.
|
|
43
48
|
*/
|
|
44
49
|
function lookupFunction(name) {
|
|
50
|
+
// Plain names don't start with `_` — skip the prefix-strip call.
|
|
51
|
+
if (name.length === 0 || name.charCodeAt(0) !== 95 /* `_` */) {
|
|
52
|
+
return registryMap.get(name);
|
|
53
|
+
}
|
|
45
54
|
return registryMap.get((0, token_types_1.stripFunctionPrefix)(name));
|
|
46
55
|
}
|
|
47
56
|
/**
|
|
@@ -111,20 +120,26 @@ function registerNativeInformationAndLogical() {
|
|
|
111
120
|
if (v.kind === 4 /* RVKind.Error */) {
|
|
112
121
|
return v;
|
|
113
122
|
}
|
|
114
|
-
|
|
123
|
+
// Excel coerces numeric strings and booleans for ISEVEN / ISODD
|
|
124
|
+
// (e.g. `ISEVEN("3")` → FALSE, `ISODD(TRUE)` → TRUE). Only genuine
|
|
125
|
+
// non-numeric text falls through to #VALUE!. Previously we rejected
|
|
126
|
+
// every non-Number kind outright.
|
|
127
|
+
const n = (0, values_1.toNumberRV)(v);
|
|
128
|
+
if (n.kind === 4 /* RVKind.Error */) {
|
|
115
129
|
return values_1.ERRORS.VALUE;
|
|
116
130
|
}
|
|
117
|
-
return (0, values_1.rvBoolean)(Math.floor(Math.abs(
|
|
131
|
+
return (0, values_1.rvBoolean)(Math.floor(Math.abs(n.value)) % 2 === 0);
|
|
118
132
|
});
|
|
119
133
|
defineEager("ISODD", 1, 1, args => {
|
|
120
134
|
const v = scalar(args);
|
|
121
135
|
if (v.kind === 4 /* RVKind.Error */) {
|
|
122
136
|
return v;
|
|
123
137
|
}
|
|
124
|
-
|
|
138
|
+
const n = (0, values_1.toNumberRV)(v);
|
|
139
|
+
if (n.kind === 4 /* RVKind.Error */) {
|
|
125
140
|
return values_1.ERRORS.VALUE;
|
|
126
141
|
}
|
|
127
|
-
return (0, values_1.rvBoolean)(Math.floor(Math.abs(
|
|
142
|
+
return (0, values_1.rvBoolean)(Math.floor(Math.abs(n.value)) % 2 === 1);
|
|
128
143
|
});
|
|
129
144
|
defineEager("N", 1, 1, args => {
|
|
130
145
|
const v = scalar(args);
|
|
@@ -175,6 +190,11 @@ function registerNativeInformationAndLogical() {
|
|
|
175
190
|
return map[v.code] !== undefined ? (0, values_1.rvNumber)(map[v.code]) : values_1.ERRORS.NA;
|
|
176
191
|
});
|
|
177
192
|
defineEager("NA", 0, 0, () => values_1.ERRORS.NA);
|
|
193
|
+
// TRUE() and FALSE() — Excel accepts both the literal and the
|
|
194
|
+
// zero-arg function form. The tokenizer routes `TRUE(` to a Function
|
|
195
|
+
// token, so we need to register these so the call binds.
|
|
196
|
+
defineEager("TRUE", 0, 0, () => (0, values_1.rvBoolean)(true));
|
|
197
|
+
defineEager("FALSE", 0, 0, () => (0, values_1.rvBoolean)(false));
|
|
178
198
|
// ── Stubs — limited implementations for functions that need runtime context ──
|
|
179
199
|
// INFO returns a handful of environment-describing strings. We implement
|
|
180
200
|
// the subset that's meaningful in a headless engine: `"release"` (engine
|
|
@@ -186,7 +206,10 @@ function registerNativeInformationAndLogical() {
|
|
|
186
206
|
if (args.length === 0) {
|
|
187
207
|
return values_1.ERRORS.NA;
|
|
188
208
|
}
|
|
189
|
-
|
|
209
|
+
// Implicit intersection — without topLeft, passing an array would
|
|
210
|
+
// route through the default `.value = ""` branch and silently
|
|
211
|
+
// surface #VALUE! instead of using the first cell.
|
|
212
|
+
const t = (0, values_1.topLeft)(args[0]);
|
|
190
213
|
if (t.kind === 4 /* RVKind.Error */) {
|
|
191
214
|
return t;
|
|
192
215
|
}
|
|
@@ -245,7 +268,10 @@ function registerNativeInformationAndLogical() {
|
|
|
245
268
|
if (url.kind === 4 /* RVKind.Error */) {
|
|
246
269
|
return url;
|
|
247
270
|
}
|
|
248
|
-
|
|
271
|
+
// Previously `String(url)` stringified a RuntimeValue object to
|
|
272
|
+
// the literal `"[object Object]"`. Route through `toStringRV`
|
|
273
|
+
// so numbers / booleans / blanks produce the expected text.
|
|
274
|
+
return (0, values_1.rvString)((0, values_1.toStringRV)(url));
|
|
249
275
|
}
|
|
250
276
|
if (display.kind === 2 /* RVKind.String */) {
|
|
251
277
|
return display;
|
|
@@ -270,6 +296,21 @@ function registerNativeInformationAndLogical() {
|
|
|
270
296
|
if (v.kind === 1 /* RVKind.Number */) {
|
|
271
297
|
return (0, values_1.rvBoolean)(v.value === 0);
|
|
272
298
|
}
|
|
299
|
+
// Excel accepts "TRUE" / "FALSE" strings (case-insensitive) and
|
|
300
|
+
// Blank cells (treated as FALSE). Previously any non-boolean,
|
|
301
|
+
// non-numeric kind fell through to #VALUE!.
|
|
302
|
+
if (v.kind === 0 /* RVKind.Blank */) {
|
|
303
|
+
return (0, values_1.rvBoolean)(true);
|
|
304
|
+
}
|
|
305
|
+
if (v.kind === 2 /* RVKind.String */) {
|
|
306
|
+
const upper = v.value.toUpperCase();
|
|
307
|
+
if (upper === "TRUE") {
|
|
308
|
+
return (0, values_1.rvBoolean)(false);
|
|
309
|
+
}
|
|
310
|
+
if (upper === "FALSE") {
|
|
311
|
+
return (0, values_1.rvBoolean)(true);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
273
314
|
return values_1.ERRORS.VALUE;
|
|
274
315
|
});
|
|
275
316
|
defineEager("AND", 1, 255, args => boolAggregate(args, true, (cur, val) => cur && val));
|
|
@@ -385,6 +426,11 @@ function registerNativeTextFunctions() {
|
|
|
385
426
|
defineEager("PROPER", 1, 1, text_1.fnPROPER);
|
|
386
427
|
defineEager("SUBSTITUTE", 3, 4, text_1.fnSUBSTITUTE);
|
|
387
428
|
defineEager("REPLACE", 4, 4, text_1.fnREPLACE);
|
|
429
|
+
// REPLACEB is REPLACE's double-byte alias. In non-DBCS locales Excel
|
|
430
|
+
// treats them identically, so aliasing to the same implementation
|
|
431
|
+
// matches behaviour without duplicating logic (matches the existing
|
|
432
|
+
// LEFTB / RIGHTB / MIDB / LENB / FINDB / SEARCHB wiring below).
|
|
433
|
+
defineEager("REPLACEB", 4, 4, text_1.fnREPLACE);
|
|
388
434
|
defineEager("FIND", 2, 3, text_1.fnFIND);
|
|
389
435
|
defineEager("FINDB", 2, 3, text_1.fnFIND);
|
|
390
436
|
defineEager("SEARCH", 2, 3, text_1.fnSEARCH);
|
|
@@ -698,12 +744,12 @@ function registerNativeMathFunctions() {
|
|
|
698
744
|
defineEager("SERIESSUM", 4, 4, math_1.fnSERIESSUM);
|
|
699
745
|
defineEager("ABS", 1, 1, math_1.fnABS);
|
|
700
746
|
defineEager("CEILING", 2, 2, math_1.fnCEILING);
|
|
701
|
-
defineEager("CEILING.MATH", 1, 3, math_1.
|
|
702
|
-
defineEager("CEILING.PRECISE", 1, 2, math_1.
|
|
703
|
-
defineEager("ISO.CEILING", 1, 2, math_1.
|
|
747
|
+
defineEager("CEILING.MATH", 1, 3, math_1.fnCEILING_MATH);
|
|
748
|
+
defineEager("CEILING.PRECISE", 1, 2, math_1.fnCEILING_PRECISE);
|
|
749
|
+
defineEager("ISO.CEILING", 1, 2, math_1.fnCEILING_PRECISE);
|
|
704
750
|
defineEager("FLOOR", 2, 2, math_1.fnFLOOR);
|
|
705
|
-
defineEager("FLOOR.MATH", 1, 3, math_1.
|
|
706
|
-
defineEager("FLOOR.PRECISE", 1, 2, math_1.
|
|
751
|
+
defineEager("FLOOR.MATH", 1, 3, math_1.fnFLOOR_MATH);
|
|
752
|
+
defineEager("FLOOR.PRECISE", 1, 2, math_1.fnFLOOR_PRECISE);
|
|
707
753
|
defineEager("INT", 1, 1, math_1.fnINT);
|
|
708
754
|
defineEager("MOD", 2, 2, math_1.fnMOD);
|
|
709
755
|
defineEager("POWER", 2, 2, math_1.fnPOWER);
|
|
@@ -24,6 +24,7 @@ exports.rvString = rvString;
|
|
|
24
24
|
exports.rvBoolean = rvBoolean;
|
|
25
25
|
exports.rvError = rvError;
|
|
26
26
|
exports.rvArray = rvArray;
|
|
27
|
+
exports.rvArrayRect = rvArrayRect;
|
|
27
28
|
exports.rvRef = rvRef;
|
|
28
29
|
exports.rvCellRef = rvCellRef;
|
|
29
30
|
exports.rvLambda = rvLambda;
|
|
@@ -112,10 +113,28 @@ function rvArray(rows, originRow, originCol, subtotalMask, hiddenRowMask) {
|
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
}
|
|
116
|
+
return buildArrayValue(normalisedRows, height, width, originRow, originCol, subtotalMask, hiddenRowMask);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Fast-path rectangular ArrayValue constructor.
|
|
120
|
+
*
|
|
121
|
+
* Callers that have already produced strictly-rectangular `rows` (every
|
|
122
|
+
* row is the same length — the length they explicitly `new Array(width)`
|
|
123
|
+
* allocated) can skip the two-pass width-scan + padding loop in
|
|
124
|
+
* `rvArray`. Examples: `buildRangeArray`, `broadcastBinaryOp`,
|
|
125
|
+
* `evaluateArrayLiteral`, `TRANSPOSE` — they all know `width` up front.
|
|
126
|
+
*
|
|
127
|
+
* Rows MUST be rectangular; passing ragged data will silently surface as
|
|
128
|
+
* `undefined` cells downstream.
|
|
129
|
+
*/
|
|
130
|
+
function rvArrayRect(rows, height, width, originRow, originCol, subtotalMask, hiddenRowMask) {
|
|
131
|
+
return buildArrayValue(rows, height, width, originRow, originCol, subtotalMask, hiddenRowMask);
|
|
132
|
+
}
|
|
133
|
+
function buildArrayValue(rows, height, width, originRow, originCol, subtotalMask, hiddenRowMask) {
|
|
115
134
|
return originRow !== undefined
|
|
116
135
|
? {
|
|
117
136
|
kind: 5 /* RVKind.Array */,
|
|
118
|
-
rows
|
|
137
|
+
rows,
|
|
119
138
|
height,
|
|
120
139
|
width,
|
|
121
140
|
originRow,
|
|
@@ -125,7 +144,7 @@ function rvArray(rows, originRow, originCol, subtotalMask, hiddenRowMask) {
|
|
|
125
144
|
}
|
|
126
145
|
: {
|
|
127
146
|
kind: 5 /* RVKind.Array */,
|
|
128
|
-
rows
|
|
147
|
+
rows,
|
|
129
148
|
height,
|
|
130
149
|
width,
|
|
131
150
|
...(subtotalMask ? { subtotalMask } : {}),
|
|
@@ -23,9 +23,15 @@ function prefixBindingPower(op) {
|
|
|
23
23
|
switch (op) {
|
|
24
24
|
case "+":
|
|
25
25
|
case "-":
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
26
|
+
// Excel's unary `-` binds TIGHTER than `^` — unique among
|
|
27
|
+
// spreadsheets and most programming languages. Microsoft's
|
|
28
|
+
// published precedence table places "Negation (as in –1)" at
|
|
29
|
+
// rank 1 (highest) and "Exponentiation" at rank 4.
|
|
30
|
+
// =-2^2 → (-2)^2 → 4 (NOT -(2^2) = -4)
|
|
31
|
+
// =-2^3 → (-2)^3 → -8
|
|
32
|
+
// Previously we used 55 which routed via `^` (60/61) and produced
|
|
33
|
+
// `-(2^3)` — matching most languages but not Excel.
|
|
34
|
+
return 70;
|
|
29
35
|
default:
|
|
30
36
|
return 0;
|
|
31
37
|
}
|
|
@@ -50,7 +56,11 @@ function infixBindingPower(op) {
|
|
|
50
56
|
case "/":
|
|
51
57
|
return [40, 41];
|
|
52
58
|
case "^":
|
|
53
|
-
|
|
59
|
+
// Excel is unusual: `^` is LEFT-associative (not right-associative
|
|
60
|
+
// like the math convention). `=2^3^2` evaluates to `(2^3)^2 = 64`,
|
|
61
|
+
// not `2^(3^2) = 512`. Using right-associative precedence silently
|
|
62
|
+
// diverged from Excel for any stacked exponent.
|
|
63
|
+
return [60, 61];
|
|
54
64
|
// Intersection operator — whitespace between two refs. In Excel
|
|
55
65
|
// precedence this sits between `:` (range, already handled at the
|
|
56
66
|
// tokenizer level) and unary +/-. Left-associative, binds tighter
|
|
@@ -247,12 +257,24 @@ class Parser {
|
|
|
247
257
|
this.next();
|
|
248
258
|
return { type: 4 /* NodeType.Error */, value: t.value };
|
|
249
259
|
}
|
|
250
|
-
// Parenthesized expression
|
|
260
|
+
// Parenthesized expression — also the syntactic entry point for
|
|
261
|
+
// reference unions: `(A1:B2, D4:E5)` is a multi-area reference
|
|
262
|
+
// that `INDEX(..., area_num)` can index into. A single expression
|
|
263
|
+
// inside parens is just an expression group (no UnionRef wrapper).
|
|
251
264
|
if (t.type === 10 /* TokenType.OpenParen */) {
|
|
252
265
|
this.next();
|
|
253
|
-
const
|
|
266
|
+
const first = this.parseExpr(0);
|
|
267
|
+
if (this.peek()?.type === 12 /* TokenType.Comma */) {
|
|
268
|
+
const areas = [first];
|
|
269
|
+
while (this.peek()?.type === 12 /* TokenType.Comma */) {
|
|
270
|
+
this.next(); // consume ','
|
|
271
|
+
areas.push(this.parseExpr(0));
|
|
272
|
+
}
|
|
273
|
+
this.expect(11 /* TokenType.CloseParen */);
|
|
274
|
+
return { type: 17 /* NodeType.UnionRef */, areas };
|
|
275
|
+
}
|
|
254
276
|
this.expect(11 /* TokenType.CloseParen */);
|
|
255
|
-
return
|
|
277
|
+
return first;
|
|
256
278
|
}
|
|
257
279
|
// Array constant: {1,2;3,4}
|
|
258
280
|
if (t.type === 15 /* TokenType.OpenBrace */) {
|
|
@@ -20,8 +20,17 @@ exports.stripFunctionPrefix = stripFunctionPrefix;
|
|
|
20
20
|
* The input may or may not already be uppercased — this helper does not
|
|
21
21
|
* alter case; callers that compare against an uppercase table should
|
|
22
22
|
* uppercase first (or compare case-insensitively).
|
|
23
|
+
*
|
|
24
|
+
* Fast path: plain names (99%+ of call sites) start with a letter, so
|
|
25
|
+
* checking the first code unit before the `startsWith` machinery lets
|
|
26
|
+
* those lookups skip the allocation.
|
|
23
27
|
*/
|
|
24
28
|
function stripFunctionPrefix(name) {
|
|
29
|
+
// `_` is code unit 95 — ASCII letters are 65..90 / 97..122. The XLFN
|
|
30
|
+
// prefix is the only legitimate name shape that begins with `_`.
|
|
31
|
+
if (name.length === 0 || name.charCodeAt(0) !== 95) {
|
|
32
|
+
return name;
|
|
33
|
+
}
|
|
25
34
|
if (name.startsWith("_XLFN._XLWS.")) {
|
|
26
35
|
return name.slice(12);
|
|
27
36
|
}
|
|
@@ -444,26 +444,56 @@ function tokenize(formula) {
|
|
|
444
444
|
// String literals
|
|
445
445
|
if (ch === '"') {
|
|
446
446
|
i++; // skip opening quote
|
|
447
|
-
|
|
447
|
+
// Fast path: walk forward looking for a closing quote. In the common
|
|
448
|
+
// case (no escaped quotes) we emit a single `slice` rather than
|
|
449
|
+
// growing a string byte by byte. The slow path falls back to the
|
|
450
|
+
// explicit escape-aware concat when we actually see `""`.
|
|
451
|
+
const start = i;
|
|
448
452
|
let closed = false;
|
|
453
|
+
let firstEscape = -1;
|
|
449
454
|
while (i < len) {
|
|
450
455
|
if (formula[i] === '"') {
|
|
451
456
|
if (i + 1 < len && formula[i + 1] === '"') {
|
|
452
|
-
|
|
453
|
-
str += '"';
|
|
454
|
-
i += 2;
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
i++; // skip closing quote
|
|
458
|
-
closed = true;
|
|
457
|
+
firstEscape = i;
|
|
459
458
|
break;
|
|
460
459
|
}
|
|
460
|
+
closed = true;
|
|
461
|
+
break;
|
|
461
462
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
463
|
+
i++;
|
|
464
|
+
}
|
|
465
|
+
let str;
|
|
466
|
+
if (firstEscape !== -1) {
|
|
467
|
+
// At least one escaped quote — use the classic byte-by-byte loop
|
|
468
|
+
// starting from the first escape so the prefix can still come
|
|
469
|
+
// from `slice`.
|
|
470
|
+
str = formula.slice(start, firstEscape);
|
|
471
|
+
i = firstEscape;
|
|
472
|
+
while (i < len) {
|
|
473
|
+
if (formula[i] === '"') {
|
|
474
|
+
if (i + 1 < len && formula[i + 1] === '"') {
|
|
475
|
+
str += '"';
|
|
476
|
+
i += 2;
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
i++;
|
|
480
|
+
closed = true;
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
str += formula[i];
|
|
486
|
+
i++;
|
|
487
|
+
}
|
|
465
488
|
}
|
|
466
489
|
}
|
|
490
|
+
else if (closed) {
|
|
491
|
+
str = formula.slice(start, i);
|
|
492
|
+
i++; // consume closing quote
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
str = formula.slice(start, i);
|
|
496
|
+
}
|
|
467
497
|
if (!closed) {
|
|
468
498
|
// Unterminated string literal — reject at tokenize time so we
|
|
469
499
|
// never hand the parser a truncated value that could alias to a
|
|
@@ -725,21 +755,48 @@ function tokenize(formula) {
|
|
|
725
755
|
// Quoted sheet name: 'Sheet Name'! or 3D ref 'Sheet1:Sheet3'!
|
|
726
756
|
if (ch === "'") {
|
|
727
757
|
i++; // skip opening quote
|
|
728
|
-
|
|
758
|
+
// Fast path: scan to the first `'` — most sheet names don't contain
|
|
759
|
+
// an escaped quote, so we can slice the prefix verbatim instead of
|
|
760
|
+
// growing a string byte-by-byte.
|
|
761
|
+
const start = i;
|
|
762
|
+
let firstEscape = -1;
|
|
729
763
|
while (i < len) {
|
|
730
764
|
if (formula[i] === "'") {
|
|
731
765
|
if (i + 1 < len && formula[i + 1] === "'") {
|
|
732
|
-
|
|
733
|
-
|
|
766
|
+
firstEscape = i;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
i++;
|
|
772
|
+
}
|
|
773
|
+
let sheetName;
|
|
774
|
+
if (firstEscape !== -1) {
|
|
775
|
+
sheetName = formula.slice(start, firstEscape);
|
|
776
|
+
i = firstEscape;
|
|
777
|
+
// Slow path from the first escape: consume `''` pairs and
|
|
778
|
+
// literal characters until the terminating single `'`.
|
|
779
|
+
while (i < len) {
|
|
780
|
+
if (formula[i] === "'") {
|
|
781
|
+
if (i + 1 < len && formula[i + 1] === "'") {
|
|
782
|
+
sheetName += "'";
|
|
783
|
+
i += 2;
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
i++;
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
734
789
|
}
|
|
735
790
|
else {
|
|
736
|
-
|
|
737
|
-
|
|
791
|
+
sheetName += formula[i];
|
|
792
|
+
i++;
|
|
738
793
|
}
|
|
739
794
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
sheetName = formula.slice(start, i);
|
|
798
|
+
if (i < len && formula[i] === "'") {
|
|
799
|
+
i++; // consume closing quote
|
|
743
800
|
}
|
|
744
801
|
}
|
|
745
802
|
// Expect ! after
|
package/dist/esm/index.js
CHANGED
|
@@ -45,6 +45,8 @@ export { DefinedNames } from "./modules/excel/defined-names.js";
|
|
|
45
45
|
// =============================================================================
|
|
46
46
|
// Cell address encoding/decoding (0-indexed)
|
|
47
47
|
export { decodeCol, encodeCol, decodeRow, encodeRow, decodeCell, encodeCell, decodeRange, encodeRange } from "./modules/excel/utils/address.js";
|
|
48
|
+
// Cell display-text helpers (apply numFmt to produce an Excel-style string)
|
|
49
|
+
export { getCellDisplayText, formatCellValue, isDateDisplayFormat } from "./modules/excel/utils/cell-format.js";
|
|
48
50
|
// Date conversion (Excel serial dates <-> JS Date)
|
|
49
51
|
export { dateToExcel, excelToDate } from "./utils/utils.base.js";
|
|
50
52
|
// Base64 utilities (cross-platform)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Enums } from "./enums.js";
|
|
2
2
|
import { ExcelError, InvalidValueTypeError } from "./errors.js";
|
|
3
3
|
import { Note } from "./note.js";
|
|
4
|
+
import { getCellDisplayText } from "./utils/cell-format.js";
|
|
4
5
|
import { colCache } from "./utils/col-cache.js";
|
|
5
6
|
import { copyStyle } from "./utils/copy-style.js";
|
|
6
7
|
import { slideFormula } from "./utils/shared-formula.js";
|
|
@@ -271,6 +272,26 @@ class Cell {
|
|
|
271
272
|
get text() {
|
|
272
273
|
return this._value.toString();
|
|
273
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* The cell's display text — the value formatted the way Excel would render
|
|
277
|
+
* it, applying the cell's `numFmt`. For a Date cell with `numFmt` `"mm-dd-yy"`,
|
|
278
|
+
* this returns e.g. `"04-12-19"` rather than the JS `Date.prototype.toString()`
|
|
279
|
+
* output you'd get from `cell.text`.
|
|
280
|
+
*
|
|
281
|
+
* Handles primitive values, dates, and formula results. For rich text,
|
|
282
|
+
* hyperlinks, errors, and other complex types, falls back to `cell.text`.
|
|
283
|
+
*
|
|
284
|
+
* Note: numFmt codes that are locale-dependent in Excel (e.g. built-in
|
|
285
|
+
* numFmtId 14 renders as `dd.mm.yyyy` under German locale but is stored
|
|
286
|
+
* as `mm-dd-yy`) are applied literally — excelts does not perform
|
|
287
|
+
* Excel's locale-based format substitution. If you need a specific date
|
|
288
|
+
* style across cells regardless of per-cell numFmts, call the exported
|
|
289
|
+
* {@link getCellDisplayText} helper with a `dateFormat` argument, or use
|
|
290
|
+
* `worksheet.toJSON({ dateFormat })`.
|
|
291
|
+
*/
|
|
292
|
+
get displayText() {
|
|
293
|
+
return getCellDisplayText(this);
|
|
294
|
+
}
|
|
274
295
|
get html() {
|
|
275
296
|
return escapeHtml(this.text);
|
|
276
297
|
}
|
|
@@ -20,7 +20,7 @@ const TABLE_FMT = {
|
|
|
20
20
|
11: "0.00E+00",
|
|
21
21
|
12: "# ?/?",
|
|
22
22
|
13: "# ??/??",
|
|
23
|
-
14: "
|
|
23
|
+
14: "mm-dd-yy",
|
|
24
24
|
15: "d-mmm-yy",
|
|
25
25
|
16: "d-mmm",
|
|
26
26
|
17: "mmm-yy",
|
|
@@ -210,6 +210,56 @@ const MONTHS_LONG = [
|
|
|
210
210
|
const MONTHS_LETTER = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"];
|
|
211
211
|
const DAYS_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
212
212
|
const DAYS_LONG = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
213
|
+
/**
|
|
214
|
+
* Disambiguate each `mm` occurrence in a format string that has already been
|
|
215
|
+
* placeholder-substituted for the other date/time tokens.
|
|
216
|
+
*
|
|
217
|
+
* Excel's rule: `mm` is minutes when it's adjacent to an hour or seconds
|
|
218
|
+
* token (with no intervening date tokens); otherwise it's a zero-padded
|
|
219
|
+
* month. This must be decided per occurrence — a single format string can
|
|
220
|
+
* contain both roles (e.g. `"yyyy-mm-dd hh:mm:ss"`).
|
|
221
|
+
*
|
|
222
|
+
* The caller has already replaced `yyyy`/`yy` → `Y4/Y2`, month-name tokens
|
|
223
|
+
* `mmmmm/mmmm/mmm` → `MN5/MN4/MN3`, `dd`/`d` → `D2/D1`, `hh`/`h` → `H2/H1`,
|
|
224
|
+
* `ss`/`s` → `S2/S1`. So any remaining literal `mm` substrings here are
|
|
225
|
+
* ambiguous between minute and month.
|
|
226
|
+
*
|
|
227
|
+
* Returns the input with each `mm` replaced by either `\x00MI2\x00` (minutes)
|
|
228
|
+
* or `\x00M2\x00` (month, zero-padded).
|
|
229
|
+
*/
|
|
230
|
+
function resolveMonthOrMinute(s) {
|
|
231
|
+
// Tokens that, when present between an `mm` and a time anchor, break the
|
|
232
|
+
// "adjacent time context" chain and push the `mm` back into month-land.
|
|
233
|
+
const DATE_TOKEN = /\x00(?:Y[24]|D[12]|MN[345])\x00/;
|
|
234
|
+
const HOUR_TOKEN = /\x00H[12]\x00/g;
|
|
235
|
+
const SEC_TOKEN = /\x00S[12]\x00/g;
|
|
236
|
+
let out = "";
|
|
237
|
+
let work = s;
|
|
238
|
+
let idx = work.search(/mm/i);
|
|
239
|
+
while (idx !== -1) {
|
|
240
|
+
const before = work.slice(0, idx);
|
|
241
|
+
const after = work.slice(idx + 2);
|
|
242
|
+
// Find the *nearest* hour token preceding this `mm` (scan from the right).
|
|
243
|
+
let nearestHourIdx = -1;
|
|
244
|
+
let m;
|
|
245
|
+
HOUR_TOKEN.lastIndex = 0;
|
|
246
|
+
while ((m = HOUR_TOKEN.exec(before)) !== null) {
|
|
247
|
+
nearestHourIdx = m.index;
|
|
248
|
+
}
|
|
249
|
+
// Find the *nearest* seconds token following this `mm`.
|
|
250
|
+
SEC_TOKEN.lastIndex = 0;
|
|
251
|
+
const secMatch = SEC_TOKEN.exec(after);
|
|
252
|
+
const nearestSecIdx = secMatch ? secMatch.index : -1;
|
|
253
|
+
const hourInRange = nearestHourIdx !== -1 && !DATE_TOKEN.test(before.slice(nearestHourIdx));
|
|
254
|
+
const secInRange = nearestSecIdx !== -1 && !DATE_TOKEN.test(after.slice(0, nearestSecIdx));
|
|
255
|
+
const isMinutes = hourInRange || secInRange;
|
|
256
|
+
out += before + (isMinutes ? "\x00MI2\x00" : "\x00M2\x00");
|
|
257
|
+
work = after;
|
|
258
|
+
idx = work.search(/mm/i);
|
|
259
|
+
}
|
|
260
|
+
out += work;
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
213
263
|
/**
|
|
214
264
|
* Format a date value using Excel date format
|
|
215
265
|
* @param serial Excel serial number (days since 1900-01-01)
|
|
@@ -270,16 +320,14 @@ function formatDate(serial, fmt) {
|
|
|
270
320
|
// Seconds (before mm to avoid confusion)
|
|
271
321
|
result = result.replace(/ss/gi, "\x00S2\x00");
|
|
272
322
|
result = result.replace(/\bs\b/gi, "\x00S1\x00");
|
|
273
|
-
// Minutes/Month mm -
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
result = result.replace(/mm/gi, "\x00M2\x00");
|
|
282
|
-
}
|
|
323
|
+
// Minutes/Month `mm` — position-dependent. Excel treats `mm` as minutes
|
|
324
|
+
// when the nearest neighboring time-token is an hour (before) or a
|
|
325
|
+
// seconds token (after); otherwise it's month. This must be decided **per
|
|
326
|
+
// occurrence**, because a single format string can contain both roles —
|
|
327
|
+
// e.g. in `"yyyy-mm-dd hh:mm:ss"` the first `mm` is month and the second
|
|
328
|
+
// is minutes. A single global `hasTimeContext` flag would miscategorise
|
|
329
|
+
// all `mm` as minutes in such mixed formats.
|
|
330
|
+
result = resolveMonthOrMinute(result);
|
|
283
331
|
result = result.replace(/\bm\b/gi, "\x00M1\x00");
|
|
284
332
|
// AM/PM
|
|
285
333
|
result = result.replace(/AM\/PM/gi, "\x00AMPM\x00");
|
|
@@ -887,6 +935,16 @@ export function isDateDisplayFormat(fmt) {
|
|
|
887
935
|
}
|
|
888
936
|
return false;
|
|
889
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* Default format applied to Date values whose numFmt is `General` or empty.
|
|
940
|
+
*
|
|
941
|
+
* Excel itself substitutes a locale-dependent short date in this case (US:
|
|
942
|
+
* `m/d/yyyy`). We pick an ISO-like `yyyy-mm-dd` so consumers who never set a
|
|
943
|
+
* `numFmt` still get a sensible, unambiguous rendering instead of the raw
|
|
944
|
+
* Excel serial number.
|
|
945
|
+
*/
|
|
946
|
+
const DEFAULT_DATE_FORMAT = "yyyy-mm-dd";
|
|
947
|
+
const DEFAULT_DATETIME_FORMAT = "yyyy-mm-dd hh:mm:ss";
|
|
890
948
|
/**
|
|
891
949
|
* Format a value according to the given format string.
|
|
892
950
|
* Handles Date objects with timezone-independent Excel serial conversion.
|
|
@@ -901,8 +959,22 @@ export function formatCellValue(value, fmt, dateFormat) {
|
|
|
901
959
|
}
|
|
902
960
|
return format(fmt, serial);
|
|
903
961
|
}
|
|
904
|
-
|
|
905
|
-
|
|
962
|
+
// For Date values whose numFmt is missing or General, Excel substitutes a
|
|
963
|
+
// default short-date format. Without this, `format("General", serial)`
|
|
964
|
+
// would emit the raw Excel serial (e.g. "43567") — almost never what the
|
|
965
|
+
// caller wants. Pick a datetime-aware default based on whether the value
|
|
966
|
+
// carries a non-midnight time component.
|
|
967
|
+
let effectiveFmt;
|
|
968
|
+
if (dateFormat && isDateDisplayFormat(fmt)) {
|
|
969
|
+
effectiveFmt = dateFormat;
|
|
970
|
+
}
|
|
971
|
+
else if (!fmt || isGeneral(fmt)) {
|
|
972
|
+
effectiveFmt = serial % 1 === 0 ? DEFAULT_DATE_FORMAT : DEFAULT_DATETIME_FORMAT;
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
effectiveFmt = fmt;
|
|
976
|
+
}
|
|
977
|
+
return format(effectiveFmt, serial);
|
|
906
978
|
}
|
|
907
979
|
return format(fmt, value);
|
|
908
980
|
}
|
|
@@ -1060,6 +1060,55 @@ class Workbook {
|
|
|
1060
1060
|
calculateFormulas() {
|
|
1061
1061
|
invokeFormulaEngine(this);
|
|
1062
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Register (or replace) a custom formula function on this workbook.
|
|
1065
|
+
*
|
|
1066
|
+
* The function becomes visible to `calculateFormulas()` on this
|
|
1067
|
+
* workbook only — the built-in registry stays untouched. Names are
|
|
1068
|
+
* case-insensitive (normalised to uppercase) and must not include
|
|
1069
|
+
* the `_XLFN.` prefix — the engine strips that automatically.
|
|
1070
|
+
*
|
|
1071
|
+
* @param name Function name (case-insensitive).
|
|
1072
|
+
* @param fn Implementation. Receives already-evaluated RuntimeValue
|
|
1073
|
+
* arguments; return a RuntimeValue. Wrap failures with
|
|
1074
|
+
* `rvError("#VALUE!")` rather than throwing — throws are
|
|
1075
|
+
* caught at the evaluator boundary and surface as
|
|
1076
|
+
* `#VALUE!` so a buggy custom function doesn't tear
|
|
1077
|
+
* down the whole calculation pass.
|
|
1078
|
+
* @param options Optional arity bounds. Defaults to `minArity=0`,
|
|
1079
|
+
* `maxArity=255` (Excel's universal argument cap), so
|
|
1080
|
+
* simple variadic functions work without extra config.
|
|
1081
|
+
* Set `volatile: true` when the function should be
|
|
1082
|
+
* re-evaluated on every calc cycle (analogous to
|
|
1083
|
+
* built-in `RAND`, `NOW`). Currently reserved for
|
|
1084
|
+
* future use; the engine recomputes every formula on
|
|
1085
|
+
* each `calculateFormulas()` call regardless.
|
|
1086
|
+
*
|
|
1087
|
+
* ```ts
|
|
1088
|
+
* import { rvNumber } from "@cj-tech-master/excelts/formula";
|
|
1089
|
+
* workbook.registerFunction("DOUBLE", ([x]) => {
|
|
1090
|
+
* return rvNumber((x as any).value * 2);
|
|
1091
|
+
* }, { minArity: 1, maxArity: 1 });
|
|
1092
|
+
* ```
|
|
1093
|
+
*/
|
|
1094
|
+
registerFunction(name, fn, options) {
|
|
1095
|
+
if (!this.userFunctions) {
|
|
1096
|
+
this.userFunctions = new Map();
|
|
1097
|
+
}
|
|
1098
|
+
this.userFunctions.set(name.toUpperCase(), {
|
|
1099
|
+
minArity: options?.minArity ?? 0,
|
|
1100
|
+
maxArity: options?.maxArity ?? 255,
|
|
1101
|
+
invoke: fn,
|
|
1102
|
+
volatile: options?.volatile ?? false
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Remove a user-registered function. No-op when the name isn't
|
|
1107
|
+
* registered; returns `true` when an entry was removed.
|
|
1108
|
+
*/
|
|
1109
|
+
unregisterFunction(name) {
|
|
1110
|
+
return this.userFunctions?.delete(name.toUpperCase()) ?? false;
|
|
1111
|
+
}
|
|
1063
1112
|
// ===========================================================================
|
|
1064
1113
|
// Themes
|
|
1065
1114
|
// ===========================================================================
|
|
@@ -17,7 +17,7 @@ const defaultNumFormats = {
|
|
|
17
17
|
19: { f: "h:mm:ss AM/PM" },
|
|
18
18
|
20: { f: "h:mm" },
|
|
19
19
|
21: { f: "h:mm:ss" },
|
|
20
|
-
22: { f:
|
|
20
|
+
22: { f: "m/d/yy h:mm" },
|
|
21
21
|
27: {
|
|
22
22
|
"zh-tw": "[$-404]e/m/d",
|
|
23
23
|
"zh-cn": 'yyyy"年"m"月"',
|
|
@@ -75,8 +75,8 @@ const defaultNumFormats = {
|
|
|
75
75
|
},
|
|
76
76
|
37: { f: "#,##0 ;(#,##0)" },
|
|
77
77
|
38: { f: "#,##0 ;[Red](#,##0)" },
|
|
78
|
-
39: { f: "#,##0.00
|
|
79
|
-
40: { f: "#,##0.00
|
|
78
|
+
39: { f: "#,##0.00;(#,##0.00)" },
|
|
79
|
+
40: { f: "#,##0.00;[Red](#,##0.00)" },
|
|
80
80
|
45: { f: "mm:ss" },
|
|
81
81
|
46: { f: "[h]:mm:ss" },
|
|
82
82
|
47: { f: "mmss.0" },
|