@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.
Files changed (109) hide show
  1. package/dist/browser/index.d.ts +1 -0
  2. package/dist/browser/index.js +2 -0
  3. package/dist/browser/modules/excel/cell.d.ts +18 -0
  4. package/dist/browser/modules/excel/cell.js +21 -0
  5. package/dist/browser/modules/excel/utils/cell-format.js +85 -13
  6. package/dist/browser/modules/excel/workbook.browser.d.ts +57 -0
  7. package/dist/browser/modules/excel/workbook.browser.js +49 -0
  8. package/dist/browser/modules/excel/xlsx/defaultnumformats.js +3 -3
  9. package/dist/browser/modules/formula/compile/binder.js +48 -6
  10. package/dist/browser/modules/formula/compile/bound-ast.d.ts +16 -2
  11. package/dist/browser/modules/formula/compile/bound-ast.js +1 -0
  12. package/dist/browser/modules/formula/compile/compiled-formula.js +41 -8
  13. package/dist/browser/modules/formula/functions/_shared.d.ts +19 -0
  14. package/dist/browser/modules/formula/functions/_shared.js +47 -0
  15. package/dist/browser/modules/formula/functions/conditional.js +103 -22
  16. package/dist/browser/modules/formula/functions/date.js +105 -23
  17. package/dist/browser/modules/formula/functions/dynamic-array.js +173 -69
  18. package/dist/browser/modules/formula/functions/engineering.d.ts +2 -2
  19. package/dist/browser/modules/formula/functions/engineering.js +103 -151
  20. package/dist/browser/modules/formula/functions/financial.js +210 -184
  21. package/dist/browser/modules/formula/functions/lookup.js +224 -157
  22. package/dist/browser/modules/formula/functions/math.d.ts +26 -0
  23. package/dist/browser/modules/formula/functions/math.js +249 -69
  24. package/dist/browser/modules/formula/functions/statistical.js +221 -171
  25. package/dist/browser/modules/formula/functions/text.js +112 -52
  26. package/dist/browser/modules/formula/integration/calculate-formulas-impl.js +20 -1
  27. package/dist/browser/modules/formula/materialize/build-writeback-plan.js +10 -6
  28. package/dist/browser/modules/formula/materialize/types.d.ts +15 -0
  29. package/dist/browser/modules/formula/runtime/evaluator.d.ts +8 -0
  30. package/dist/browser/modules/formula/runtime/evaluator.js +582 -162
  31. package/dist/browser/modules/formula/runtime/function-registry.d.ts +5 -0
  32. package/dist/browser/modules/formula/runtime/function-registry.js +59 -13
  33. package/dist/browser/modules/formula/runtime/values.d.ts +13 -0
  34. package/dist/browser/modules/formula/runtime/values.js +20 -2
  35. package/dist/browser/modules/formula/syntax/ast.d.ts +14 -2
  36. package/dist/browser/modules/formula/syntax/ast.js +1 -0
  37. package/dist/browser/modules/formula/syntax/parser.js +29 -7
  38. package/dist/browser/modules/formula/syntax/token-types.d.ts +4 -0
  39. package/dist/browser/modules/formula/syntax/token-types.js +9 -0
  40. package/dist/browser/modules/formula/syntax/tokenizer.js +76 -19
  41. package/dist/cjs/index.js +7 -2
  42. package/dist/cjs/modules/excel/cell.js +21 -0
  43. package/dist/cjs/modules/excel/utils/cell-format.js +85 -13
  44. package/dist/cjs/modules/excel/workbook.browser.js +49 -0
  45. package/dist/cjs/modules/excel/xlsx/defaultnumformats.js +3 -3
  46. package/dist/cjs/modules/formula/compile/binder.js +48 -6
  47. package/dist/cjs/modules/formula/compile/compiled-formula.js +41 -8
  48. package/dist/cjs/modules/formula/functions/_shared.js +48 -0
  49. package/dist/cjs/modules/formula/functions/conditional.js +103 -22
  50. package/dist/cjs/modules/formula/functions/date.js +104 -22
  51. package/dist/cjs/modules/formula/functions/dynamic-array.js +173 -69
  52. package/dist/cjs/modules/formula/functions/engineering.js +109 -157
  53. package/dist/cjs/modules/formula/functions/financial.js +209 -183
  54. package/dist/cjs/modules/formula/functions/lookup.js +224 -157
  55. package/dist/cjs/modules/formula/functions/math.js +254 -70
  56. package/dist/cjs/modules/formula/functions/statistical.js +222 -172
  57. package/dist/cjs/modules/formula/functions/text.js +112 -52
  58. package/dist/cjs/modules/formula/integration/calculate-formulas-impl.js +20 -1
  59. package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +10 -6
  60. package/dist/cjs/modules/formula/runtime/evaluator.js +581 -161
  61. package/dist/cjs/modules/formula/runtime/function-registry.js +57 -11
  62. package/dist/cjs/modules/formula/runtime/values.js +21 -2
  63. package/dist/cjs/modules/formula/syntax/parser.js +29 -7
  64. package/dist/cjs/modules/formula/syntax/token-types.js +9 -0
  65. package/dist/cjs/modules/formula/syntax/tokenizer.js +76 -19
  66. package/dist/esm/index.js +2 -0
  67. package/dist/esm/modules/excel/cell.js +21 -0
  68. package/dist/esm/modules/excel/utils/cell-format.js +85 -13
  69. package/dist/esm/modules/excel/workbook.browser.js +49 -0
  70. package/dist/esm/modules/excel/xlsx/defaultnumformats.js +3 -3
  71. package/dist/esm/modules/formula/compile/binder.js +48 -6
  72. package/dist/esm/modules/formula/compile/bound-ast.js +1 -0
  73. package/dist/esm/modules/formula/compile/compiled-formula.js +41 -8
  74. package/dist/esm/modules/formula/functions/_shared.js +47 -0
  75. package/dist/esm/modules/formula/functions/conditional.js +103 -22
  76. package/dist/esm/modules/formula/functions/date.js +105 -23
  77. package/dist/esm/modules/formula/functions/dynamic-array.js +173 -69
  78. package/dist/esm/modules/formula/functions/engineering.js +103 -151
  79. package/dist/esm/modules/formula/functions/financial.js +210 -184
  80. package/dist/esm/modules/formula/functions/lookup.js +224 -157
  81. package/dist/esm/modules/formula/functions/math.js +249 -69
  82. package/dist/esm/modules/formula/functions/statistical.js +221 -171
  83. package/dist/esm/modules/formula/functions/text.js +112 -52
  84. package/dist/esm/modules/formula/integration/calculate-formulas-impl.js +20 -1
  85. package/dist/esm/modules/formula/materialize/build-writeback-plan.js +10 -6
  86. package/dist/esm/modules/formula/runtime/evaluator.js +582 -162
  87. package/dist/esm/modules/formula/runtime/function-registry.js +59 -13
  88. package/dist/esm/modules/formula/runtime/values.js +20 -2
  89. package/dist/esm/modules/formula/syntax/ast.js +1 -0
  90. package/dist/esm/modules/formula/syntax/parser.js +29 -7
  91. package/dist/esm/modules/formula/syntax/token-types.js +9 -0
  92. package/dist/esm/modules/formula/syntax/tokenizer.js +76 -19
  93. package/dist/iife/excelts.iife.js +1502 -1379
  94. package/dist/iife/excelts.iife.js.map +1 -1
  95. package/dist/iife/excelts.iife.min.js +26 -26
  96. package/dist/types/index.d.ts +1 -0
  97. package/dist/types/modules/excel/cell.d.ts +18 -0
  98. package/dist/types/modules/excel/workbook.browser.d.ts +57 -0
  99. package/dist/types/modules/formula/compile/bound-ast.d.ts +16 -2
  100. package/dist/types/modules/formula/functions/_shared.d.ts +19 -0
  101. package/dist/types/modules/formula/functions/engineering.d.ts +2 -2
  102. package/dist/types/modules/formula/functions/math.d.ts +26 -0
  103. package/dist/types/modules/formula/materialize/types.d.ts +15 -0
  104. package/dist/types/modules/formula/runtime/evaluator.d.ts +8 -0
  105. package/dist/types/modules/formula/runtime/function-registry.d.ts +5 -0
  106. package/dist/types/modules/formula/runtime/values.d.ts +13 -0
  107. package/dist/types/modules/formula/syntax/ast.d.ts +14 -2
  108. package/dist/types/modules/formula/syntax/token-types.d.ts +4 -0
  109. 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
- const resolved = snapshot.tablesByName.get(tableName.toLowerCase());
329
- if (resolved) {
330
- return { table: resolved.table, sheetName: resolved.sheetName };
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
- if (bound.kind === BoundExprKind.Call && DYNAMIC_ARRAY_FUNCTION_NAMES.has(bound.name)) {
280
- return true;
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
- (bound.name === "SUBTOTAL" || bound.name === "AGGREGATE")) {
303
- return true;
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
- if (VOLATILE_FUNCTIONS.has(e.name)) {
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
- const numericOf = (v) => {
51
- if (v.kind === RVKind.Number) {
52
- return v.value;
53
- }
54
- if (v.kind === RVKind.Boolean) {
55
- return v.value ? 1 : 0;
56
- }
57
- if (v.kind === RVKind.Blank) {
58
- return 0;
59
- }
60
- return Number.NaN;
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 isNum ? vn === numVal : vs === cs;
90
+ return vs === cs;
69
91
  case "<>":
70
- return isNum ? vn !== numVal : vs !== cs;
92
+ return vs !== cs;
71
93
  case ">":
72
- return isNum ? vn > numVal : vs > cs;
94
+ return vs > cs;
73
95
  case "<":
74
- return isNum ? vn < numVal : vs < cs;
96
+ return vs < cs;
75
97
  case ">=":
76
- return isNum ? vn >= numVal : vs >= cs;
98
+ return vs >= cs;
77
99
  case "<=":
78
- return isNum ? vn <= numVal : vs <= cs;
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(toStringRV(v));
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 => toStringRV(v).toLowerCase() === literal;
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
  }