@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
|
@@ -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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
101
|
+
return vs === cs;
|
|
80
102
|
case "<>":
|
|
81
|
-
return
|
|
103
|
+
return vs !== cs;
|
|
82
104
|
case ">":
|
|
83
|
-
return
|
|
105
|
+
return vs > cs;
|
|
84
106
|
case "<":
|
|
85
|
-
return
|
|
107
|
+
return vs < cs;
|
|
86
108
|
case ">=":
|
|
87
|
-
return
|
|
109
|
+
return vs >= cs;
|
|
88
110
|
case "<=":
|
|
89
|
-
return
|
|
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(
|
|
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 =>
|
|
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
|
-
|
|
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() +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
871
|
-
|
|
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);
|