@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
package/dist/browser/index.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export type { NodeInput } from "./modules/excel/stream/workbook-reader.js";
|
|
|
35
35
|
export { decodeCol, encodeCol, decodeRow, encodeRow, decodeCell, encodeCell, decodeRange, encodeRange } from "./modules/excel/utils/address.js";
|
|
36
36
|
export type { CellAddress, SheetRange, Origin } from "./modules/excel/utils/address.js";
|
|
37
37
|
export type { SheetToJSONOptions, AddJSONOptions, AddAOAOptions } from "./modules/excel/worksheet.js";
|
|
38
|
+
export { getCellDisplayText, formatCellValue, isDateDisplayFormat } from "./modules/excel/utils/cell-format.js";
|
|
38
39
|
export { dateToExcel, excelToDate } from "./utils/utils.base.js";
|
|
39
40
|
export { base64ToUint8Array, uint8ArrayToBase64 } from "./utils/utils.base.js";
|
|
40
41
|
export { xmlEncode, xmlDecode } from "./modules/xml/encode.js";
|
package/dist/browser/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)
|
|
@@ -130,6 +130,24 @@ declare class Cell {
|
|
|
130
130
|
get comment(): Note | undefined;
|
|
131
131
|
set comment(comment: Note | NoteConfig | undefined);
|
|
132
132
|
get text(): string;
|
|
133
|
+
/**
|
|
134
|
+
* The cell's display text — the value formatted the way Excel would render
|
|
135
|
+
* it, applying the cell's `numFmt`. For a Date cell with `numFmt` `"mm-dd-yy"`,
|
|
136
|
+
* this returns e.g. `"04-12-19"` rather than the JS `Date.prototype.toString()`
|
|
137
|
+
* output you'd get from `cell.text`.
|
|
138
|
+
*
|
|
139
|
+
* Handles primitive values, dates, and formula results. For rich text,
|
|
140
|
+
* hyperlinks, errors, and other complex types, falls back to `cell.text`.
|
|
141
|
+
*
|
|
142
|
+
* Note: numFmt codes that are locale-dependent in Excel (e.g. built-in
|
|
143
|
+
* numFmtId 14 renders as `dd.mm.yyyy` under German locale but is stored
|
|
144
|
+
* as `mm-dd-yy`) are applied literally — excelts does not perform
|
|
145
|
+
* Excel's locale-based format substitution. If you need a specific date
|
|
146
|
+
* style across cells regardless of per-cell numFmts, call the exported
|
|
147
|
+
* {@link getCellDisplayText} helper with a `dateFormat` argument, or use
|
|
148
|
+
* `worksheet.toJSON({ dateFormat })`.
|
|
149
|
+
*/
|
|
150
|
+
get displayText(): string;
|
|
133
151
|
get html(): string;
|
|
134
152
|
toString(): string;
|
|
135
153
|
get formula(): string | undefined;
|
|
@@ -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
|
}
|
|
@@ -603,6 +603,63 @@ declare class Workbook {
|
|
|
603
603
|
* ```
|
|
604
604
|
*/
|
|
605
605
|
calculateFormulas(): void;
|
|
606
|
+
/**
|
|
607
|
+
* Per-workbook registry of user-defined functions. The formula engine
|
|
608
|
+
* consults this map before the built-in 433-function registry, so a
|
|
609
|
+
* registered name either adds a new function (`MYFN`) or shadows a
|
|
610
|
+
* built-in (`IRR` → project-specific variant).
|
|
611
|
+
*
|
|
612
|
+
* Populated by {@link registerFunction}; read by the formula engine
|
|
613
|
+
* when the host calls `calculateFormulas()` — see
|
|
614
|
+
* `@formula/runtime/evaluator.ts::evaluateCall`.
|
|
615
|
+
*/
|
|
616
|
+
userFunctions?: Map<string, {
|
|
617
|
+
minArity: number;
|
|
618
|
+
maxArity: number;
|
|
619
|
+
invoke: (args: unknown[]) => unknown;
|
|
620
|
+
volatile?: boolean;
|
|
621
|
+
}>;
|
|
622
|
+
/**
|
|
623
|
+
* Register (or replace) a custom formula function on this workbook.
|
|
624
|
+
*
|
|
625
|
+
* The function becomes visible to `calculateFormulas()` on this
|
|
626
|
+
* workbook only — the built-in registry stays untouched. Names are
|
|
627
|
+
* case-insensitive (normalised to uppercase) and must not include
|
|
628
|
+
* the `_XLFN.` prefix — the engine strips that automatically.
|
|
629
|
+
*
|
|
630
|
+
* @param name Function name (case-insensitive).
|
|
631
|
+
* @param fn Implementation. Receives already-evaluated RuntimeValue
|
|
632
|
+
* arguments; return a RuntimeValue. Wrap failures with
|
|
633
|
+
* `rvError("#VALUE!")` rather than throwing — throws are
|
|
634
|
+
* caught at the evaluator boundary and surface as
|
|
635
|
+
* `#VALUE!` so a buggy custom function doesn't tear
|
|
636
|
+
* down the whole calculation pass.
|
|
637
|
+
* @param options Optional arity bounds. Defaults to `minArity=0`,
|
|
638
|
+
* `maxArity=255` (Excel's universal argument cap), so
|
|
639
|
+
* simple variadic functions work without extra config.
|
|
640
|
+
* Set `volatile: true` when the function should be
|
|
641
|
+
* re-evaluated on every calc cycle (analogous to
|
|
642
|
+
* built-in `RAND`, `NOW`). Currently reserved for
|
|
643
|
+
* future use; the engine recomputes every formula on
|
|
644
|
+
* each `calculateFormulas()` call regardless.
|
|
645
|
+
*
|
|
646
|
+
* ```ts
|
|
647
|
+
* import { rvNumber } from "@cj-tech-master/excelts/formula";
|
|
648
|
+
* workbook.registerFunction("DOUBLE", ([x]) => {
|
|
649
|
+
* return rvNumber((x as any).value * 2);
|
|
650
|
+
* }, { minArity: 1, maxArity: 1 });
|
|
651
|
+
* ```
|
|
652
|
+
*/
|
|
653
|
+
registerFunction(name: string, fn: (args: unknown[]) => unknown, options?: {
|
|
654
|
+
minArity?: number;
|
|
655
|
+
maxArity?: number;
|
|
656
|
+
volatile?: boolean;
|
|
657
|
+
}): void;
|
|
658
|
+
/**
|
|
659
|
+
* Remove a user-registered function. No-op when the name isn't
|
|
660
|
+
* registered; returns `true` when an entry was removed.
|
|
661
|
+
*/
|
|
662
|
+
unregisterFunction(name: string): boolean;
|
|
606
663
|
clearThemes(): void;
|
|
607
664
|
/**
|
|
608
665
|
* Add Image to Workbook and return the id
|
|
@@ -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" },
|
|
@@ -105,6 +105,8 @@ export function bind(node, ctx) {
|
|
|
105
105
|
return bindName(node.name, ctx);
|
|
106
106
|
case NodeType.StructuredRef:
|
|
107
107
|
return bindStructuredRef(node.tableName, node.columns, node.specials, ctx);
|
|
108
|
+
case NodeType.UnionRef:
|
|
109
|
+
return bindUnionRef(node, ctx);
|
|
108
110
|
default:
|
|
109
111
|
return assertNever(node);
|
|
110
112
|
}
|
|
@@ -160,6 +162,11 @@ function bindRangeRef(node, ctx) {
|
|
|
160
162
|
const bottom = Math.max(startRow, endRow);
|
|
161
163
|
const left = Math.min(startCol, endCol);
|
|
162
164
|
const right = Math.max(startCol, endCol);
|
|
165
|
+
// Bounds-check the rectangle against Excel's sheet limits. Defined-name
|
|
166
|
+
// strings that bypass the tokenizer can carry arbitrary addresses.
|
|
167
|
+
if (top < 1 || bottom > 1048576 || left < 1 || right > 16384) {
|
|
168
|
+
return boundErrorLiteral("#REF!");
|
|
169
|
+
}
|
|
163
170
|
// 3D range reference: Sheet1:Sheet3!A1:B2
|
|
164
171
|
if (node.endSheet) {
|
|
165
172
|
const sheets = getSheetsInRange(ctx.snapshot, sheet, node.endSheet);
|
|
@@ -173,6 +180,13 @@ function bindRangeRef(node, ctx) {
|
|
|
173
180
|
inner
|
|
174
181
|
};
|
|
175
182
|
}
|
|
183
|
+
// Validate sheet exists — matches the parity check `bindCellRef` and
|
|
184
|
+
// `bindColRangeRef` / `bindRowRangeRef` perform. Without this, a range
|
|
185
|
+
// like `NoSuchSheet!A1:B2` would silently bind to an empty-read at
|
|
186
|
+
// runtime instead of surfacing as `#REF!` at compile time.
|
|
187
|
+
if (!ctx.snapshot.worksheetsByName.has(sheet.toLowerCase())) {
|
|
188
|
+
return boundErrorLiteral("#REF!");
|
|
189
|
+
}
|
|
176
190
|
return boundAreaRef(sheet, top, left, bottom, right);
|
|
177
191
|
}
|
|
178
192
|
function bindColRangeRef(node, ctx) {
|
|
@@ -181,6 +195,12 @@ function bindColRangeRef(node, ctx) {
|
|
|
181
195
|
const endCol = colLetterToNumber(node.endCol);
|
|
182
196
|
const leftCol = Math.min(startCol, endCol);
|
|
183
197
|
const rightCol = Math.max(startCol, endCol);
|
|
198
|
+
// Excel's maximum column is 16384 (XFD). The tokenizer enforces this
|
|
199
|
+
// for plain refs, but defined-name range strings that bypass the
|
|
200
|
+
// tokenizer could carry larger letter sequences (e.g. `ZZZ`).
|
|
201
|
+
if (leftCol < 1 || rightCol > 16384) {
|
|
202
|
+
return boundErrorLiteral("#REF!");
|
|
203
|
+
}
|
|
184
204
|
// Validate sheet exists
|
|
185
205
|
if (!ctx.snapshot.worksheetsByName.has(sheet.toLowerCase())) {
|
|
186
206
|
return boundErrorLiteral("#REF!");
|
|
@@ -210,6 +230,11 @@ function bindRowRangeRef(node, ctx) {
|
|
|
210
230
|
const sheet = node.sheet ?? ctx.currentSheet;
|
|
211
231
|
const topRow = Math.min(node.startRow, node.endRow);
|
|
212
232
|
const bottomRow = Math.max(node.startRow, node.endRow);
|
|
233
|
+
// Excel's maximum row is 1048576. Re-check here because defined-name
|
|
234
|
+
// strings can bypass the tokenizer.
|
|
235
|
+
if (topRow < 1 || bottomRow > 1048576) {
|
|
236
|
+
return boundErrorLiteral("#REF!");
|
|
237
|
+
}
|
|
213
238
|
// Validate sheet exists
|
|
214
239
|
if (!ctx.snapshot.worksheetsByName.has(sheet.toLowerCase())) {
|
|
215
240
|
return boundErrorLiteral("#REF!");
|
|
@@ -235,6 +260,24 @@ function bindRowRangeRef(node, ctx) {
|
|
|
235
260
|
};
|
|
236
261
|
}
|
|
237
262
|
// ============================================================================
|
|
263
|
+
// Union Reference Binding — `(A1:B2, D4:E5)`
|
|
264
|
+
// ============================================================================
|
|
265
|
+
function bindUnionRef(node, ctx) {
|
|
266
|
+
// Each area must bind to a reference-producing expression. If any
|
|
267
|
+
// member is a non-reference literal, the whole union collapses to
|
|
268
|
+
// `#REF!` — Excel rejects things like `(1, A1)` outright. We defer
|
|
269
|
+
// the runtime-reference check (INDIRECT/OFFSET) to the evaluator.
|
|
270
|
+
const bounds = [];
|
|
271
|
+
for (const area of node.areas) {
|
|
272
|
+
const bound = bind(area, ctx);
|
|
273
|
+
bounds.push(bound);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
kind: BoundExprKind.UnionRef,
|
|
277
|
+
areas: bounds
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// ============================================================================
|
|
238
281
|
// Name Binding
|
|
239
282
|
// ============================================================================
|
|
240
283
|
function bindName(name, ctx) {
|
|
@@ -324,12 +367,11 @@ function findTable(snapshot, tableName) {
|
|
|
324
367
|
if (!tableName) {
|
|
325
368
|
return null;
|
|
326
369
|
}
|
|
327
|
-
// Use the pre-built tablesByName index for O(1) lookup
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
return null;
|
|
370
|
+
// Use the pre-built tablesByName index for O(1) lookup. The snapshot's
|
|
371
|
+
// `ResolvedTable` already matches our `TableWithSheet` shape (same
|
|
372
|
+
// `{ table, sheetName }` pair), so we can return it directly instead
|
|
373
|
+
// of wrapping every hit in a fresh object.
|
|
374
|
+
return snapshot.tablesByName.get(tableName.toLowerCase()) ?? null;
|
|
333
375
|
}
|
|
334
376
|
function resolveStructuredRefBounds(tw, columns, specials) {
|
|
335
377
|
const t = tw.table;
|
|
@@ -46,7 +46,8 @@ export declare const enum BoundExprKind {
|
|
|
46
46
|
Array = 12,
|
|
47
47
|
NameExpr = 13,
|
|
48
48
|
Lambda = 14,
|
|
49
|
-
StructuredRef = 15
|
|
49
|
+
StructuredRef = 15,
|
|
50
|
+
UnionRef = 16
|
|
50
51
|
}
|
|
51
52
|
/**
|
|
52
53
|
* A resolved literal value.
|
|
@@ -220,7 +221,20 @@ export interface BoundStructuredRef {
|
|
|
220
221
|
/** Special items (#Headers, #Data, #Totals, #All, #This Row). */
|
|
221
222
|
readonly specials: readonly string[];
|
|
222
223
|
}
|
|
223
|
-
|
|
224
|
+
/**
|
|
225
|
+
* A union of reference-producing sub-expressions — `(A1:B2, D4:E5)`.
|
|
226
|
+
*
|
|
227
|
+
* Produced only by parenthesised comma lists, and only used by callers
|
|
228
|
+
* that explicitly know how to consume a multi-area reference (INDEX's
|
|
229
|
+
* `area_num`, AREAS, union-operator arithmetic). Evaluating a
|
|
230
|
+
* UnionRef in any other context surfaces as `#VALUE!` since Excel
|
|
231
|
+
* forbids arithmetic / coercion on disjoint areas.
|
|
232
|
+
*/
|
|
233
|
+
export interface BoundUnionRef {
|
|
234
|
+
readonly kind: BoundExprKind.UnionRef;
|
|
235
|
+
readonly areas: readonly BoundExpr[];
|
|
236
|
+
}
|
|
237
|
+
export type BoundExpr = BoundLiteral | BoundCellRef | BoundAreaRef | BoundColRangeRef | BoundRowRangeRef | BoundRef3D | BoundBinaryOp | BoundUnaryOp | BoundPercent | BoundCall | BoundSpecialCall | BoundArray | BoundNameExpr | BoundLambda | BoundStructuredRef | BoundUnionRef;
|
|
224
238
|
export declare function boundLiteral(value: number | string | boolean | null, errorCode?: string): BoundLiteral;
|
|
225
239
|
export declare function boundCellRef(sheet: string, row: number, col: number): BoundCellRef;
|
|
226
240
|
export declare function boundAreaRef(sheet: string, top: number, left: number, bottom: number, right: number): BoundAreaRef;
|
|
@@ -51,6 +51,7 @@ export var BoundExprKind;
|
|
|
51
51
|
BoundExprKind[BoundExprKind["NameExpr"] = 13] = "NameExpr";
|
|
52
52
|
BoundExprKind[BoundExprKind["Lambda"] = 14] = "Lambda";
|
|
53
53
|
BoundExprKind[BoundExprKind["StructuredRef"] = 15] = "StructuredRef";
|
|
54
|
+
BoundExprKind[BoundExprKind["UnionRef"] = 16] = "UnionRef";
|
|
54
55
|
})(BoundExprKind || (BoundExprKind = {}));
|
|
55
56
|
// ============================================================================
|
|
56
57
|
// Constructor Helpers
|
|
@@ -209,6 +209,13 @@ function walkDeps(expr, cells, areas, tablesByName, nameResolver, visitedNames)
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
break;
|
|
212
|
+
case BoundExprKind.UnionRef:
|
|
213
|
+
// Each member of a `(a1, a2, ...)` union contributes its own
|
|
214
|
+
// dependencies — downstream reads target cells in every area.
|
|
215
|
+
for (const area of expr.areas) {
|
|
216
|
+
walkDeps(area, cells, areas, tablesByName, nameResolver, visitedNames);
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
212
219
|
}
|
|
213
220
|
}
|
|
214
221
|
// ============================================================================
|
|
@@ -275,9 +282,15 @@ export function detectDynamicArrayFunction(ast, bound) {
|
|
|
275
282
|
return true;
|
|
276
283
|
}
|
|
277
284
|
}
|
|
278
|
-
// Check bound expression level
|
|
279
|
-
|
|
280
|
-
|
|
285
|
+
// Check bound expression level. Strip `_XLFN.` prefix here too —
|
|
286
|
+
// `boundCall` preserves the prefix on the bound name, so without the
|
|
287
|
+
// strip a synthesised bound call (e.g. from INDIRECT re-parse) would
|
|
288
|
+
// miss detection.
|
|
289
|
+
if (bound.kind === BoundExprKind.Call) {
|
|
290
|
+
const canonical = stripFunctionPrefix(bound.name);
|
|
291
|
+
if (DYNAMIC_ARRAY_FUNCTION_NAMES.has(canonical)) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
281
294
|
}
|
|
282
295
|
return false;
|
|
283
296
|
}
|
|
@@ -298,9 +311,15 @@ export function detectSubtotalOutput(ast, bound) {
|
|
|
298
311
|
return true;
|
|
299
312
|
}
|
|
300
313
|
}
|
|
301
|
-
if (bound.kind === BoundExprKind.Call
|
|
302
|
-
|
|
303
|
-
|
|
314
|
+
if (bound.kind === BoundExprKind.Call) {
|
|
315
|
+
// Strip `_XLFN.` / `_XLFN._XLWS.` prefixes before matching — otherwise
|
|
316
|
+
// `_XLFN.AGGREGATE(...)` silently wouldn't be marked as a subtotal
|
|
317
|
+
// output, so an outer SUBTOTAL / AGGREGATE over its cell would
|
|
318
|
+
// double-count the aggregated value.
|
|
319
|
+
const canonical = stripFunctionPrefix(bound.name);
|
|
320
|
+
if (canonical === "SUBTOTAL" || canonical === "AGGREGATE") {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
304
323
|
}
|
|
305
324
|
return false;
|
|
306
325
|
}
|
|
@@ -319,15 +338,24 @@ export function analyzeExpr(expr, nameResolver) {
|
|
|
319
338
|
return { isVolatile, hasDynamicRefs, containsLambda };
|
|
320
339
|
function walkAnalyze(e) {
|
|
321
340
|
switch (e.kind) {
|
|
322
|
-
case BoundExprKind.Call:
|
|
323
|
-
|
|
341
|
+
case BoundExprKind.Call: {
|
|
342
|
+
// `boundCall` stores the function name uppercased but preserves
|
|
343
|
+
// any `_XLFN.` / `_XLFN._XLWS.` prefix the source text contained.
|
|
344
|
+
// Strip the prefix before VOLATILE_FUNCTIONS lookup so e.g.
|
|
345
|
+
// `_XLFN.RANDARRAY()` (an XLFN-prefixed volatile) correctly
|
|
346
|
+
// invalidates the session cache across calc cycles.
|
|
347
|
+
const canonical = stripFunctionPrefix(e.name);
|
|
348
|
+
if (VOLATILE_FUNCTIONS.has(canonical)) {
|
|
324
349
|
isVolatile = true;
|
|
325
350
|
}
|
|
326
351
|
for (const arg of e.args) {
|
|
327
352
|
walkAnalyze(arg);
|
|
328
353
|
}
|
|
329
354
|
break;
|
|
355
|
+
}
|
|
330
356
|
case BoundExprKind.SpecialCall:
|
|
357
|
+
// Special-call names are already stripped of any `_XLFN.` prefix
|
|
358
|
+
// by `canonicalSpecialForm` in the binder, so no re-strip here.
|
|
331
359
|
if (DYNAMIC_REF_FUNCTIONS.has(e.name)) {
|
|
332
360
|
hasDynamicRefs = true;
|
|
333
361
|
}
|
|
@@ -375,6 +403,11 @@ export function analyzeExpr(expr, nameResolver) {
|
|
|
375
403
|
}
|
|
376
404
|
}
|
|
377
405
|
break;
|
|
406
|
+
case BoundExprKind.UnionRef:
|
|
407
|
+
for (const area of e.areas) {
|
|
408
|
+
walkAnalyze(area);
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
378
411
|
default:
|
|
379
412
|
// Literal, CellRef, AreaRef, etc. — no children to analyze
|
|
380
413
|
break;
|
|
@@ -33,6 +33,25 @@ export declare function argToNumber(arg: RuntimeValue): NumberValue | ErrorValue
|
|
|
33
33
|
* `number[]` after an error check should map `.value` themselves.
|
|
34
34
|
*/
|
|
35
35
|
export declare function flattenNumbers(args: RuntimeValue[]): (NumberValue | ErrorValue)[];
|
|
36
|
+
/**
|
|
37
|
+
* Streaming fold over numeric arguments.
|
|
38
|
+
*
|
|
39
|
+
* Same selection rules as `flattenNumbers` (array cells contribute only
|
|
40
|
+
* Number/Error; direct scalar coercion via `toNumberRV`; blanks dropped),
|
|
41
|
+
* but the caller's `onNumber` callback fires inline — no intermediate
|
|
42
|
+
* array is allocated. On the first error encountered the scan short-
|
|
43
|
+
* circuits and returns that error.
|
|
44
|
+
*
|
|
45
|
+
* Returns:
|
|
46
|
+
* - `null` when iteration finished without encountering an error, or
|
|
47
|
+
* - the `ErrorValue` that aborted the scan.
|
|
48
|
+
*
|
|
49
|
+
* Prefer this over `flattenNumbers` + `firstError` + manual loop in hot
|
|
50
|
+
* aggregates (SUM / AVERAGE / MIN / MAX / …). The allocation saved is
|
|
51
|
+
* one `NumberValue | ErrorValue` array per invocation — meaningful
|
|
52
|
+
* when the engine sums tens of thousands of cells.
|
|
53
|
+
*/
|
|
54
|
+
export declare function forEachNumber(args: readonly RuntimeValue[], onNumber: (n: number) => void): ErrorValue | null;
|
|
36
55
|
/**
|
|
37
56
|
* Flatten all cells from the arguments into a flat list of ScalarValue,
|
|
38
57
|
* preserving every cell (including blanks, errors, booleans, strings).
|