@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
|
@@ -29,7 +29,7 @@ const TABLE_FMT = {
|
|
|
29
29
|
11: "0.00E+00",
|
|
30
30
|
12: "# ?/?",
|
|
31
31
|
13: "# ??/??",
|
|
32
|
-
14: "
|
|
32
|
+
14: "mm-dd-yy",
|
|
33
33
|
15: "d-mmm-yy",
|
|
34
34
|
16: "d-mmm",
|
|
35
35
|
17: "mmm-yy",
|
|
@@ -219,6 +219,56 @@ const MONTHS_LONG = [
|
|
|
219
219
|
const MONTHS_LETTER = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"];
|
|
220
220
|
const DAYS_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
221
221
|
const DAYS_LONG = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
222
|
+
/**
|
|
223
|
+
* Disambiguate each `mm` occurrence in a format string that has already been
|
|
224
|
+
* placeholder-substituted for the other date/time tokens.
|
|
225
|
+
*
|
|
226
|
+
* Excel's rule: `mm` is minutes when it's adjacent to an hour or seconds
|
|
227
|
+
* token (with no intervening date tokens); otherwise it's a zero-padded
|
|
228
|
+
* month. This must be decided per occurrence — a single format string can
|
|
229
|
+
* contain both roles (e.g. `"yyyy-mm-dd hh:mm:ss"`).
|
|
230
|
+
*
|
|
231
|
+
* The caller has already replaced `yyyy`/`yy` → `Y4/Y2`, month-name tokens
|
|
232
|
+
* `mmmmm/mmmm/mmm` → `MN5/MN4/MN3`, `dd`/`d` → `D2/D1`, `hh`/`h` → `H2/H1`,
|
|
233
|
+
* `ss`/`s` → `S2/S1`. So any remaining literal `mm` substrings here are
|
|
234
|
+
* ambiguous between minute and month.
|
|
235
|
+
*
|
|
236
|
+
* Returns the input with each `mm` replaced by either `\x00MI2\x00` (minutes)
|
|
237
|
+
* or `\x00M2\x00` (month, zero-padded).
|
|
238
|
+
*/
|
|
239
|
+
function resolveMonthOrMinute(s) {
|
|
240
|
+
// Tokens that, when present between an `mm` and a time anchor, break the
|
|
241
|
+
// "adjacent time context" chain and push the `mm` back into month-land.
|
|
242
|
+
const DATE_TOKEN = /\x00(?:Y[24]|D[12]|MN[345])\x00/;
|
|
243
|
+
const HOUR_TOKEN = /\x00H[12]\x00/g;
|
|
244
|
+
const SEC_TOKEN = /\x00S[12]\x00/g;
|
|
245
|
+
let out = "";
|
|
246
|
+
let work = s;
|
|
247
|
+
let idx = work.search(/mm/i);
|
|
248
|
+
while (idx !== -1) {
|
|
249
|
+
const before = work.slice(0, idx);
|
|
250
|
+
const after = work.slice(idx + 2);
|
|
251
|
+
// Find the *nearest* hour token preceding this `mm` (scan from the right).
|
|
252
|
+
let nearestHourIdx = -1;
|
|
253
|
+
let m;
|
|
254
|
+
HOUR_TOKEN.lastIndex = 0;
|
|
255
|
+
while ((m = HOUR_TOKEN.exec(before)) !== null) {
|
|
256
|
+
nearestHourIdx = m.index;
|
|
257
|
+
}
|
|
258
|
+
// Find the *nearest* seconds token following this `mm`.
|
|
259
|
+
SEC_TOKEN.lastIndex = 0;
|
|
260
|
+
const secMatch = SEC_TOKEN.exec(after);
|
|
261
|
+
const nearestSecIdx = secMatch ? secMatch.index : -1;
|
|
262
|
+
const hourInRange = nearestHourIdx !== -1 && !DATE_TOKEN.test(before.slice(nearestHourIdx));
|
|
263
|
+
const secInRange = nearestSecIdx !== -1 && !DATE_TOKEN.test(after.slice(0, nearestSecIdx));
|
|
264
|
+
const isMinutes = hourInRange || secInRange;
|
|
265
|
+
out += before + (isMinutes ? "\x00MI2\x00" : "\x00M2\x00");
|
|
266
|
+
work = after;
|
|
267
|
+
idx = work.search(/mm/i);
|
|
268
|
+
}
|
|
269
|
+
out += work;
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
222
272
|
/**
|
|
223
273
|
* Format a date value using Excel date format
|
|
224
274
|
* @param serial Excel serial number (days since 1900-01-01)
|
|
@@ -279,16 +329,14 @@ function formatDate(serial, fmt) {
|
|
|
279
329
|
// Seconds (before mm to avoid confusion)
|
|
280
330
|
result = result.replace(/ss/gi, "\x00S2\x00");
|
|
281
331
|
result = result.replace(/\bs\b/gi, "\x00S1\x00");
|
|
282
|
-
// Minutes/Month mm -
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
result = result.replace(/mm/gi, "\x00M2\x00");
|
|
291
|
-
}
|
|
332
|
+
// Minutes/Month `mm` — position-dependent. Excel treats `mm` as minutes
|
|
333
|
+
// when the nearest neighboring time-token is an hour (before) or a
|
|
334
|
+
// seconds token (after); otherwise it's month. This must be decided **per
|
|
335
|
+
// occurrence**, because a single format string can contain both roles —
|
|
336
|
+
// e.g. in `"yyyy-mm-dd hh:mm:ss"` the first `mm` is month and the second
|
|
337
|
+
// is minutes. A single global `hasTimeContext` flag would miscategorise
|
|
338
|
+
// all `mm` as minutes in such mixed formats.
|
|
339
|
+
result = resolveMonthOrMinute(result);
|
|
292
340
|
result = result.replace(/\bm\b/gi, "\x00M1\x00");
|
|
293
341
|
// AM/PM
|
|
294
342
|
result = result.replace(/AM\/PM/gi, "\x00AMPM\x00");
|
|
@@ -896,6 +944,16 @@ function isDateDisplayFormat(fmt) {
|
|
|
896
944
|
}
|
|
897
945
|
return false;
|
|
898
946
|
}
|
|
947
|
+
/**
|
|
948
|
+
* Default format applied to Date values whose numFmt is `General` or empty.
|
|
949
|
+
*
|
|
950
|
+
* Excel itself substitutes a locale-dependent short date in this case (US:
|
|
951
|
+
* `m/d/yyyy`). We pick an ISO-like `yyyy-mm-dd` so consumers who never set a
|
|
952
|
+
* `numFmt` still get a sensible, unambiguous rendering instead of the raw
|
|
953
|
+
* Excel serial number.
|
|
954
|
+
*/
|
|
955
|
+
const DEFAULT_DATE_FORMAT = "yyyy-mm-dd";
|
|
956
|
+
const DEFAULT_DATETIME_FORMAT = "yyyy-mm-dd hh:mm:ss";
|
|
899
957
|
/**
|
|
900
958
|
* Format a value according to the given format string.
|
|
901
959
|
* Handles Date objects with timezone-independent Excel serial conversion.
|
|
@@ -910,8 +968,22 @@ function formatCellValue(value, fmt, dateFormat) {
|
|
|
910
968
|
}
|
|
911
969
|
return format(fmt, serial);
|
|
912
970
|
}
|
|
913
|
-
|
|
914
|
-
|
|
971
|
+
// For Date values whose numFmt is missing or General, Excel substitutes a
|
|
972
|
+
// default short-date format. Without this, `format("General", serial)`
|
|
973
|
+
// would emit the raw Excel serial (e.g. "43567") — almost never what the
|
|
974
|
+
// caller wants. Pick a datetime-aware default based on whether the value
|
|
975
|
+
// carries a non-midnight time component.
|
|
976
|
+
let effectiveFmt;
|
|
977
|
+
if (dateFormat && isDateDisplayFormat(fmt)) {
|
|
978
|
+
effectiveFmt = dateFormat;
|
|
979
|
+
}
|
|
980
|
+
else if (!fmt || isGeneral(fmt)) {
|
|
981
|
+
effectiveFmt = serial % 1 === 0 ? DEFAULT_DATE_FORMAT : DEFAULT_DATETIME_FORMAT;
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
effectiveFmt = fmt;
|
|
985
|
+
}
|
|
986
|
+
return format(effectiveFmt, serial);
|
|
915
987
|
}
|
|
916
988
|
return format(fmt, value);
|
|
917
989
|
}
|
|
@@ -1063,6 +1063,55 @@ class Workbook {
|
|
|
1063
1063
|
calculateFormulas() {
|
|
1064
1064
|
(0, host_registry_1.invokeFormulaEngine)(this);
|
|
1065
1065
|
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Register (or replace) a custom formula function on this workbook.
|
|
1068
|
+
*
|
|
1069
|
+
* The function becomes visible to `calculateFormulas()` on this
|
|
1070
|
+
* workbook only — the built-in registry stays untouched. Names are
|
|
1071
|
+
* case-insensitive (normalised to uppercase) and must not include
|
|
1072
|
+
* the `_XLFN.` prefix — the engine strips that automatically.
|
|
1073
|
+
*
|
|
1074
|
+
* @param name Function name (case-insensitive).
|
|
1075
|
+
* @param fn Implementation. Receives already-evaluated RuntimeValue
|
|
1076
|
+
* arguments; return a RuntimeValue. Wrap failures with
|
|
1077
|
+
* `rvError("#VALUE!")` rather than throwing — throws are
|
|
1078
|
+
* caught at the evaluator boundary and surface as
|
|
1079
|
+
* `#VALUE!` so a buggy custom function doesn't tear
|
|
1080
|
+
* down the whole calculation pass.
|
|
1081
|
+
* @param options Optional arity bounds. Defaults to `minArity=0`,
|
|
1082
|
+
* `maxArity=255` (Excel's universal argument cap), so
|
|
1083
|
+
* simple variadic functions work without extra config.
|
|
1084
|
+
* Set `volatile: true` when the function should be
|
|
1085
|
+
* re-evaluated on every calc cycle (analogous to
|
|
1086
|
+
* built-in `RAND`, `NOW`). Currently reserved for
|
|
1087
|
+
* future use; the engine recomputes every formula on
|
|
1088
|
+
* each `calculateFormulas()` call regardless.
|
|
1089
|
+
*
|
|
1090
|
+
* ```ts
|
|
1091
|
+
* import { rvNumber } from "@cj-tech-master/excelts/formula";
|
|
1092
|
+
* workbook.registerFunction("DOUBLE", ([x]) => {
|
|
1093
|
+
* return rvNumber((x as any).value * 2);
|
|
1094
|
+
* }, { minArity: 1, maxArity: 1 });
|
|
1095
|
+
* ```
|
|
1096
|
+
*/
|
|
1097
|
+
registerFunction(name, fn, options) {
|
|
1098
|
+
if (!this.userFunctions) {
|
|
1099
|
+
this.userFunctions = new Map();
|
|
1100
|
+
}
|
|
1101
|
+
this.userFunctions.set(name.toUpperCase(), {
|
|
1102
|
+
minArity: options?.minArity ?? 0,
|
|
1103
|
+
maxArity: options?.maxArity ?? 255,
|
|
1104
|
+
invoke: fn,
|
|
1105
|
+
volatile: options?.volatile ?? false
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Remove a user-registered function. No-op when the name isn't
|
|
1110
|
+
* registered; returns `true` when an entry was removed.
|
|
1111
|
+
*/
|
|
1112
|
+
unregisterFunction(name) {
|
|
1113
|
+
return this.userFunctions?.delete(name.toUpperCase()) ?? false;
|
|
1114
|
+
}
|
|
1066
1115
|
// ===========================================================================
|
|
1067
1116
|
// Themes
|
|
1068
1117
|
// ===========================================================================
|
|
@@ -20,7 +20,7 @@ const defaultNumFormats = {
|
|
|
20
20
|
19: { f: "h:mm:ss AM/PM" },
|
|
21
21
|
20: { f: "h:mm" },
|
|
22
22
|
21: { f: "h:mm:ss" },
|
|
23
|
-
22: { f:
|
|
23
|
+
22: { f: "m/d/yy h:mm" },
|
|
24
24
|
27: {
|
|
25
25
|
"zh-tw": "[$-404]e/m/d",
|
|
26
26
|
"zh-cn": 'yyyy"年"m"月"',
|
|
@@ -78,8 +78,8 @@ const defaultNumFormats = {
|
|
|
78
78
|
},
|
|
79
79
|
37: { f: "#,##0 ;(#,##0)" },
|
|
80
80
|
38: { f: "#,##0 ;[Red](#,##0)" },
|
|
81
|
-
39: { f: "#,##0.00
|
|
82
|
-
40: { f: "#,##0.00
|
|
81
|
+
39: { f: "#,##0.00;(#,##0.00)" },
|
|
82
|
+
40: { f: "#,##0.00;[Red](#,##0.00)" },
|
|
83
83
|
45: { f: "mm:ss" },
|
|
84
84
|
46: { f: "[h]:mm:ss" },
|
|
85
85
|
47: { f: "mmss.0" },
|
|
@@ -107,6 +107,8 @@ function bind(node, ctx) {
|
|
|
107
107
|
return bindName(node.name, ctx);
|
|
108
108
|
case 15 /* NodeType.StructuredRef */:
|
|
109
109
|
return bindStructuredRef(node.tableName, node.columns, node.specials, ctx);
|
|
110
|
+
case 17 /* NodeType.UnionRef */:
|
|
111
|
+
return bindUnionRef(node, ctx);
|
|
110
112
|
default:
|
|
111
113
|
return assertNever(node);
|
|
112
114
|
}
|
|
@@ -162,6 +164,11 @@ function bindRangeRef(node, ctx) {
|
|
|
162
164
|
const bottom = Math.max(startRow, endRow);
|
|
163
165
|
const left = Math.min(startCol, endCol);
|
|
164
166
|
const right = Math.max(startCol, endCol);
|
|
167
|
+
// Bounds-check the rectangle against Excel's sheet limits. Defined-name
|
|
168
|
+
// strings that bypass the tokenizer can carry arbitrary addresses.
|
|
169
|
+
if (top < 1 || bottom > 1048576 || left < 1 || right > 16384) {
|
|
170
|
+
return (0, bound_ast_1.boundErrorLiteral)("#REF!");
|
|
171
|
+
}
|
|
165
172
|
// 3D range reference: Sheet1:Sheet3!A1:B2
|
|
166
173
|
if (node.endSheet) {
|
|
167
174
|
const sheets = getSheetsInRange(ctx.snapshot, sheet, node.endSheet);
|
|
@@ -175,6 +182,13 @@ function bindRangeRef(node, ctx) {
|
|
|
175
182
|
inner
|
|
176
183
|
};
|
|
177
184
|
}
|
|
185
|
+
// Validate sheet exists — matches the parity check `bindCellRef` and
|
|
186
|
+
// `bindColRangeRef` / `bindRowRangeRef` perform. Without this, a range
|
|
187
|
+
// like `NoSuchSheet!A1:B2` would silently bind to an empty-read at
|
|
188
|
+
// runtime instead of surfacing as `#REF!` at compile time.
|
|
189
|
+
if (!ctx.snapshot.worksheetsByName.has(sheet.toLowerCase())) {
|
|
190
|
+
return (0, bound_ast_1.boundErrorLiteral)("#REF!");
|
|
191
|
+
}
|
|
178
192
|
return (0, bound_ast_1.boundAreaRef)(sheet, top, left, bottom, right);
|
|
179
193
|
}
|
|
180
194
|
function bindColRangeRef(node, ctx) {
|
|
@@ -183,6 +197,12 @@ function bindColRangeRef(node, ctx) {
|
|
|
183
197
|
const endCol = (0, address_utils_1.colLetterToNumber)(node.endCol);
|
|
184
198
|
const leftCol = Math.min(startCol, endCol);
|
|
185
199
|
const rightCol = Math.max(startCol, endCol);
|
|
200
|
+
// Excel's maximum column is 16384 (XFD). The tokenizer enforces this
|
|
201
|
+
// for plain refs, but defined-name range strings that bypass the
|
|
202
|
+
// tokenizer could carry larger letter sequences (e.g. `ZZZ`).
|
|
203
|
+
if (leftCol < 1 || rightCol > 16384) {
|
|
204
|
+
return (0, bound_ast_1.boundErrorLiteral)("#REF!");
|
|
205
|
+
}
|
|
186
206
|
// Validate sheet exists
|
|
187
207
|
if (!ctx.snapshot.worksheetsByName.has(sheet.toLowerCase())) {
|
|
188
208
|
return (0, bound_ast_1.boundErrorLiteral)("#REF!");
|
|
@@ -212,6 +232,11 @@ function bindRowRangeRef(node, ctx) {
|
|
|
212
232
|
const sheet = node.sheet ?? ctx.currentSheet;
|
|
213
233
|
const topRow = Math.min(node.startRow, node.endRow);
|
|
214
234
|
const bottomRow = Math.max(node.startRow, node.endRow);
|
|
235
|
+
// Excel's maximum row is 1048576. Re-check here because defined-name
|
|
236
|
+
// strings can bypass the tokenizer.
|
|
237
|
+
if (topRow < 1 || bottomRow > 1048576) {
|
|
238
|
+
return (0, bound_ast_1.boundErrorLiteral)("#REF!");
|
|
239
|
+
}
|
|
215
240
|
// Validate sheet exists
|
|
216
241
|
if (!ctx.snapshot.worksheetsByName.has(sheet.toLowerCase())) {
|
|
217
242
|
return (0, bound_ast_1.boundErrorLiteral)("#REF!");
|
|
@@ -237,6 +262,24 @@ function bindRowRangeRef(node, ctx) {
|
|
|
237
262
|
};
|
|
238
263
|
}
|
|
239
264
|
// ============================================================================
|
|
265
|
+
// Union Reference Binding — `(A1:B2, D4:E5)`
|
|
266
|
+
// ============================================================================
|
|
267
|
+
function bindUnionRef(node, ctx) {
|
|
268
|
+
// Each area must bind to a reference-producing expression. If any
|
|
269
|
+
// member is a non-reference literal, the whole union collapses to
|
|
270
|
+
// `#REF!` — Excel rejects things like `(1, A1)` outright. We defer
|
|
271
|
+
// the runtime-reference check (INDIRECT/OFFSET) to the evaluator.
|
|
272
|
+
const bounds = [];
|
|
273
|
+
for (const area of node.areas) {
|
|
274
|
+
const bound = bind(area, ctx);
|
|
275
|
+
bounds.push(bound);
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
kind: 16 /* BoundExprKind.UnionRef */,
|
|
279
|
+
areas: bounds
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
// ============================================================================
|
|
240
283
|
// Name Binding
|
|
241
284
|
// ============================================================================
|
|
242
285
|
function bindName(name, ctx) {
|
|
@@ -326,12 +369,11 @@ function findTable(snapshot, tableName) {
|
|
|
326
369
|
if (!tableName) {
|
|
327
370
|
return null;
|
|
328
371
|
}
|
|
329
|
-
// Use the pre-built tablesByName index for O(1) lookup
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return null;
|
|
372
|
+
// Use the pre-built tablesByName index for O(1) lookup. The snapshot's
|
|
373
|
+
// `ResolvedTable` already matches our `TableWithSheet` shape (same
|
|
374
|
+
// `{ table, sheetName }` pair), so we can return it directly instead
|
|
375
|
+
// of wrapping every hit in a fresh object.
|
|
376
|
+
return snapshot.tablesByName.get(tableName.toLowerCase()) ?? null;
|
|
335
377
|
}
|
|
336
378
|
function resolveStructuredRefBounds(tw, columns, specials) {
|
|
337
379
|
const t = tw.table;
|
|
@@ -213,6 +213,13 @@ function walkDeps(expr, cells, areas, tablesByName, nameResolver, visitedNames)
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
break;
|
|
216
|
+
case 16 /* BoundExprKind.UnionRef */:
|
|
217
|
+
// Each member of a `(a1, a2, ...)` union contributes its own
|
|
218
|
+
// dependencies — downstream reads target cells in every area.
|
|
219
|
+
for (const area of expr.areas) {
|
|
220
|
+
walkDeps(area, cells, areas, tablesByName, nameResolver, visitedNames);
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
216
223
|
}
|
|
217
224
|
}
|
|
218
225
|
// ============================================================================
|
|
@@ -279,9 +286,15 @@ function detectDynamicArrayFunction(ast, bound) {
|
|
|
279
286
|
return true;
|
|
280
287
|
}
|
|
281
288
|
}
|
|
282
|
-
// Check bound expression level
|
|
283
|
-
|
|
284
|
-
|
|
289
|
+
// Check bound expression level. Strip `_XLFN.` prefix here too —
|
|
290
|
+
// `boundCall` preserves the prefix on the bound name, so without the
|
|
291
|
+
// strip a synthesised bound call (e.g. from INDIRECT re-parse) would
|
|
292
|
+
// miss detection.
|
|
293
|
+
if (bound.kind === 10 /* BoundExprKind.Call */) {
|
|
294
|
+
const canonical = (0, token_types_1.stripFunctionPrefix)(bound.name);
|
|
295
|
+
if (DYNAMIC_ARRAY_FUNCTION_NAMES.has(canonical)) {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
285
298
|
}
|
|
286
299
|
return false;
|
|
287
300
|
}
|
|
@@ -302,9 +315,15 @@ function detectSubtotalOutput(ast, bound) {
|
|
|
302
315
|
return true;
|
|
303
316
|
}
|
|
304
317
|
}
|
|
305
|
-
if (bound.kind === 10 /* BoundExprKind.Call */
|
|
306
|
-
|
|
307
|
-
|
|
318
|
+
if (bound.kind === 10 /* BoundExprKind.Call */) {
|
|
319
|
+
// Strip `_XLFN.` / `_XLFN._XLWS.` prefixes before matching — otherwise
|
|
320
|
+
// `_XLFN.AGGREGATE(...)` silently wouldn't be marked as a subtotal
|
|
321
|
+
// output, so an outer SUBTOTAL / AGGREGATE over its cell would
|
|
322
|
+
// double-count the aggregated value.
|
|
323
|
+
const canonical = (0, token_types_1.stripFunctionPrefix)(bound.name);
|
|
324
|
+
if (canonical === "SUBTOTAL" || canonical === "AGGREGATE") {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
308
327
|
}
|
|
309
328
|
return false;
|
|
310
329
|
}
|
|
@@ -323,15 +342,24 @@ function analyzeExpr(expr, nameResolver) {
|
|
|
323
342
|
return { isVolatile, hasDynamicRefs, containsLambda };
|
|
324
343
|
function walkAnalyze(e) {
|
|
325
344
|
switch (e.kind) {
|
|
326
|
-
case 10 /* BoundExprKind.Call */:
|
|
327
|
-
|
|
345
|
+
case 10 /* BoundExprKind.Call */: {
|
|
346
|
+
// `boundCall` stores the function name uppercased but preserves
|
|
347
|
+
// any `_XLFN.` / `_XLFN._XLWS.` prefix the source text contained.
|
|
348
|
+
// Strip the prefix before VOLATILE_FUNCTIONS lookup so e.g.
|
|
349
|
+
// `_XLFN.RANDARRAY()` (an XLFN-prefixed volatile) correctly
|
|
350
|
+
// invalidates the session cache across calc cycles.
|
|
351
|
+
const canonical = (0, token_types_1.stripFunctionPrefix)(e.name);
|
|
352
|
+
if (VOLATILE_FUNCTIONS.has(canonical)) {
|
|
328
353
|
isVolatile = true;
|
|
329
354
|
}
|
|
330
355
|
for (const arg of e.args) {
|
|
331
356
|
walkAnalyze(arg);
|
|
332
357
|
}
|
|
333
358
|
break;
|
|
359
|
+
}
|
|
334
360
|
case 11 /* BoundExprKind.SpecialCall */:
|
|
361
|
+
// Special-call names are already stripped of any `_XLFN.` prefix
|
|
362
|
+
// by `canonicalSpecialForm` in the binder, so no re-strip here.
|
|
335
363
|
if (DYNAMIC_REF_FUNCTIONS.has(e.name)) {
|
|
336
364
|
hasDynamicRefs = true;
|
|
337
365
|
}
|
|
@@ -379,6 +407,11 @@ function analyzeExpr(expr, nameResolver) {
|
|
|
379
407
|
}
|
|
380
408
|
}
|
|
381
409
|
break;
|
|
410
|
+
case 16 /* BoundExprKind.UnionRef */:
|
|
411
|
+
for (const area of e.areas) {
|
|
412
|
+
walkAnalyze(area);
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
382
415
|
default:
|
|
383
416
|
// Literal, CellRef, AreaRef, etc. — no children to analyze
|
|
384
417
|
break;
|
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.checkError = checkError;
|
|
13
13
|
exports.argToNumber = argToNumber;
|
|
14
14
|
exports.flattenNumbers = flattenNumbers;
|
|
15
|
+
exports.forEachNumber = forEachNumber;
|
|
15
16
|
exports.flattenAll = flattenAll;
|
|
16
17
|
exports.firstError = firstError;
|
|
17
18
|
exports.asArray = asArray;
|
|
@@ -91,6 +92,53 @@ function flattenNumbers(args) {
|
|
|
91
92
|
}
|
|
92
93
|
return result;
|
|
93
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Streaming fold over numeric arguments.
|
|
97
|
+
*
|
|
98
|
+
* Same selection rules as `flattenNumbers` (array cells contribute only
|
|
99
|
+
* Number/Error; direct scalar coercion via `toNumberRV`; blanks dropped),
|
|
100
|
+
* but the caller's `onNumber` callback fires inline — no intermediate
|
|
101
|
+
* array is allocated. On the first error encountered the scan short-
|
|
102
|
+
* circuits and returns that error.
|
|
103
|
+
*
|
|
104
|
+
* Returns:
|
|
105
|
+
* - `null` when iteration finished without encountering an error, or
|
|
106
|
+
* - the `ErrorValue` that aborted the scan.
|
|
107
|
+
*
|
|
108
|
+
* Prefer this over `flattenNumbers` + `firstError` + manual loop in hot
|
|
109
|
+
* aggregates (SUM / AVERAGE / MIN / MAX / …). The allocation saved is
|
|
110
|
+
* one `NumberValue | ErrorValue` array per invocation — meaningful
|
|
111
|
+
* when the engine sums tens of thousands of cells.
|
|
112
|
+
*/
|
|
113
|
+
function forEachNumber(args, onNumber) {
|
|
114
|
+
for (const arg of args) {
|
|
115
|
+
if (arg.kind === 5 /* RVKind.Array */) {
|
|
116
|
+
for (const row of arg.rows) {
|
|
117
|
+
for (const cell of row) {
|
|
118
|
+
if (cell.kind === 4 /* RVKind.Error */) {
|
|
119
|
+
return cell;
|
|
120
|
+
}
|
|
121
|
+
if (cell.kind === 1 /* RVKind.Number */) {
|
|
122
|
+
onNumber(cell.value);
|
|
123
|
+
}
|
|
124
|
+
// Booleans, strings, blanks inside arrays are skipped.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (arg.kind === 4 /* RVKind.Error */) {
|
|
129
|
+
return arg;
|
|
130
|
+
}
|
|
131
|
+
else if (arg.kind !== 0 /* RVKind.Blank */) {
|
|
132
|
+
const n = (0, values_1.toNumberRV)(arg);
|
|
133
|
+
if (n.kind === 4 /* RVKind.Error */) {
|
|
134
|
+
return n;
|
|
135
|
+
}
|
|
136
|
+
onNumber(n.value);
|
|
137
|
+
}
|
|
138
|
+
// Direct blank scalars are dropped.
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
94
142
|
/**
|
|
95
143
|
* Flatten all cells from the arguments into a flat list of ScalarValue,
|
|
96
144
|
* preserving every cell (including blanks, errors, booleans, strings).
|