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