@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
@@ -58,35 +58,57 @@ function buildCriteriaPredicateRV(criteria) {
58
58
  // blank (→0); numeric strings are NOT coerced (COUNTIF stays textual
59
59
  // for those). Only real Number / Boolean / Blank cells participate in
60
60
  // numeric comparisons; everything else falls back to string compare.
61
- const numericOf = (v) => {
62
- if (v.kind === 1 /* RVKind.Number */) {
63
- return v.value;
64
- }
65
- if (v.kind === 3 /* RVKind.Boolean */) {
66
- return v.value ? 1 : 0;
67
- }
68
- if (v.kind === 0 /* RVKind.Blank */) {
69
- return 0;
70
- }
71
- return Number.NaN;
72
- };
61
+ if (isNum) {
62
+ // Specialised numeric-comparison path avoids per-cell string
63
+ // coercion allocations that dominate the hot loop otherwise.
64
+ // Non-numeric cells produce NaN and fall through to the switch,
65
+ // where only `<>` evaluates NaN-relations to TRUE (Excel semantics).
66
+ return (v) => {
67
+ const vn = v.kind === 1 /* RVKind.Number */
68
+ ? v.value
69
+ : v.kind === 3 /* RVKind.Boolean */
70
+ ? v.value
71
+ ? 1
72
+ : 0
73
+ : v.kind === 0 /* RVKind.Blank */
74
+ ? 0
75
+ : Number.NaN;
76
+ switch (op) {
77
+ case "=":
78
+ return vn === numVal;
79
+ case "<>":
80
+ return vn !== numVal;
81
+ case ">":
82
+ return vn > numVal;
83
+ case "<":
84
+ return vn < numVal;
85
+ case ">=":
86
+ return vn >= numVal;
87
+ case "<=":
88
+ return vn <= numVal;
89
+ default:
90
+ return false;
91
+ }
92
+ };
93
+ }
94
+ // String-comparison path — lowercase the criterion once outside the
95
+ // closure so every cell only pays a single `toStringRV().toLowerCase()`.
96
+ const cs = valStr.toLowerCase();
73
97
  return (v) => {
74
- const vn = numericOf(v);
75
98
  const vs = (0, values_1.toStringRV)(v).toLowerCase();
76
- const cs = valStr.toLowerCase();
77
99
  switch (op) {
78
100
  case "=":
79
- return isNum ? vn === numVal : vs === cs;
101
+ return vs === cs;
80
102
  case "<>":
81
- return isNum ? vn !== numVal : vs !== cs;
103
+ return vs !== cs;
82
104
  case ">":
83
- return isNum ? vn > numVal : vs > cs;
105
+ return vs > cs;
84
106
  case "<":
85
- return isNum ? vn < numVal : vs < cs;
107
+ return vs < cs;
86
108
  case ">=":
87
- return isNum ? vn >= numVal : vs >= cs;
109
+ return vs >= cs;
88
110
  case "<=":
89
- return isNum ? vn <= numVal : vs <= cs;
111
+ return vs <= cs;
90
112
  default:
91
113
  return false;
92
114
  }
@@ -96,14 +118,19 @@ function buildCriteriaPredicateRV(criteria) {
96
118
  // literal `*`, `?`, `~` and everything else as a regex special character
97
119
  // that must be escaped. Only an unescaped `*` or `?` triggers the wildcard
98
120
  // path; a pattern like `~*` matches a literal asterisk.
121
+ //
122
+ // Excel restricts wildcard matching to TEXT cells — a criterion like
123
+ // `"1*"` must not match the number 1 even though `String(1) === "1"`.
124
+ // Without this guard, `COUNTIF({1, 15, "15"}, "1*")` would return 3
125
+ // instead of the correct `1` (only the string `"15"` matches).
99
126
  if ((0, _shared_1.hasUnescapedWildcard)(s)) {
100
127
  try {
101
128
  const re = new RegExp("^" + (0, _shared_1.excelWildcardToRegex)(s) + "$", "i");
102
- return v => re.test((0, values_1.toStringRV)(v));
129
+ return v => v.kind === 2 /* RVKind.String */ && re.test(v.value);
103
130
  }
104
131
  catch {
105
132
  const literal = (0, _shared_1.unescapeExcelWildcard)(s).toLowerCase();
106
- return v => (0, values_1.toStringRV)(v).toLowerCase() === literal;
133
+ return v => v.kind === 2 /* RVKind.String */ && v.value.toLowerCase() === literal;
107
134
  }
108
135
  }
109
136
  // No wildcards: strip any `~` escapes and do a literal case-insensitive compare.
@@ -144,6 +171,12 @@ function fnSUMIF(args) {
144
171
  for (let c = 0; c < rangeArr.width; c++) {
145
172
  if (pred((0, _shared_1.getCell)(rangeArr, r, c))) {
146
173
  const sv = (0, _shared_1.getCell)(sumArr, r, c);
174
+ // Excel propagates errors from the sum-range; previously we
175
+ // silently skipped them, masking `#DIV/0!` / `#VALUE!` cells
176
+ // under the aggregation.
177
+ if (sv.kind === 4 /* RVKind.Error */) {
178
+ return sv;
179
+ }
147
180
  if (sv.kind === 1 /* RVKind.Number */) {
148
181
  sum += sv.value;
149
182
  }
@@ -216,12 +249,23 @@ function fnSUMIFS(args) {
216
249
  return pairs.error;
217
250
  }
218
251
  let sum = 0;
252
+ let sumErr = null;
219
253
  iterateMultiCriteria(sumArr, pairs.pairs, (r, c) => {
254
+ if (sumErr) {
255
+ return;
256
+ }
220
257
  const sv = (0, _shared_1.getCell)(sumArr, r, c);
258
+ if (sv.kind === 4 /* RVKind.Error */) {
259
+ sumErr = sv;
260
+ return;
261
+ }
221
262
  if (sv.kind === 1 /* RVKind.Number */) {
222
263
  sum += sv.value;
223
264
  }
224
265
  });
266
+ if (sumErr) {
267
+ return sumErr;
268
+ }
225
269
  return (0, values_1.rvNumber)(sum);
226
270
  }
227
271
  function fnCOUNTIF(args) {
@@ -279,6 +323,10 @@ function fnAVERAGEIF(args) {
279
323
  for (let c = 0; c < rangeArr.width; c++) {
280
324
  if (pred((0, _shared_1.getCell)(rangeArr, r, c))) {
281
325
  const sv = (0, _shared_1.getCell)(avgArr, r, c);
326
+ // Propagate errors from the average-range — see SUMIF for rationale.
327
+ if (sv.kind === 4 /* RVKind.Error */) {
328
+ return sv;
329
+ }
282
330
  if (sv.kind === 1 /* RVKind.Number */) {
283
331
  sum += sv.value;
284
332
  count++;
@@ -299,13 +347,24 @@ function fnAVERAGEIFS(args) {
299
347
  }
300
348
  let sum = 0;
301
349
  let count = 0;
350
+ let avgErr = null;
302
351
  iterateMultiCriteria(avgArr, pairs.pairs, (r, c) => {
352
+ if (avgErr) {
353
+ return;
354
+ }
303
355
  const sv = (0, _shared_1.getCell)(avgArr, r, c);
356
+ if (sv.kind === 4 /* RVKind.Error */) {
357
+ avgErr = sv;
358
+ return;
359
+ }
304
360
  if (sv.kind === 1 /* RVKind.Number */) {
305
361
  sum += sv.value;
306
362
  count++;
307
363
  }
308
364
  });
365
+ if (avgErr) {
366
+ return avgErr;
367
+ }
309
368
  return count === 0 ? values_1.ERRORS.DIV0 : (0, values_1.rvNumber)(sum / count);
310
369
  }
311
370
  function fnMAXIFS(args) {
@@ -319,8 +378,16 @@ function fnMAXIFS(args) {
319
378
  }
320
379
  let result = -Infinity;
321
380
  let found = false;
381
+ let maxErr = null;
322
382
  iterateMultiCriteria(maxArr, pairs.pairs, (r, c) => {
383
+ if (maxErr) {
384
+ return;
385
+ }
323
386
  const sv = (0, _shared_1.getCell)(maxArr, r, c);
387
+ if (sv.kind === 4 /* RVKind.Error */) {
388
+ maxErr = sv;
389
+ return;
390
+ }
324
391
  if (sv.kind === 1 /* RVKind.Number */) {
325
392
  if (sv.value > result) {
326
393
  result = sv.value;
@@ -328,6 +395,9 @@ function fnMAXIFS(args) {
328
395
  found = true;
329
396
  }
330
397
  });
398
+ if (maxErr) {
399
+ return maxErr;
400
+ }
331
401
  return (0, values_1.rvNumber)(found ? result : 0);
332
402
  }
333
403
  function fnMINIFS(args) {
@@ -341,8 +411,16 @@ function fnMINIFS(args) {
341
411
  }
342
412
  let result = Infinity;
343
413
  let found = false;
414
+ let minErr = null;
344
415
  iterateMultiCriteria(minArr, pairs.pairs, (r, c) => {
416
+ if (minErr) {
417
+ return;
418
+ }
345
419
  const sv = (0, _shared_1.getCell)(minArr, r, c);
420
+ if (sv.kind === 4 /* RVKind.Error */) {
421
+ minErr = sv;
422
+ return;
423
+ }
346
424
  if (sv.kind === 1 /* RVKind.Number */) {
347
425
  if (sv.value < result) {
348
426
  result = sv.value;
@@ -350,5 +428,8 @@ function fnMINIFS(args) {
350
428
  found = true;
351
429
  }
352
430
  });
431
+ if (minErr) {
432
+ return minErr;
433
+ }
353
434
  return (0, values_1.rvNumber)(found ? result : 0);
354
435
  }
@@ -39,6 +39,11 @@ function collectHolidays(arg) {
39
39
  if ((0, values_1.isArray)(arg)) {
40
40
  for (const row of arg.rows) {
41
41
  for (const cell of row) {
42
+ // Propagate errors from the holidays list rather than silently
43
+ // skipping them — Excel surfaces `#N/A` from a holiday cell.
44
+ if (cell.kind === 4 /* RVKind.Error */) {
45
+ return cell;
46
+ }
42
47
  if (cell.kind === 1 /* RVKind.Number */) {
43
48
  set.add(Math.floor(cell.value));
44
49
  }
@@ -46,6 +51,9 @@ function collectHolidays(arg) {
46
51
  }
47
52
  }
48
53
  else {
54
+ if (arg.kind === 4 /* RVKind.Error */) {
55
+ return arg;
56
+ }
49
57
  const n = (0, values_1.toNumberRV)(arg);
50
58
  if (n.kind === 1 /* RVKind.Number */) {
51
59
  set.add(Math.floor(n.value));
@@ -219,7 +227,10 @@ const fnWEEKDAY = args => {
219
227
  return n;
220
228
  }
221
229
  const d = toDate(n.value);
222
- const returnType = args.length > 1 ? (0, _shared_1.argToNumber)(args[1]) : (0, values_1.rvNumber)(1);
230
+ // Blank `return_type` Excel default 1 (Sun=1..Sat=7). Without the
231
+ // blank guard, `argToNumber(BLANK)` coerces to 0 which falls to the
232
+ // default branch and yields a spurious #NUM! for `WEEKDAY(date, )`.
233
+ const returnType = args.length > 1 && args[1].kind !== 0 /* RVKind.Blank */ ? (0, _shared_1.argToNumber)(args[1]) : (0, values_1.rvNumber)(1);
223
234
  if ((0, values_1.isError)(returnType)) {
224
235
  return returnType;
225
236
  }
@@ -253,8 +264,14 @@ const fnEOMONTH = args => {
253
264
  if ((0, values_1.isError)(months)) {
254
265
  return months;
255
266
  }
267
+ // Excel truncates `months` toward zero before doing month arithmetic.
268
+ // `Date.UTC` happens to truncate too, but the explicit `Math.trunc`
269
+ // makes the contract visible and protects against engines that might
270
+ // not (or against a future refactor that routes through a different
271
+ // date constructor).
272
+ const m = Math.trunc(months.value);
256
273
  const d = toDate(startDate.value);
257
- const result = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months.value + 1, 0));
274
+ const result = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + m + 1, 0));
258
275
  return (0, values_1.rvNumber)(fromDate(result));
259
276
  };
260
277
  exports.fnEOMONTH = fnEOMONTH;
@@ -267,8 +284,19 @@ const fnEDATE = args => {
267
284
  if ((0, values_1.isError)(months)) {
268
285
  return months;
269
286
  }
287
+ const m = Math.trunc(months.value);
270
288
  const d = toDate(startDate.value);
271
- const result = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months.value, d.getUTCDate()));
289
+ // Excel clamps to the last day of the target month when the source day
290
+ // would overflow (e.g. `EDATE(2024-01-31, 1)` → 2024-02-29, not rolling
291
+ // forward into March). JS Date.UTC rolls over by default, so we detect
292
+ // the overflow and clamp explicitly. To do so we first construct the
293
+ // 1st of the target month, read `daysInMonth` via the "day 0 of next
294
+ // month" trick, and cap the original day at that.
295
+ const targetYearMonth = d.getUTCMonth() + m;
296
+ const firstOfTarget = new Date(Date.UTC(d.getUTCFullYear(), targetYearMonth, 1));
297
+ const lastDayOfTarget = new Date(Date.UTC(firstOfTarget.getUTCFullYear(), firstOfTarget.getUTCMonth() + 1, 0)).getUTCDate();
298
+ const clampedDay = Math.min(d.getUTCDate(), lastDayOfTarget);
299
+ const result = new Date(Date.UTC(firstOfTarget.getUTCFullYear(), firstOfTarget.getUTCMonth(), clampedDay));
272
300
  return (0, values_1.rvNumber)(fromDate(result));
273
301
  };
274
302
  exports.fnEDATE = fnEDATE;
@@ -285,7 +313,7 @@ const fnDATEDIF = args => {
285
313
  if (endN.value < startN.value) {
286
314
  return values_1.ERRORS.NUM;
287
315
  }
288
- const unit = (0, values_1.toStringRV)(args[2]).toUpperCase();
316
+ const unit = (0, values_1.toStringRV)((0, values_1.topLeft)(args[2])).toUpperCase();
289
317
  const startD = toDate(startN.value);
290
318
  const endD = toDate(endN.value);
291
319
  const sy = startD.getUTCFullYear();
@@ -372,7 +400,9 @@ const fnWEEKNUM = args => {
372
400
  return n;
373
401
  }
374
402
  const d = toDate(n.value);
375
- const returnType = args.length > 1 ? (0, _shared_1.argToNumber)(args[1]) : (0, values_1.rvNumber)(1);
403
+ // Blank `return_type` Excel default 1 (Sunday start). See WEEKDAY
404
+ // for the same rationale.
405
+ const returnType = args.length > 1 && args[1].kind !== 0 /* RVKind.Blank */ ? (0, _shared_1.argToNumber)(args[1]) : (0, values_1.rvNumber)(1);
376
406
  if ((0, values_1.isError)(returnType)) {
377
407
  return returnType;
378
408
  }
@@ -427,15 +457,44 @@ function networkdaysHelper(startN, endN, holidays) {
427
457
  const s = Math.floor(Math.min(startN, endN));
428
458
  const e = Math.floor(Math.max(startN, endN));
429
459
  const sign = startN <= endN ? 1 : -1;
430
- let count = 0;
431
- for (let d = s; d <= e; d++) {
432
- const dt = toDate(d);
433
- const dow = dt.getUTCDay();
434
- if (dow !== 0 && dow !== 6 && !holidays.has(d)) {
435
- count++;
460
+ // Closed-form weekday count: partition `[s, e]` into whole weeks plus
461
+ // a tail. Each whole week contributes 5 weekdays, regardless of its
462
+ // starting day-of-week. The tail contributes however many of its
463
+ // remaining days fall on Monday..Friday.
464
+ //
465
+ // `getUTCDay()`: Sun=0, Mon=1, …, Sat=6. We compute `dow` once for
466
+ // the start date and then just walk the `tail` days forward by
467
+ // modular arithmetic — no Date allocations in the loop.
468
+ const totalDays = e - s + 1;
469
+ const weeks = Math.floor(totalDays / 7);
470
+ const tail = totalDays % 7;
471
+ let weekdays = weeks * 5;
472
+ if (tail > 0) {
473
+ const startDow = toDate(s).getUTCDay();
474
+ for (let i = 0; i < tail; i++) {
475
+ const dow = (startDow + i) % 7;
476
+ if (dow !== 0 && dow !== 6) {
477
+ weekdays++;
478
+ }
436
479
  }
437
480
  }
438
- return count * sign;
481
+ else {
482
+ // When `totalDays` is an exact multiple of 7 the start DOW still
483
+ // governs whether any holidays from the caller's list fall on a
484
+ // weekday, so we stop here — no tail to walk.
485
+ }
486
+ // Subtract holidays that land on a weekday and fall within [s, e].
487
+ if (holidays.size > 0) {
488
+ for (const h of holidays) {
489
+ if (h >= s && h <= e) {
490
+ const dow = toDate(h).getUTCDay();
491
+ if (dow !== 0 && dow !== 6) {
492
+ weekdays--;
493
+ }
494
+ }
495
+ }
496
+ }
497
+ return weekdays * sign;
439
498
  }
440
499
  const fnNETWORKDAYS = args => {
441
500
  const startN = (0, _shared_1.argToNumber)(args[0]);
@@ -447,7 +506,10 @@ const fnNETWORKDAYS = args => {
447
506
  return endN;
448
507
  }
449
508
  const holidays = args.length > 2 ? collectHolidays(args[2]) : new Set();
450
- return (0, values_1.rvNumber)(networkdaysHelper(startN.value, endN.value, holidays));
509
+ if (holidays instanceof Set) {
510
+ return (0, values_1.rvNumber)(networkdaysHelper(startN.value, endN.value, holidays));
511
+ }
512
+ return holidays;
451
513
  };
452
514
  exports.fnNETWORKDAYS = fnNETWORKDAYS;
453
515
  const fnWORKDAY = args => {
@@ -460,9 +522,16 @@ const fnWORKDAY = args => {
460
522
  return days;
461
523
  }
462
524
  const holidays = args.length > 2 ? collectHolidays(args[2]) : new Set();
525
+ if (!(holidays instanceof Set)) {
526
+ return holidays;
527
+ }
463
528
  let current = Math.floor(startN.value);
464
- const step = days.value >= 0 ? 1 : -1;
465
- let remaining = Math.abs(days.value);
529
+ // Excel truncates `days` toward zero. Without this, a fractional input
530
+ // like 2.7 would walk extra iterations until the fractional remainder
531
+ // underflowed past zero, silently producing a wrong result.
532
+ const daysInt = Math.trunc(days.value);
533
+ const step = daysInt >= 0 ? 1 : -1;
534
+ let remaining = Math.abs(daysInt);
466
535
  while (remaining > 0) {
467
536
  current += step;
468
537
  const dt = toDate(current);
@@ -582,7 +651,7 @@ const fnDATEVALUE = args => {
582
651
  if (err) {
583
652
  return err;
584
653
  }
585
- const text = (0, values_1.toStringRV)(args[0]).trim();
654
+ const text = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0])).trim();
586
655
  // Lotus 1-2-3 bug: "2/29/1900" or "February 29, 1900" etc. should return 60
587
656
  const lotus29 = /^(2[/-]29[/-]1900|1900[/-]2[/-]29|1900[/-]02[/-]29|02[/-]29[/-]1900|Feb(ruary)?\s+29[,]?\s+1900)$/i;
588
657
  if (lotus29.test(text)) {
@@ -600,7 +669,7 @@ const fnTIMEVALUE = args => {
600
669
  if (err) {
601
670
  return err;
602
671
  }
603
- const text = (0, values_1.toStringRV)(args[0]).trim();
672
+ const text = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0])).trim();
604
673
  const parsed = parseTimeOnly(text);
605
674
  if (parsed === null) {
606
675
  return values_1.ERRORS.VALUE;
@@ -756,7 +825,7 @@ const fnDAYS360 = args => {
756
825
  if ((0, values_1.isError)(endN)) {
757
826
  return endN;
758
827
  }
759
- const methodRV = args.length > 2 ? (0, values_1.toBooleanRV)(args[2]) : (0, values_1.rvBoolean)(false);
828
+ const methodRV = args.length > 2 ? (0, values_1.toBooleanRV)((0, values_1.topLeft)(args[2])) : (0, values_1.rvBoolean)(false);
760
829
  if ((0, values_1.isError)(methodRV)) {
761
830
  return methodRV;
762
831
  }
@@ -832,11 +901,16 @@ const fnNETWORKDAYS_INTL = args => {
832
901
  if ((0, values_1.isError)(endN)) {
833
902
  return endN;
834
903
  }
835
- const weekendArg = args.length > 2 ? (0, _shared_1.argToNumber)(args[2]) : (0, values_1.rvNumber)(1);
904
+ // Blank `weekend` Excel default 1 (Sat+Sun). See getWeekendDays
905
+ // default fallback.
906
+ const weekendArg = args.length > 2 && args[2].kind !== 0 /* RVKind.Blank */ ? (0, _shared_1.argToNumber)(args[2]) : (0, values_1.rvNumber)(1);
836
907
  if ((0, values_1.isError)(weekendArg)) {
837
908
  return weekendArg;
838
909
  }
839
910
  const holidays = args.length > 3 ? collectHolidays(args[3]) : new Set();
911
+ if (!(holidays instanceof Set)) {
912
+ return holidays;
913
+ }
840
914
  const weekendDays = getWeekendDays(weekendArg.value);
841
915
  const s = Math.floor(Math.min(startN.value, endN.value));
842
916
  const e = Math.floor(Math.max(startN.value, endN.value));
@@ -860,15 +934,23 @@ const fnWORKDAY_INTL = args => {
860
934
  if ((0, values_1.isError)(days)) {
861
935
  return days;
862
936
  }
863
- const weekendArg = args.length > 2 ? (0, _shared_1.argToNumber)(args[2]) : (0, values_1.rvNumber)(1);
937
+ // Blank `weekend` Excel default 1 (Sat+Sun).
938
+ const weekendArg = args.length > 2 && args[2].kind !== 0 /* RVKind.Blank */ ? (0, _shared_1.argToNumber)(args[2]) : (0, values_1.rvNumber)(1);
864
939
  if ((0, values_1.isError)(weekendArg)) {
865
940
  return weekendArg;
866
941
  }
867
942
  const holidays = args.length > 3 ? collectHolidays(args[3]) : new Set();
943
+ if (!(holidays instanceof Set)) {
944
+ return holidays;
945
+ }
868
946
  const weekendDays = getWeekendDays(weekendArg.value);
869
947
  let current = Math.floor(startN.value);
870
- const step = days.value >= 0 ? 1 : -1;
871
- let remaining = Math.abs(days.value);
948
+ // Truncate `days` toward zero before stepping see WORKDAY for the
949
+ // same rationale. A fractional input like 2.7 would otherwise walk
950
+ // extra iterations and silently produce the wrong result.
951
+ const daysInt = Math.trunc(days.value);
952
+ const step = daysInt >= 0 ? 1 : -1;
953
+ let remaining = Math.abs(daysInt);
872
954
  while (remaining > 0) {
873
955
  current += step;
874
956
  const dt = toDate(current);