@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
|
@@ -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;
|
|
@@ -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;
|
|
@@ -76,6 +76,53 @@ export function flattenNumbers(args) {
|
|
|
76
76
|
}
|
|
77
77
|
return result;
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Streaming fold over numeric arguments.
|
|
81
|
+
*
|
|
82
|
+
* Same selection rules as `flattenNumbers` (array cells contribute only
|
|
83
|
+
* Number/Error; direct scalar coercion via `toNumberRV`; blanks dropped),
|
|
84
|
+
* but the caller's `onNumber` callback fires inline — no intermediate
|
|
85
|
+
* array is allocated. On the first error encountered the scan short-
|
|
86
|
+
* circuits and returns that error.
|
|
87
|
+
*
|
|
88
|
+
* Returns:
|
|
89
|
+
* - `null` when iteration finished without encountering an error, or
|
|
90
|
+
* - the `ErrorValue` that aborted the scan.
|
|
91
|
+
*
|
|
92
|
+
* Prefer this over `flattenNumbers` + `firstError` + manual loop in hot
|
|
93
|
+
* aggregates (SUM / AVERAGE / MIN / MAX / …). The allocation saved is
|
|
94
|
+
* one `NumberValue | ErrorValue` array per invocation — meaningful
|
|
95
|
+
* when the engine sums tens of thousands of cells.
|
|
96
|
+
*/
|
|
97
|
+
export function forEachNumber(args, onNumber) {
|
|
98
|
+
for (const arg of args) {
|
|
99
|
+
if (arg.kind === RVKind.Array) {
|
|
100
|
+
for (const row of arg.rows) {
|
|
101
|
+
for (const cell of row) {
|
|
102
|
+
if (cell.kind === RVKind.Error) {
|
|
103
|
+
return cell;
|
|
104
|
+
}
|
|
105
|
+
if (cell.kind === RVKind.Number) {
|
|
106
|
+
onNumber(cell.value);
|
|
107
|
+
}
|
|
108
|
+
// Booleans, strings, blanks inside arrays are skipped.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (arg.kind === RVKind.Error) {
|
|
113
|
+
return arg;
|
|
114
|
+
}
|
|
115
|
+
else if (arg.kind !== RVKind.Blank) {
|
|
116
|
+
const n = toNumberRV(arg);
|
|
117
|
+
if (n.kind === RVKind.Error) {
|
|
118
|
+
return n;
|
|
119
|
+
}
|
|
120
|
+
onNumber(n.value);
|
|
121
|
+
}
|
|
122
|
+
// Direct blank scalars are dropped.
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
79
126
|
/**
|
|
80
127
|
* Flatten all cells from the arguments into a flat list of ScalarValue,
|
|
81
128
|
* preserving every cell (including blanks, errors, booleans, strings).
|
|
@@ -47,35 +47,57 @@ export function buildCriteriaPredicateRV(criteria) {
|
|
|
47
47
|
// blank (→0); numeric strings are NOT coerced (COUNTIF stays textual
|
|
48
48
|
// for those). Only real Number / Boolean / Blank cells participate in
|
|
49
49
|
// numeric comparisons; everything else falls back to string compare.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
if (isNum) {
|
|
51
|
+
// Specialised numeric-comparison path — avoids per-cell string
|
|
52
|
+
// coercion allocations that dominate the hot loop otherwise.
|
|
53
|
+
// Non-numeric cells produce NaN and fall through to the switch,
|
|
54
|
+
// where only `<>` evaluates NaN-relations to TRUE (Excel semantics).
|
|
55
|
+
return (v) => {
|
|
56
|
+
const vn = v.kind === RVKind.Number
|
|
57
|
+
? v.value
|
|
58
|
+
: v.kind === RVKind.Boolean
|
|
59
|
+
? v.value
|
|
60
|
+
? 1
|
|
61
|
+
: 0
|
|
62
|
+
: v.kind === RVKind.Blank
|
|
63
|
+
? 0
|
|
64
|
+
: Number.NaN;
|
|
65
|
+
switch (op) {
|
|
66
|
+
case "=":
|
|
67
|
+
return vn === numVal;
|
|
68
|
+
case "<>":
|
|
69
|
+
return vn !== numVal;
|
|
70
|
+
case ">":
|
|
71
|
+
return vn > numVal;
|
|
72
|
+
case "<":
|
|
73
|
+
return vn < numVal;
|
|
74
|
+
case ">=":
|
|
75
|
+
return vn >= numVal;
|
|
76
|
+
case "<=":
|
|
77
|
+
return vn <= numVal;
|
|
78
|
+
default:
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// String-comparison path — lowercase the criterion once outside the
|
|
84
|
+
// closure so every cell only pays a single `toStringRV().toLowerCase()`.
|
|
85
|
+
const cs = valStr.toLowerCase();
|
|
62
86
|
return (v) => {
|
|
63
|
-
const vn = numericOf(v);
|
|
64
87
|
const vs = toStringRV(v).toLowerCase();
|
|
65
|
-
const cs = valStr.toLowerCase();
|
|
66
88
|
switch (op) {
|
|
67
89
|
case "=":
|
|
68
|
-
return
|
|
90
|
+
return vs === cs;
|
|
69
91
|
case "<>":
|
|
70
|
-
return
|
|
92
|
+
return vs !== cs;
|
|
71
93
|
case ">":
|
|
72
|
-
return
|
|
94
|
+
return vs > cs;
|
|
73
95
|
case "<":
|
|
74
|
-
return
|
|
96
|
+
return vs < cs;
|
|
75
97
|
case ">=":
|
|
76
|
-
return
|
|
98
|
+
return vs >= cs;
|
|
77
99
|
case "<=":
|
|
78
|
-
return
|
|
100
|
+
return vs <= cs;
|
|
79
101
|
default:
|
|
80
102
|
return false;
|
|
81
103
|
}
|
|
@@ -85,14 +107,19 @@ export function buildCriteriaPredicateRV(criteria) {
|
|
|
85
107
|
// literal `*`, `?`, `~` and everything else as a regex special character
|
|
86
108
|
// that must be escaped. Only an unescaped `*` or `?` triggers the wildcard
|
|
87
109
|
// path; a pattern like `~*` matches a literal asterisk.
|
|
110
|
+
//
|
|
111
|
+
// Excel restricts wildcard matching to TEXT cells — a criterion like
|
|
112
|
+
// `"1*"` must not match the number 1 even though `String(1) === "1"`.
|
|
113
|
+
// Without this guard, `COUNTIF({1, 15, "15"}, "1*")` would return 3
|
|
114
|
+
// instead of the correct `1` (only the string `"15"` matches).
|
|
88
115
|
if (hasUnescapedWildcard(s)) {
|
|
89
116
|
try {
|
|
90
117
|
const re = new RegExp("^" + excelWildcardToRegex(s) + "$", "i");
|
|
91
|
-
return v => re.test(
|
|
118
|
+
return v => v.kind === RVKind.String && re.test(v.value);
|
|
92
119
|
}
|
|
93
120
|
catch {
|
|
94
121
|
const literal = unescapeExcelWildcard(s).toLowerCase();
|
|
95
|
-
return v =>
|
|
122
|
+
return v => v.kind === RVKind.String && v.value.toLowerCase() === literal;
|
|
96
123
|
}
|
|
97
124
|
}
|
|
98
125
|
// No wildcards: strip any `~` escapes and do a literal case-insensitive compare.
|
|
@@ -133,6 +160,12 @@ export function fnSUMIF(args) {
|
|
|
133
160
|
for (let c = 0; c < rangeArr.width; c++) {
|
|
134
161
|
if (pred(getCell(rangeArr, r, c))) {
|
|
135
162
|
const sv = getCell(sumArr, r, c);
|
|
163
|
+
// Excel propagates errors from the sum-range; previously we
|
|
164
|
+
// silently skipped them, masking `#DIV/0!` / `#VALUE!` cells
|
|
165
|
+
// under the aggregation.
|
|
166
|
+
if (sv.kind === RVKind.Error) {
|
|
167
|
+
return sv;
|
|
168
|
+
}
|
|
136
169
|
if (sv.kind === RVKind.Number) {
|
|
137
170
|
sum += sv.value;
|
|
138
171
|
}
|
|
@@ -205,12 +238,23 @@ export function fnSUMIFS(args) {
|
|
|
205
238
|
return pairs.error;
|
|
206
239
|
}
|
|
207
240
|
let sum = 0;
|
|
241
|
+
let sumErr = null;
|
|
208
242
|
iterateMultiCriteria(sumArr, pairs.pairs, (r, c) => {
|
|
243
|
+
if (sumErr) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
209
246
|
const sv = getCell(sumArr, r, c);
|
|
247
|
+
if (sv.kind === RVKind.Error) {
|
|
248
|
+
sumErr = sv;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
210
251
|
if (sv.kind === RVKind.Number) {
|
|
211
252
|
sum += sv.value;
|
|
212
253
|
}
|
|
213
254
|
});
|
|
255
|
+
if (sumErr) {
|
|
256
|
+
return sumErr;
|
|
257
|
+
}
|
|
214
258
|
return rvNumber(sum);
|
|
215
259
|
}
|
|
216
260
|
export function fnCOUNTIF(args) {
|
|
@@ -268,6 +312,10 @@ export function fnAVERAGEIF(args) {
|
|
|
268
312
|
for (let c = 0; c < rangeArr.width; c++) {
|
|
269
313
|
if (pred(getCell(rangeArr, r, c))) {
|
|
270
314
|
const sv = getCell(avgArr, r, c);
|
|
315
|
+
// Propagate errors from the average-range — see SUMIF for rationale.
|
|
316
|
+
if (sv.kind === RVKind.Error) {
|
|
317
|
+
return sv;
|
|
318
|
+
}
|
|
271
319
|
if (sv.kind === RVKind.Number) {
|
|
272
320
|
sum += sv.value;
|
|
273
321
|
count++;
|
|
@@ -288,13 +336,24 @@ export function fnAVERAGEIFS(args) {
|
|
|
288
336
|
}
|
|
289
337
|
let sum = 0;
|
|
290
338
|
let count = 0;
|
|
339
|
+
let avgErr = null;
|
|
291
340
|
iterateMultiCriteria(avgArr, pairs.pairs, (r, c) => {
|
|
341
|
+
if (avgErr) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
292
344
|
const sv = getCell(avgArr, r, c);
|
|
345
|
+
if (sv.kind === RVKind.Error) {
|
|
346
|
+
avgErr = sv;
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
293
349
|
if (sv.kind === RVKind.Number) {
|
|
294
350
|
sum += sv.value;
|
|
295
351
|
count++;
|
|
296
352
|
}
|
|
297
353
|
});
|
|
354
|
+
if (avgErr) {
|
|
355
|
+
return avgErr;
|
|
356
|
+
}
|
|
298
357
|
return count === 0 ? ERRORS.DIV0 : rvNumber(sum / count);
|
|
299
358
|
}
|
|
300
359
|
export function fnMAXIFS(args) {
|
|
@@ -308,8 +367,16 @@ export function fnMAXIFS(args) {
|
|
|
308
367
|
}
|
|
309
368
|
let result = -Infinity;
|
|
310
369
|
let found = false;
|
|
370
|
+
let maxErr = null;
|
|
311
371
|
iterateMultiCriteria(maxArr, pairs.pairs, (r, c) => {
|
|
372
|
+
if (maxErr) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
312
375
|
const sv = getCell(maxArr, r, c);
|
|
376
|
+
if (sv.kind === RVKind.Error) {
|
|
377
|
+
maxErr = sv;
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
313
380
|
if (sv.kind === RVKind.Number) {
|
|
314
381
|
if (sv.value > result) {
|
|
315
382
|
result = sv.value;
|
|
@@ -317,6 +384,9 @@ export function fnMAXIFS(args) {
|
|
|
317
384
|
found = true;
|
|
318
385
|
}
|
|
319
386
|
});
|
|
387
|
+
if (maxErr) {
|
|
388
|
+
return maxErr;
|
|
389
|
+
}
|
|
320
390
|
return rvNumber(found ? result : 0);
|
|
321
391
|
}
|
|
322
392
|
export function fnMINIFS(args) {
|
|
@@ -330,8 +400,16 @@ export function fnMINIFS(args) {
|
|
|
330
400
|
}
|
|
331
401
|
let result = Infinity;
|
|
332
402
|
let found = false;
|
|
403
|
+
let minErr = null;
|
|
333
404
|
iterateMultiCriteria(minArr, pairs.pairs, (r, c) => {
|
|
405
|
+
if (minErr) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
334
408
|
const sv = getCell(minArr, r, c);
|
|
409
|
+
if (sv.kind === RVKind.Error) {
|
|
410
|
+
minErr = sv;
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
335
413
|
if (sv.kind === RVKind.Number) {
|
|
336
414
|
if (sv.value < result) {
|
|
337
415
|
result = sv.value;
|
|
@@ -339,5 +417,8 @@ export function fnMINIFS(args) {
|
|
|
339
417
|
found = true;
|
|
340
418
|
}
|
|
341
419
|
});
|
|
420
|
+
if (minErr) {
|
|
421
|
+
return minErr;
|
|
422
|
+
}
|
|
342
423
|
return rvNumber(found ? result : 0);
|
|
343
424
|
}
|