@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
@@ -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
  }
@@ -16,7 +16,7 @@
16
16
  * correctly through `excelToDate()`.
17
17
  */
18
18
  import { dateToExcel, excelToDate } from "../../../utils/utils.base.js";
19
- import { RVKind, ERRORS, isError, isArray, toNumberRV, toStringRV, toBooleanRV, rvNumber, rvBoolean } from "../runtime/values.js";
19
+ import { RVKind, ERRORS, isError, isArray, toNumberRV, toStringRV, toBooleanRV, topLeft, rvNumber, rvBoolean } from "../runtime/values.js";
20
20
  import { isDate1904 } from "./_date-context.js";
21
21
  import { argToNumber, checkError } from "./_shared.js";
22
22
  // ============================================================================
@@ -36,6 +36,11 @@ function collectHolidays(arg) {
36
36
  if (isArray(arg)) {
37
37
  for (const row of arg.rows) {
38
38
  for (const cell of row) {
39
+ // Propagate errors from the holidays list rather than silently
40
+ // skipping them — Excel surfaces `#N/A` from a holiday cell.
41
+ if (cell.kind === RVKind.Error) {
42
+ return cell;
43
+ }
39
44
  if (cell.kind === RVKind.Number) {
40
45
  set.add(Math.floor(cell.value));
41
46
  }
@@ -43,6 +48,9 @@ function collectHolidays(arg) {
43
48
  }
44
49
  }
45
50
  else {
51
+ if (arg.kind === RVKind.Error) {
52
+ return arg;
53
+ }
46
54
  const n = toNumberRV(arg);
47
55
  if (n.kind === RVKind.Number) {
48
56
  set.add(Math.floor(n.value));
@@ -206,7 +214,10 @@ export const fnWEEKDAY = args => {
206
214
  return n;
207
215
  }
208
216
  const d = toDate(n.value);
209
- const returnType = args.length > 1 ? argToNumber(args[1]) : rvNumber(1);
217
+ // Blank `return_type` Excel default 1 (Sun=1..Sat=7). Without the
218
+ // blank guard, `argToNumber(BLANK)` coerces to 0 which falls to the
219
+ // default branch and yields a spurious #NUM! for `WEEKDAY(date, )`.
220
+ const returnType = args.length > 1 && args[1].kind !== RVKind.Blank ? argToNumber(args[1]) : rvNumber(1);
210
221
  if (isError(returnType)) {
211
222
  return returnType;
212
223
  }
@@ -239,8 +250,14 @@ export const fnEOMONTH = args => {
239
250
  if (isError(months)) {
240
251
  return months;
241
252
  }
253
+ // Excel truncates `months` toward zero before doing month arithmetic.
254
+ // `Date.UTC` happens to truncate too, but the explicit `Math.trunc`
255
+ // makes the contract visible and protects against engines that might
256
+ // not (or against a future refactor that routes through a different
257
+ // date constructor).
258
+ const m = Math.trunc(months.value);
242
259
  const d = toDate(startDate.value);
243
- const result = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months.value + 1, 0));
260
+ const result = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + m + 1, 0));
244
261
  return rvNumber(fromDate(result));
245
262
  };
246
263
  export const fnEDATE = args => {
@@ -252,8 +269,19 @@ export const fnEDATE = args => {
252
269
  if (isError(months)) {
253
270
  return months;
254
271
  }
272
+ const m = Math.trunc(months.value);
255
273
  const d = toDate(startDate.value);
256
- const result = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months.value, d.getUTCDate()));
274
+ // Excel clamps to the last day of the target month when the source day
275
+ // would overflow (e.g. `EDATE(2024-01-31, 1)` → 2024-02-29, not rolling
276
+ // forward into March). JS Date.UTC rolls over by default, so we detect
277
+ // the overflow and clamp explicitly. To do so we first construct the
278
+ // 1st of the target month, read `daysInMonth` via the "day 0 of next
279
+ // month" trick, and cap the original day at that.
280
+ const targetYearMonth = d.getUTCMonth() + m;
281
+ const firstOfTarget = new Date(Date.UTC(d.getUTCFullYear(), targetYearMonth, 1));
282
+ const lastDayOfTarget = new Date(Date.UTC(firstOfTarget.getUTCFullYear(), firstOfTarget.getUTCMonth() + 1, 0)).getUTCDate();
283
+ const clampedDay = Math.min(d.getUTCDate(), lastDayOfTarget);
284
+ const result = new Date(Date.UTC(firstOfTarget.getUTCFullYear(), firstOfTarget.getUTCMonth(), clampedDay));
257
285
  return rvNumber(fromDate(result));
258
286
  };
259
287
  export const fnDATEDIF = args => {
@@ -269,7 +297,7 @@ export const fnDATEDIF = args => {
269
297
  if (endN.value < startN.value) {
270
298
  return ERRORS.NUM;
271
299
  }
272
- const unit = toStringRV(args[2]).toUpperCase();
300
+ const unit = toStringRV(topLeft(args[2])).toUpperCase();
273
301
  const startD = toDate(startN.value);
274
302
  const endD = toDate(endN.value);
275
303
  const sy = startD.getUTCFullYear();
@@ -353,7 +381,9 @@ export const fnWEEKNUM = args => {
353
381
  return n;
354
382
  }
355
383
  const d = toDate(n.value);
356
- const returnType = args.length > 1 ? argToNumber(args[1]) : rvNumber(1);
384
+ // Blank `return_type` Excel default 1 (Sunday start). See WEEKDAY
385
+ // for the same rationale.
386
+ const returnType = args.length > 1 && args[1].kind !== RVKind.Blank ? argToNumber(args[1]) : rvNumber(1);
357
387
  if (isError(returnType)) {
358
388
  return returnType;
359
389
  }
@@ -407,15 +437,44 @@ function networkdaysHelper(startN, endN, holidays) {
407
437
  const s = Math.floor(Math.min(startN, endN));
408
438
  const e = Math.floor(Math.max(startN, endN));
409
439
  const sign = startN <= endN ? 1 : -1;
410
- let count = 0;
411
- for (let d = s; d <= e; d++) {
412
- const dt = toDate(d);
413
- const dow = dt.getUTCDay();
414
- if (dow !== 0 && dow !== 6 && !holidays.has(d)) {
415
- count++;
440
+ // Closed-form weekday count: partition `[s, e]` into whole weeks plus
441
+ // a tail. Each whole week contributes 5 weekdays, regardless of its
442
+ // starting day-of-week. The tail contributes however many of its
443
+ // remaining days fall on Monday..Friday.
444
+ //
445
+ // `getUTCDay()`: Sun=0, Mon=1, …, Sat=6. We compute `dow` once for
446
+ // the start date and then just walk the `tail` days forward by
447
+ // modular arithmetic — no Date allocations in the loop.
448
+ const totalDays = e - s + 1;
449
+ const weeks = Math.floor(totalDays / 7);
450
+ const tail = totalDays % 7;
451
+ let weekdays = weeks * 5;
452
+ if (tail > 0) {
453
+ const startDow = toDate(s).getUTCDay();
454
+ for (let i = 0; i < tail; i++) {
455
+ const dow = (startDow + i) % 7;
456
+ if (dow !== 0 && dow !== 6) {
457
+ weekdays++;
458
+ }
416
459
  }
417
460
  }
418
- return count * sign;
461
+ else {
462
+ // When `totalDays` is an exact multiple of 7 the start DOW still
463
+ // governs whether any holidays from the caller's list fall on a
464
+ // weekday, so we stop here — no tail to walk.
465
+ }
466
+ // Subtract holidays that land on a weekday and fall within [s, e].
467
+ if (holidays.size > 0) {
468
+ for (const h of holidays) {
469
+ if (h >= s && h <= e) {
470
+ const dow = toDate(h).getUTCDay();
471
+ if (dow !== 0 && dow !== 6) {
472
+ weekdays--;
473
+ }
474
+ }
475
+ }
476
+ }
477
+ return weekdays * sign;
419
478
  }
420
479
  export const fnNETWORKDAYS = args => {
421
480
  const startN = argToNumber(args[0]);
@@ -427,7 +486,10 @@ export const fnNETWORKDAYS = args => {
427
486
  return endN;
428
487
  }
429
488
  const holidays = args.length > 2 ? collectHolidays(args[2]) : new Set();
430
- return rvNumber(networkdaysHelper(startN.value, endN.value, holidays));
489
+ if (holidays instanceof Set) {
490
+ return rvNumber(networkdaysHelper(startN.value, endN.value, holidays));
491
+ }
492
+ return holidays;
431
493
  };
432
494
  export const fnWORKDAY = args => {
433
495
  const startN = argToNumber(args[0]);
@@ -439,9 +501,16 @@ export const fnWORKDAY = args => {
439
501
  return days;
440
502
  }
441
503
  const holidays = args.length > 2 ? collectHolidays(args[2]) : new Set();
504
+ if (!(holidays instanceof Set)) {
505
+ return holidays;
506
+ }
442
507
  let current = Math.floor(startN.value);
443
- const step = days.value >= 0 ? 1 : -1;
444
- let remaining = Math.abs(days.value);
508
+ // Excel truncates `days` toward zero. Without this, a fractional input
509
+ // like 2.7 would walk extra iterations until the fractional remainder
510
+ // underflowed past zero, silently producing a wrong result.
511
+ const daysInt = Math.trunc(days.value);
512
+ const step = daysInt >= 0 ? 1 : -1;
513
+ let remaining = Math.abs(daysInt);
445
514
  while (remaining > 0) {
446
515
  current += step;
447
516
  const dt = toDate(current);
@@ -559,7 +628,7 @@ export const fnDATEVALUE = args => {
559
628
  if (err) {
560
629
  return err;
561
630
  }
562
- const text = toStringRV(args[0]).trim();
631
+ const text = toStringRV(topLeft(args[0])).trim();
563
632
  // Lotus 1-2-3 bug: "2/29/1900" or "February 29, 1900" etc. should return 60
564
633
  const lotus29 = /^(2[/-]29[/-]1900|1900[/-]2[/-]29|1900[/-]02[/-]29|02[/-]29[/-]1900|Feb(ruary)?\s+29[,]?\s+1900)$/i;
565
634
  if (lotus29.test(text)) {
@@ -576,7 +645,7 @@ export const fnTIMEVALUE = args => {
576
645
  if (err) {
577
646
  return err;
578
647
  }
579
- const text = toStringRV(args[0]).trim();
648
+ const text = toStringRV(topLeft(args[0])).trim();
580
649
  const parsed = parseTimeOnly(text);
581
650
  if (parsed === null) {
582
651
  return ERRORS.VALUE;
@@ -731,7 +800,7 @@ export const fnDAYS360 = args => {
731
800
  if (isError(endN)) {
732
801
  return endN;
733
802
  }
734
- const methodRV = args.length > 2 ? toBooleanRV(args[2]) : rvBoolean(false);
803
+ const methodRV = args.length > 2 ? toBooleanRV(topLeft(args[2])) : rvBoolean(false);
735
804
  if (isError(methodRV)) {
736
805
  return methodRV;
737
806
  }
@@ -806,11 +875,16 @@ export const fnNETWORKDAYS_INTL = args => {
806
875
  if (isError(endN)) {
807
876
  return endN;
808
877
  }
809
- const weekendArg = args.length > 2 ? argToNumber(args[2]) : rvNumber(1);
878
+ // Blank `weekend` Excel default 1 (Sat+Sun). See getWeekendDays
879
+ // default fallback.
880
+ const weekendArg = args.length > 2 && args[2].kind !== RVKind.Blank ? argToNumber(args[2]) : rvNumber(1);
810
881
  if (isError(weekendArg)) {
811
882
  return weekendArg;
812
883
  }
813
884
  const holidays = args.length > 3 ? collectHolidays(args[3]) : new Set();
885
+ if (!(holidays instanceof Set)) {
886
+ return holidays;
887
+ }
814
888
  const weekendDays = getWeekendDays(weekendArg.value);
815
889
  const s = Math.floor(Math.min(startN.value, endN.value));
816
890
  const e = Math.floor(Math.max(startN.value, endN.value));
@@ -833,15 +907,23 @@ export const fnWORKDAY_INTL = args => {
833
907
  if (isError(days)) {
834
908
  return days;
835
909
  }
836
- const weekendArg = args.length > 2 ? argToNumber(args[2]) : rvNumber(1);
910
+ // Blank `weekend` Excel default 1 (Sat+Sun).
911
+ const weekendArg = args.length > 2 && args[2].kind !== RVKind.Blank ? argToNumber(args[2]) : rvNumber(1);
837
912
  if (isError(weekendArg)) {
838
913
  return weekendArg;
839
914
  }
840
915
  const holidays = args.length > 3 ? collectHolidays(args[3]) : new Set();
916
+ if (!(holidays instanceof Set)) {
917
+ return holidays;
918
+ }
841
919
  const weekendDays = getWeekendDays(weekendArg.value);
842
920
  let current = Math.floor(startN.value);
843
- const step = days.value >= 0 ? 1 : -1;
844
- let remaining = Math.abs(days.value);
921
+ // Truncate `days` toward zero before stepping see WORKDAY for the
922
+ // same rationale. A fractional input like 2.7 would otherwise walk
923
+ // extra iterations and silently produce the wrong result.
924
+ const daysInt = Math.trunc(days.value);
925
+ const step = daysInt >= 0 ? 1 : -1;
926
+ let remaining = Math.abs(daysInt);
845
927
  while (remaining > 0) {
846
928
  current += step;
847
929
  const dt = toDate(current);