@rowsncolumns/fast-formula-parser 6.2.36 → 6.2.38
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/cjs/index.js +675 -120
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +675 -121
- package/dist/esm/index.js.map +1 -1
- package/index.d.ts +12 -0
- package/package.json +3 -3
package/dist/cjs/index.js
CHANGED
|
@@ -3343,6 +3343,15 @@ function formatValue (value, parseData, opts) {
|
|
|
3343
3343
|
return part ? runPart(value, part, opts, l10n) : opts.overflow;
|
|
3344
3344
|
}
|
|
3345
3345
|
|
|
3346
|
+
function isDate (partitions) {
|
|
3347
|
+
return !!(
|
|
3348
|
+
(partitions[0] && partitions[0].date) ||
|
|
3349
|
+
(partitions[1] && partitions[1].date) ||
|
|
3350
|
+
(partitions[2] && partitions[2].date) ||
|
|
3351
|
+
(partitions[3] && partitions[3].date)
|
|
3352
|
+
);
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3346
3355
|
const tokenHandlers = [
|
|
3347
3356
|
[ TOKEN_GENERAL, /^General/i, 0 ],
|
|
3348
3357
|
[ TOKEN_HASH, /^#/, 0 ],
|
|
@@ -3780,6 +3789,22 @@ function format (pattern, value, options = {}) {
|
|
|
3780
3789
|
return formatValue(val, data, opts);
|
|
3781
3790
|
}
|
|
3782
3791
|
|
|
3792
|
+
// FIXME: what is a a section?...
|
|
3793
|
+
/**
|
|
3794
|
+
* Determine if a given format pattern is a date pattern.
|
|
3795
|
+
*
|
|
3796
|
+
* The pattern is considered a date pattern if any of its sections contain a
|
|
3797
|
+
* date symbol (such as `Y` or `H`). Each section is restricted to be
|
|
3798
|
+
* _either_ a number or date format.
|
|
3799
|
+
*
|
|
3800
|
+
* @param {string} pattern - A format pattern in the ECMA-376 number format.
|
|
3801
|
+
* @returns {boolean} True if the specified pattern is date pattern, False otherwise.
|
|
3802
|
+
*/
|
|
3803
|
+
function isDateFormat (pattern) {
|
|
3804
|
+
const data = prepareFormatterData(pattern, false);
|
|
3805
|
+
return isDate(data.partitions);
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3783
3808
|
// Excel indexed colors mapping used for legacy color references
|
|
3784
3809
|
const IndexedColors = Object.freeze([
|
|
3785
3810
|
// 0-7
|
|
@@ -3858,6 +3883,80 @@ const IndexedColors = Object.freeze([
|
|
|
3858
3883
|
|
|
3859
3884
|
const isSingleSectionElapsedTimePattern = (pattern) => !pattern.includes(";") &&
|
|
3860
3885
|
/\[(?:h+|m+|s+)\]/i.test(pattern.replace(/\[\$[^\]]+\]/g, ""));
|
|
3886
|
+
const intlMonthNames = (locale, options) => {
|
|
3887
|
+
const formatter = new Intl.DateTimeFormat(locale, {
|
|
3888
|
+
...options,
|
|
3889
|
+
timeZone: "UTC",
|
|
3890
|
+
});
|
|
3891
|
+
return Array.from({ length: 12 }, (_, month) => formatter
|
|
3892
|
+
.formatToParts(new Date(Date.UTC(2020, month, 1)))
|
|
3893
|
+
.find((part) => part.type === "month")?.value ?? "");
|
|
3894
|
+
};
|
|
3895
|
+
const intlRegisteredLocales = new Set();
|
|
3896
|
+
/**
|
|
3897
|
+
* numfmt ships name tables for a fixed set of languages; a tag it does not know
|
|
3898
|
+
* (et, ms, nn, …) silently renders English month/weekday names and en-US
|
|
3899
|
+
* separators. Excel and `Intl.DateTimeFormat` localize every locale, so an
|
|
3900
|
+
* unknown tag gets a numfmt locale built from Intl on first use: separators
|
|
3901
|
+
* from `Intl.NumberFormat`, the in-date month names (numfmt's single table is
|
|
3902
|
+
* the form used beside a day number — see `getStandaloneMonthLocale`), the
|
|
3903
|
+
* in-date weekday names (the form `dateStyle: "full"` shows) and the AM/PM
|
|
3904
|
+
* designators. Tags Intl itself does not know keep numfmt's fallback.
|
|
3905
|
+
*/
|
|
3906
|
+
const ensureNumfmtLocale = (locale) => {
|
|
3907
|
+
if (intlRegisteredLocales.has(locale))
|
|
3908
|
+
return;
|
|
3909
|
+
intlRegisteredLocales.add(locale);
|
|
3910
|
+
try {
|
|
3911
|
+
if (getLocale(locale))
|
|
3912
|
+
return;
|
|
3913
|
+
const canonical = Intl.DateTimeFormat.supportedLocalesOf([locale])[0];
|
|
3914
|
+
if (!canonical)
|
|
3915
|
+
return;
|
|
3916
|
+
const numberParts = new Intl.NumberFormat(locale).formatToParts(1234567.5);
|
|
3917
|
+
const part = (type) => numberParts.find((p) => p.type === type)?.value;
|
|
3918
|
+
const weekdayNames = (weekday) => {
|
|
3919
|
+
// 2024-01-07 was a Sunday; numfmt indexes `dddd[0]` as Sunday.
|
|
3920
|
+
const fmt = new Intl.DateTimeFormat(locale, {
|
|
3921
|
+
weekday,
|
|
3922
|
+
day: "numeric",
|
|
3923
|
+
month: "long",
|
|
3924
|
+
year: "numeric",
|
|
3925
|
+
timeZone: "UTC",
|
|
3926
|
+
});
|
|
3927
|
+
return Array.from({ length: 7 }, (_, i) => fmt
|
|
3928
|
+
.formatToParts(new Date(Date.UTC(2024, 0, 7 + i)))
|
|
3929
|
+
.find((p) => p.type === "weekday")?.value ?? "");
|
|
3930
|
+
};
|
|
3931
|
+
const dayPeriod = (hour) => new Intl.DateTimeFormat(locale, {
|
|
3932
|
+
hour: "numeric",
|
|
3933
|
+
hour12: true,
|
|
3934
|
+
timeZone: "UTC",
|
|
3935
|
+
})
|
|
3936
|
+
.formatToParts(new Date(Date.UTC(2024, 0, 7, hour)))
|
|
3937
|
+
.find((p) => p.type === "dayPeriod")?.value;
|
|
3938
|
+
const dateParts = new Intl.DateTimeFormat(locale, {
|
|
3939
|
+
timeZone: "UTC",
|
|
3940
|
+
}).formatToParts(new Date(Date.UTC(2024, 0, 7)));
|
|
3941
|
+
const monthIndex = dateParts.findIndex((p) => p.type === "month");
|
|
3942
|
+
const dayIndex = dateParts.findIndex((p) => p.type === "day");
|
|
3943
|
+
const am = dayPeriod(9);
|
|
3944
|
+
const pm = dayPeriod(21);
|
|
3945
|
+
addLocale({
|
|
3946
|
+
group: part("group") ?? "\u00a0",
|
|
3947
|
+
decimal: part("decimal") ?? ".",
|
|
3948
|
+
mmmm: intlMonthNames(locale, { day: "numeric", month: "long" }),
|
|
3949
|
+
mmm: intlMonthNames(locale, { day: "numeric", month: "short" }),
|
|
3950
|
+
dddd: weekdayNames("long"),
|
|
3951
|
+
ddd: weekdayNames("short"),
|
|
3952
|
+
...(am && pm && am !== pm ? { ampm: [am, pm] } : {}),
|
|
3953
|
+
preferMDY: monthIndex >= 0 && dayIndex >= 0 && monthIndex < dayIndex,
|
|
3954
|
+
}, locale);
|
|
3955
|
+
}
|
|
3956
|
+
catch {
|
|
3957
|
+
// Malformed tag: numfmt keeps its own fallback.
|
|
3958
|
+
}
|
|
3959
|
+
};
|
|
3861
3960
|
const localeForPattern = (pattern, locale) => {
|
|
3862
3961
|
return locale;
|
|
3863
3962
|
};
|
|
@@ -3965,14 +4064,158 @@ const DEFAULT_SEPARATORS = { decimal: ".", group: "," };
|
|
|
3965
4064
|
const separatorsFor = (locale) => {
|
|
3966
4065
|
return DEFAULT_SEPARATORS;
|
|
3967
4066
|
};
|
|
4067
|
+
/**
|
|
4068
|
+
* Rewrite the `[$symbol-LCID]` tokens of one section through `rewrite`,
|
|
4069
|
+
* which receives the bracket content (`€-407`) and returns the new content,
|
|
4070
|
+
* or an empty string to drop the token. Quoted literals, escapes and other
|
|
4071
|
+
* bracket directives are left as written.
|
|
4072
|
+
*/
|
|
4073
|
+
const rewriteSectionLocaleTokens = (section, rewrite) => {
|
|
4074
|
+
let out = "";
|
|
4075
|
+
let inQuotes = false;
|
|
4076
|
+
let inBrackets = false;
|
|
4077
|
+
for (let i = 0; i < section.length; i++) {
|
|
4078
|
+
const char = section[i];
|
|
4079
|
+
if (inQuotes) {
|
|
4080
|
+
inQuotes = char !== '"';
|
|
4081
|
+
}
|
|
4082
|
+
else if (inBrackets) {
|
|
4083
|
+
inBrackets = char !== "]";
|
|
4084
|
+
}
|
|
4085
|
+
else if (char === '"') {
|
|
4086
|
+
inQuotes = true;
|
|
4087
|
+
}
|
|
4088
|
+
else if (char === "[") {
|
|
4089
|
+
const end = section[i + 1] === "$" ? section.indexOf("]", i) : -1;
|
|
4090
|
+
if (end > 0) {
|
|
4091
|
+
const token = rewrite(section.slice(i + 1, end));
|
|
4092
|
+
if (token)
|
|
4093
|
+
out += `[${token}]`;
|
|
4094
|
+
i = end;
|
|
4095
|
+
continue;
|
|
4096
|
+
}
|
|
4097
|
+
inBrackets = true;
|
|
4098
|
+
}
|
|
4099
|
+
else if (char === "\\" || char === "_" || char === "*") {
|
|
4100
|
+
out += char + (section[i + 1] ?? "");
|
|
4101
|
+
i++;
|
|
4102
|
+
continue;
|
|
4103
|
+
}
|
|
4104
|
+
out += char;
|
|
4105
|
+
}
|
|
4106
|
+
return out;
|
|
4107
|
+
};
|
|
4108
|
+
/** `[$€-407]` → `[$€]`; a bare `[$-407]` goes away. */
|
|
4109
|
+
const currencySymbolOnly = (token) => {
|
|
4110
|
+
const dash = token.indexOf("-");
|
|
4111
|
+
const symbol = dash < 0 ? token : token.slice(0, dash);
|
|
4112
|
+
return symbol.length > 1 ? symbol : "";
|
|
4113
|
+
};
|
|
4114
|
+
const separatorLocales = new Map();
|
|
4115
|
+
/**
|
|
4116
|
+
* Companion numfmt locale carrying `locale`'s calendar names and AM/PM with
|
|
4117
|
+
* the workbook's decimal / group separators, so a date section pinned to
|
|
4118
|
+
* `[$-407]` renders German names but a workbook decimal mark in `ss.00`.
|
|
4119
|
+
* Returns `locale` itself when its separators already match.
|
|
4120
|
+
*/
|
|
4121
|
+
const getWorkbookSeparatorLocale = (locale, separators) => {
|
|
4122
|
+
const key = `${locale}\u0000${separators.decimal}\u0000${separators.group}`;
|
|
4123
|
+
const cached = separatorLocales.get(key);
|
|
4124
|
+
if (cached !== undefined)
|
|
4125
|
+
return cached;
|
|
4126
|
+
let overlay = locale;
|
|
4127
|
+
try {
|
|
4128
|
+
const base = getLocale(locale);
|
|
4129
|
+
if (base &&
|
|
4130
|
+
(base.decimal !== separators.decimal || base.group !== separators.group)) {
|
|
4131
|
+
// numfmt locale ids are `[a-z\d]+` with one optional `_` part.
|
|
4132
|
+
const hex = (text) => [...text].map((c) => c.codePointAt(0).toString(16)).join("x");
|
|
4133
|
+
overlay = `sep${hex(separators.decimal)}x${hex(separators.group)}_${locale.replace(/[^a-z\d]/gi, "")}`;
|
|
4134
|
+
addLocale({ ...base, decimal: separators.decimal, group: separators.group }, overlay);
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
catch {
|
|
4138
|
+
overlay = locale;
|
|
4139
|
+
}
|
|
4140
|
+
separatorLocales.set(key, overlay);
|
|
4141
|
+
return overlay;
|
|
4142
|
+
};
|
|
4143
|
+
/**
|
|
4144
|
+
* Re-point a date section's `[$symbol-LCID]` token at the companion locale
|
|
4145
|
+
* with the workbook's separators (`$-407` → `$-sep2ex2c_407`). An LCID numfmt
|
|
4146
|
+
* does not know or one carrying calendar bits stays as written.
|
|
4147
|
+
*/
|
|
4148
|
+
const workbookSeparatorLocaleToken = (separators) => (token) => {
|
|
4149
|
+
const dash = token.indexOf("-");
|
|
4150
|
+
if (dash < 0)
|
|
4151
|
+
return token;
|
|
4152
|
+
const code = token.slice(dash + 1);
|
|
4153
|
+
const wincode = Number.parseInt(code, 16);
|
|
4154
|
+
if (Number.isFinite(wincode) && wincode & 0xff0000)
|
|
4155
|
+
return token;
|
|
4156
|
+
if (!getLocale(code))
|
|
4157
|
+
return token;
|
|
4158
|
+
return `${token.slice(0, dash + 1)}${getWorkbookSeparatorLocale(code, separators)}`;
|
|
4159
|
+
};
|
|
4160
|
+
const isDateSection = (section) => {
|
|
4161
|
+
try {
|
|
4162
|
+
return isDateFormat(section);
|
|
4163
|
+
}
|
|
4164
|
+
catch {
|
|
4165
|
+
return false;
|
|
4166
|
+
}
|
|
4167
|
+
};
|
|
4168
|
+
/**
|
|
4169
|
+
* Excel's `[$symbol-LCID]` token selects the currency symbol and the
|
|
4170
|
+
* language of day/month names and AM/PM; digit grouping and the decimal
|
|
4171
|
+
* separator follow the workbook locale (`[$€-407] #,##0.00` is
|
|
4172
|
+
* `€ 1,234.50` on a US host). numfmt applies the token's locale to the whole
|
|
4173
|
+
* section, separators included, so the locale part is dropped from every
|
|
4174
|
+
* section that is not a date (number, scientific, fraction, text, …); a
|
|
4175
|
+
* date section keeps the token for its names, re-pointed at a companion
|
|
4176
|
+
* locale that carries the workbook's separators for fractional seconds.
|
|
4177
|
+
* Returns `undefined` when nothing had to change.
|
|
4178
|
+
*/
|
|
4179
|
+
const withWorkbookSeparators = (pattern, locale) => {
|
|
4180
|
+
if (!pattern.includes("[$"))
|
|
4181
|
+
return undefined;
|
|
4182
|
+
const sections = splitFormatSections(pattern);
|
|
4183
|
+
let changed = false;
|
|
4184
|
+
let dateTokens;
|
|
4185
|
+
const rewritten = sections.map((section) => {
|
|
4186
|
+
if (!section.includes("[$"))
|
|
4187
|
+
return section;
|
|
4188
|
+
let rewrite = currencySymbolOnly;
|
|
4189
|
+
if (isDateSection(section)) {
|
|
4190
|
+
dateTokens ??= workbookSeparatorLocaleToken(separatorsFor());
|
|
4191
|
+
rewrite = dateTokens;
|
|
4192
|
+
}
|
|
4193
|
+
const stripped = rewriteSectionLocaleTokens(section, rewrite);
|
|
4194
|
+
changed ||= stripped !== section;
|
|
4195
|
+
return stripped;
|
|
4196
|
+
});
|
|
4197
|
+
return changed ? rewritten.join(";") : undefined;
|
|
4198
|
+
};
|
|
4199
|
+
const workbookSeparatorPatternCache = new Map();
|
|
4200
|
+
const withWorkbookSeparatorsCached = (code, locale) => {
|
|
4201
|
+
const key = `${""}\u0000${code}`;
|
|
4202
|
+
let rewritten = workbookSeparatorPatternCache.get(key);
|
|
4203
|
+
if (rewritten === undefined) {
|
|
4204
|
+
rewritten = withWorkbookSeparators(code) ?? code;
|
|
4205
|
+
workbookSeparatorPatternCache.set(key, rewritten);
|
|
4206
|
+
}
|
|
4207
|
+
return rewritten;
|
|
4208
|
+
};
|
|
3968
4209
|
const ssfFormat = (pattern, value, locale) => {
|
|
3969
4210
|
try {
|
|
4211
|
+
if (locale)
|
|
4212
|
+
;
|
|
3970
4213
|
let shortAmPm = shortAmPmPatternCache.get(pattern);
|
|
3971
4214
|
if (shortAmPm === undefined && !shortAmPmPatternCache.has(pattern)) {
|
|
3972
4215
|
shortAmPm = withShortAmPmMarker(pattern);
|
|
3973
4216
|
shortAmPmPatternCache.set(pattern, shortAmPm);
|
|
3974
4217
|
}
|
|
3975
|
-
const code = shortAmPm ?? pattern;
|
|
4218
|
+
const code = withWorkbookSeparatorsCached(shortAmPm ?? pattern, locale);
|
|
3976
4219
|
let patternLocale = localeForPattern(pattern, locale);
|
|
3977
4220
|
if (shortAmPm !== undefined && patternLocale) {
|
|
3978
4221
|
patternLocale = getShortAmPmLocale(patternLocale);
|
|
@@ -4675,10 +4918,12 @@ class FormulaHelpersClass {
|
|
|
4675
4918
|
!isRaw && Object.prototype.hasOwnProperty.call(param, "value");
|
|
4676
4919
|
if (hasValueProp) {
|
|
4677
4920
|
if (param.value == null) {
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4921
|
+
// A blank cell is not an omitted argument (only omission takes
|
|
4922
|
+
// `defValue`): Excel reads it as "" in a text slot, 0 in a number
|
|
4923
|
+
// slot and FALSE in a logical slot.
|
|
4924
|
+
if (type === Types.STRING) return "";
|
|
4925
|
+
if (type === Types.NUMBER || type === Types.NUMBER_NO_BOOLEAN) return 0;
|
|
4926
|
+
if (type === Types.BOOLEAN) return false;
|
|
4682
4927
|
param = param.value;
|
|
4683
4928
|
} else {
|
|
4684
4929
|
param = param.value;
|
|
@@ -4758,6 +5003,7 @@ class FormulaHelpersClass {
|
|
|
4758
5003
|
* @param range2
|
|
4759
5004
|
*/
|
|
4760
5005
|
retrieveRanges(context, range1, range2) {
|
|
5006
|
+
if (range1 === undefined) throw FormulaError.ARG_MISSING([Types.ARRAY]);
|
|
4761
5007
|
// process args
|
|
4762
5008
|
range2 = Address.extend(range1, range2);
|
|
4763
5009
|
|
|
@@ -4774,6 +5020,9 @@ class FormulaHelpersClass {
|
|
|
4774
5020
|
}
|
|
4775
5021
|
|
|
4776
5022
|
retrieveArg(context, arg) {
|
|
5023
|
+
// `null` is an explicitly skipped argument (`SUMIF(A1:A5,,B1:B5)`);
|
|
5024
|
+
// `undefined` is one the call never supplied — too few arguments.
|
|
5025
|
+
if (arg === undefined) throw FormulaError.ARG_MISSING([Types.ARRAY]);
|
|
4777
5026
|
if (arg === null) return { value: 0, isArray: false, omitted: true };
|
|
4778
5027
|
const res = context.utils.extractRefValue(arg);
|
|
4779
5028
|
return { value: res.val, isArray: res.isArray, ref: arg.ref };
|
|
@@ -6343,33 +6592,48 @@ const TextFunctions = {
|
|
|
6343
6592
|
return text.slice(0, m.index) + expanded + text.slice(m.index + m[0].length);
|
|
6344
6593
|
},
|
|
6345
6594
|
|
|
6595
|
+
/**
|
|
6596
|
+
* JOIN(delimiter, value_or_array1, [value_or_array2, …]) — Google Sheets.
|
|
6597
|
+
* Ranges / arrays contribute every element; blanks and empty text are
|
|
6598
|
+
* skipped; values spell as TEXTJOIN spells them (TRUE, 15-digit numbers) and
|
|
6599
|
+
* an error among the values propagates. The rnc engine mirrors these rules.
|
|
6600
|
+
*/
|
|
6346
6601
|
JOIN: (delimiter, ...values) => {
|
|
6347
6602
|
delimiter = H$a.accept(delimiter, Types.STRING);
|
|
6348
6603
|
const result = [];
|
|
6349
6604
|
|
|
6350
|
-
H$a.flattenParams(values,
|
|
6351
|
-
if (item
|
|
6352
|
-
|
|
6353
|
-
|
|
6605
|
+
H$a.flattenParams(values, Types.STRING, true, (item) => {
|
|
6606
|
+
if (item === undefined || item === null) return;
|
|
6607
|
+
const str = H$a.accept(item, Types.STRING);
|
|
6608
|
+
if (str !== "") result.push(str);
|
|
6354
6609
|
});
|
|
6355
6610
|
|
|
6356
6611
|
return result.join(delimiter);
|
|
6357
6612
|
},
|
|
6358
6613
|
|
|
6614
|
+
/**
|
|
6615
|
+
* SPLIT(text, delimiter, [split_by_each=TRUE], [remove_empty_text=TRUE]) —
|
|
6616
|
+
* Google Sheets. Pieces stay text (no number coercion); an empty delimiter
|
|
6617
|
+
* is #VALUE!; when every piece is removed the result is empty text. The
|
|
6618
|
+
* rnc engine mirrors these rules.
|
|
6619
|
+
*/
|
|
6359
6620
|
SPLIT: (text, delimiter, splitByEach, removeEmpty) => {
|
|
6360
6621
|
text = H$a.accept(text, Types.STRING);
|
|
6361
6622
|
delimiter = H$a.accept(delimiter, Types.STRING);
|
|
6362
6623
|
splitByEach = H$a.accept(splitByEach, Types.BOOLEAN, true);
|
|
6363
6624
|
removeEmpty = H$a.accept(removeEmpty, Types.BOOLEAN, true);
|
|
6625
|
+
if (delimiter === "") throw FormulaError.VALUE;
|
|
6364
6626
|
|
|
6365
6627
|
let parts;
|
|
6366
6628
|
if (splitByEach) {
|
|
6367
|
-
//
|
|
6368
|
-
|
|
6369
|
-
const
|
|
6370
|
-
|
|
6371
|
-
)
|
|
6372
|
-
|
|
6629
|
+
// Every character of the delimiter is a separator — a literal character,
|
|
6630
|
+
// never a RegExp class ("d" must not become \d, "b" must not become \b).
|
|
6631
|
+
const separators = new Set(delimiter);
|
|
6632
|
+
parts = [""];
|
|
6633
|
+
for (const ch of text) {
|
|
6634
|
+
if (separators.has(ch)) parts.push("");
|
|
6635
|
+
else parts[parts.length - 1] += ch;
|
|
6636
|
+
}
|
|
6373
6637
|
} else {
|
|
6374
6638
|
// Split by the entire delimiter string
|
|
6375
6639
|
parts = text.split(delimiter);
|
|
@@ -6378,6 +6642,7 @@ const TextFunctions = {
|
|
|
6378
6642
|
if (removeEmpty) {
|
|
6379
6643
|
parts = parts.filter((part) => part !== "");
|
|
6380
6644
|
}
|
|
6645
|
+
if (parts.length === 0) return "";
|
|
6381
6646
|
|
|
6382
6647
|
// Return as a single row array
|
|
6383
6648
|
return [parts];
|
|
@@ -12353,20 +12618,56 @@ const collectNumericValues = (array) => {
|
|
|
12353
12618
|
// FORECAST.ETS helpers — Holt-Winters AAA implementation.
|
|
12354
12619
|
// ---------------------------------------------------------------------
|
|
12355
12620
|
|
|
12621
|
+
// Excel's `seasonality` argument: 1 (the default) auto-detects the
|
|
12622
|
+
// period, 0 fits without a seasonal component, 2..8760 forces the
|
|
12623
|
+
// period; anything else is #NUM!. Fractions are truncated.
|
|
12624
|
+
const ETS_MAX_SEASONALITY = 8760;
|
|
12625
|
+
const forecastEtsSeasonality = (seasonalityArg) => {
|
|
12626
|
+
if (seasonalityArg == null) return 1;
|
|
12627
|
+
const value = H$8.accept(seasonalityArg, Types.NUMBER, 1);
|
|
12628
|
+
if (value == null) return 1;
|
|
12629
|
+
const seasonality = Math.floor(value);
|
|
12630
|
+
if (seasonality < 0 || seasonality > ETS_MAX_SEASONALITY) throw FormulaError.NUM;
|
|
12631
|
+
return seasonality;
|
|
12632
|
+
};
|
|
12633
|
+
|
|
12634
|
+
// `data_completion` must be 0 or 1 and `aggregation` 1..7 (AVERAGE,
|
|
12635
|
+
// COUNT, COUNTA, MAX, MEDIAN, MIN, SUM), otherwise #NUM!. Neither
|
|
12636
|
+
// changes the fit: the timeline is used as given (no gap filling,
|
|
12637
|
+
// duplicates are not aggregated).
|
|
12638
|
+
const forecastEtsValidateOptions = (dataCompletionArg, aggregationArg) => {
|
|
12639
|
+
if (dataCompletionArg != null) {
|
|
12640
|
+
const completion = Math.floor(H$8.accept(dataCompletionArg, Types.NUMBER, 1));
|
|
12641
|
+
if (completion !== 0 && completion !== 1) throw FormulaError.NUM;
|
|
12642
|
+
}
|
|
12643
|
+
if (aggregationArg != null) {
|
|
12644
|
+
const aggregation = Math.floor(H$8.accept(aggregationArg, Types.NUMBER, 1));
|
|
12645
|
+
if (aggregation < 1 || aggregation > 7) throw FormulaError.NUM;
|
|
12646
|
+
}
|
|
12647
|
+
};
|
|
12648
|
+
|
|
12356
12649
|
// Pair up timeline + values, sort by timeline, infer the constant
|
|
12357
12650
|
// step (Excel requires a uniformly-spaced timeline), and resolve
|
|
12358
|
-
// the seasonality parameter (auto-
|
|
12651
|
+
// the seasonality parameter (Excel spelling: 1 auto-detects, 0 is no
|
|
12652
|
+
// seasonality, n > 1 is the period).
|
|
12359
12653
|
const forecastEtsPrep = (ys, xs, seasonality) => {
|
|
12360
12654
|
if (xs.length !== ys.length) throw FormulaError.NA;
|
|
12361
12655
|
const pairs = [];
|
|
12362
12656
|
for (let i = 0; i < xs.length; i++) {
|
|
12363
12657
|
const x = xs[i];
|
|
12364
12658
|
const yv = ys[i];
|
|
12659
|
+
// An error anywhere in the series is the result (Excel and the Rust
|
|
12660
|
+
// port's `series_number`); other non-numbers (text, blanks) are skipped.
|
|
12661
|
+
if (x instanceof FormulaError) throw x;
|
|
12662
|
+
if (yv instanceof FormulaError) throw yv;
|
|
12365
12663
|
if (typeof x !== "number") continue;
|
|
12366
12664
|
if (typeof yv !== "number") continue;
|
|
12367
12665
|
pairs.push([x, yv]);
|
|
12368
12666
|
}
|
|
12369
|
-
|
|
12667
|
+
// Excel fits from two pairs (there is no four-pair floor); fewer is
|
|
12668
|
+
// #N/A, and two pairs on the same timeline point have no step (#NUM!
|
|
12669
|
+
// below). rowsncolumns/spreadsheet#642 B-01.
|
|
12670
|
+
if (pairs.length < 2) throw FormulaError.NA;
|
|
12370
12671
|
pairs.sort((a, b) => a[0] - b[0]);
|
|
12371
12672
|
const sortedX = pairs.map(p => p[0]);
|
|
12372
12673
|
const y = pairs.map(p => p[1]);
|
|
@@ -12382,12 +12683,12 @@ const forecastEtsPrep = (ys, xs, seasonality) => {
|
|
|
12382
12683
|
diffs.sort((a, b) => a - b);
|
|
12383
12684
|
const step = diffs[Math.floor(diffs.length / 2)];
|
|
12384
12685
|
let period;
|
|
12385
|
-
if (seasonality ===
|
|
12686
|
+
if (seasonality === 0) {
|
|
12386
12687
|
period = 1; // No seasonality (linear Holt).
|
|
12387
|
-
} else if (seasonality
|
|
12388
|
-
period = seasonality;
|
|
12389
|
-
} else {
|
|
12688
|
+
} else if (seasonality === 1) {
|
|
12390
12689
|
period = detectSeasonality(y);
|
|
12690
|
+
} else {
|
|
12691
|
+
period = seasonality;
|
|
12391
12692
|
}
|
|
12392
12693
|
return { y, lastX: sortedX[sortedX.length - 1], step, period };
|
|
12393
12694
|
};
|
|
@@ -12404,7 +12705,7 @@ const detectSeasonality = (y) => {
|
|
|
12404
12705
|
const n = y.length;
|
|
12405
12706
|
if (n < 4) return 1;
|
|
12406
12707
|
const maxPeriod = Math.min(Math.floor(n / 2), 12);
|
|
12407
|
-
const baseline = runHoltWinters(y, 1, 0.5, 0.1, 0
|
|
12708
|
+
const baseline = runHoltWinters(y, 1, 0.5, 0.1, 0).sse;
|
|
12408
12709
|
let bestPeriod = 1;
|
|
12409
12710
|
let bestSse = baseline;
|
|
12410
12711
|
for (let m = 2; m <= maxPeriod; m++) {
|
|
@@ -12415,7 +12716,7 @@ const detectSeasonality = (y) => {
|
|
|
12415
12716
|
for (const alpha of [0.3, 0.7]) {
|
|
12416
12717
|
for (const beta of [0.1, 0.3]) {
|
|
12417
12718
|
for (const gamma of [0.3, 0.7]) {
|
|
12418
|
-
const sse = runHoltWinters(y, m, alpha, beta, gamma
|
|
12719
|
+
const sse = runHoltWinters(y, m, alpha, beta, gamma).sse;
|
|
12419
12720
|
if (sse < mSse) mSse = sse;
|
|
12420
12721
|
}
|
|
12421
12722
|
}
|
|
@@ -12431,11 +12732,10 @@ const detectSeasonality = (y) => {
|
|
|
12431
12732
|
};
|
|
12432
12733
|
|
|
12433
12734
|
// Fit Holt-Winters additive model with the supplied (or auto-fit) α,
|
|
12434
|
-
// β, γ smoothing parameters. Returns fitted in-sample values
|
|
12435
|
-
// `
|
|
12436
|
-
// (no seasonal component).
|
|
12437
|
-
const fitHoltWinters = (y, period
|
|
12438
|
-
y.length;
|
|
12735
|
+
// β, γ smoothing parameters. Returns the fitted in-sample values and the
|
|
12736
|
+
// terminal state (`forecastAt(h)` extrapolates it to any horizon in O(1)).
|
|
12737
|
+
// period=1 collapses to Holt linear (no seasonal component).
|
|
12738
|
+
const fitHoltWinters = (y, period) => {
|
|
12439
12739
|
// Grid search the smoothing params. The 5×5×5 grid is coarse but
|
|
12440
12740
|
// close enough to the SSE-minimizing optimum that Excel's results
|
|
12441
12741
|
// round-match within ~1% on the seasonal demo series.
|
|
@@ -12445,7 +12745,7 @@ const fitHoltWinters = (y, period, horizon) => {
|
|
|
12445
12745
|
for (const alpha of grid) {
|
|
12446
12746
|
for (const beta of grid) {
|
|
12447
12747
|
for (const gamma of candidateGammas) {
|
|
12448
|
-
const result = runHoltWinters(y, period, alpha, beta, gamma
|
|
12748
|
+
const result = runHoltWinters(y, period, alpha, beta, gamma);
|
|
12449
12749
|
if (best == null || result.sse < best.sse) {
|
|
12450
12750
|
best = { alpha, beta, gamma, ...result };
|
|
12451
12751
|
}
|
|
@@ -12455,7 +12755,25 @@ const fitHoltWinters = (y, period, horizon) => {
|
|
|
12455
12755
|
return best;
|
|
12456
12756
|
};
|
|
12457
12757
|
|
|
12458
|
-
|
|
12758
|
+
// The forecast `h` whole steps past the last observation from a run's
|
|
12759
|
+
// terminal state: level + h·trend + seasonal[(n + h - 1) mod m]. O(1) in
|
|
12760
|
+
// `h` — the old per-step forecast array made `FORECAST.ETS(1E15, …)`
|
|
12761
|
+
// hang the tab (and trapped the Rust engine's wasm with a capacity
|
|
12762
|
+
// overflow that poisoned the whole store). rowsncolumns/spreadsheet#642 B-06.
|
|
12763
|
+
const holtWintersForecastAt = ({ n, m, level, trend, seasonal }, h) => {
|
|
12764
|
+
const seasonComp = m > 1 ? seasonal[(n + h - 1) % m] : 0;
|
|
12765
|
+
return level + h * trend + seasonComp;
|
|
12766
|
+
};
|
|
12767
|
+
|
|
12768
|
+
// One additive Holt-Winters pass with fixed smoothing parameters. The
|
|
12769
|
+
// initial level is the level one step *before* the series (first value
|
|
12770
|
+
// or first-cycle mean, minus the trend), so the one-step-ahead prediction
|
|
12771
|
+
// of the first observation is the observation itself and a perfectly
|
|
12772
|
+
// linear history has zero residual — an initial level *at* the first
|
|
12773
|
+
// value predicted y₀ + trend for y₀, and that spurious residual pulled
|
|
12774
|
+
// the grid search off the exact continuation (10,20,…,100 → 109.56
|
|
12775
|
+
// instead of Excel's 110). rowsncolumns/spreadsheet#642 B-01.
|
|
12776
|
+
const runHoltWinters = (y, period, alpha, beta, gamma) => {
|
|
12459
12777
|
const n = y.length;
|
|
12460
12778
|
const m = period;
|
|
12461
12779
|
const fitted = new Array(n).fill(null);
|
|
@@ -12463,21 +12781,21 @@ const runHoltWinters = (y, period, alpha, beta, gamma, horizon) => {
|
|
|
12463
12781
|
const seasonal = new Array(m).fill(0);
|
|
12464
12782
|
if (m > 1 && n >= 2 * m) {
|
|
12465
12783
|
// Initial seasonal indices = first-cycle deviations from the
|
|
12466
|
-
// first-cycle mean. Initial
|
|
12467
|
-
//
|
|
12468
|
-
//
|
|
12784
|
+
// first-cycle mean. Initial trend = average of per-step changes
|
|
12785
|
+
// between first and second cycle, normalized by m. Initial level
|
|
12786
|
+
// = mean of first cycle, one trend step back.
|
|
12469
12787
|
let sum1 = 0;
|
|
12470
12788
|
for (let i = 0; i < m; i++) sum1 += y[i];
|
|
12471
12789
|
const mean1 = sum1 / m;
|
|
12472
|
-
level = mean1;
|
|
12473
12790
|
let trendSum = 0;
|
|
12474
12791
|
for (let i = 0; i < m; i++) trendSum += (y[i + m] - y[i]) / m;
|
|
12475
12792
|
trend = trendSum / m;
|
|
12793
|
+
level = mean1 - trend;
|
|
12476
12794
|
for (let i = 0; i < m; i++) seasonal[i] = y[i] - mean1;
|
|
12477
12795
|
} else {
|
|
12478
|
-
// Holt linear initialization.
|
|
12479
|
-
level = y[0];
|
|
12796
|
+
// Holt linear initialization, one trend step back.
|
|
12480
12797
|
trend = n > 1 ? y[1] - y[0] : 0;
|
|
12798
|
+
level = y[0] - trend;
|
|
12481
12799
|
}
|
|
12482
12800
|
let sse = 0;
|
|
12483
12801
|
let count = 0;
|
|
@@ -12496,12 +12814,12 @@ const runHoltWinters = (y, period, alpha, beta, gamma, horizon) => {
|
|
|
12496
12814
|
seasonal[seasonIdx] = gamma * (y[t] - level) + (1 - gamma) * seasonComp;
|
|
12497
12815
|
}
|
|
12498
12816
|
}
|
|
12499
|
-
const
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
|
|
12503
|
-
|
|
12504
|
-
|
|
12817
|
+
const state = { n, m, level, trend, seasonal };
|
|
12818
|
+
return {
|
|
12819
|
+
fitted,
|
|
12820
|
+
sse: count > 0 ? sse / count : Infinity,
|
|
12821
|
+
forecastAt: (h) => holtWintersForecastAt(state, h),
|
|
12822
|
+
};
|
|
12505
12823
|
};
|
|
12506
12824
|
|
|
12507
12825
|
const percentileInclusive = (values, k) => {
|
|
@@ -13267,20 +13585,18 @@ const DistributionFunctions = {
|
|
|
13267
13585
|
// trend. We use the same idea — scan candidate periods 2..N/2,
|
|
13268
13586
|
// capped at 12 (Excel's default cap for monthly data), and pick
|
|
13269
13587
|
// the one with the lowest in-sample SSE for the fitted Holt-Winters
|
|
13270
|
-
// model.
|
|
13271
|
-
// linear).
|
|
13588
|
+
// model. `seasonality` follows Excel: 1 (default) auto-detects, 0
|
|
13589
|
+
// forces "no seasonality" (Holt linear), n > 1 is the period. The
|
|
13590
|
+
// Rust engine (formualizer `stats/forecast_ets.rs`) is a port of this
|
|
13591
|
+
// code, so both engines agree to 1e-9 on the same inputs.
|
|
13272
13592
|
// -------------------------------------------------------------------
|
|
13273
13593
|
|
|
13274
13594
|
'FORECAST.ETS': (targetDate, values, timeline, seasonalityArg, dataCompletionArg, aggregationArg) => {
|
|
13275
13595
|
const target = H$8.accept(targetDate, Types.NUMBER);
|
|
13276
13596
|
const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
|
|
13277
13597
|
const xs = H$8.accept(timeline, Types.ARRAY, undefined, true, true);
|
|
13278
|
-
const seasonality = seasonalityArg
|
|
13279
|
-
|
|
13280
|
-
: Math.floor(H$8.accept(seasonalityArg, Types.NUMBER, 1));
|
|
13281
|
-
// dataCompletion + aggregation honored only at the prep step —
|
|
13282
|
-
// gap-filling uses the previous value (matches Excel's
|
|
13283
|
-
// dataCompletion=1 default).
|
|
13598
|
+
const seasonality = forecastEtsSeasonality(seasonalityArg);
|
|
13599
|
+
forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
|
|
13284
13600
|
const { y, lastX, step, period } = forecastEtsPrep(ys, xs, seasonality);
|
|
13285
13601
|
if (target < lastX) {
|
|
13286
13602
|
// Excel returns #NUM! for in-history targets in FORECAST.ETS
|
|
@@ -13288,12 +13604,14 @@ const DistributionFunctions = {
|
|
|
13288
13604
|
throw FormulaError.NUM;
|
|
13289
13605
|
}
|
|
13290
13606
|
const horizon = Math.round((target - lastX) / step);
|
|
13607
|
+
if (!Number.isFinite(horizon)) throw FormulaError.NUM;
|
|
13291
13608
|
if (horizon < 1) return y[y.length - 1];
|
|
13292
|
-
const
|
|
13293
|
-
|
|
13609
|
+
const forecast = fitHoltWinters(y, period).forecastAt(horizon);
|
|
13610
|
+
if (!Number.isFinite(forecast)) throw FormulaError.NUM;
|
|
13611
|
+
return forecast;
|
|
13294
13612
|
},
|
|
13295
13613
|
|
|
13296
|
-
'FORECAST.ETS.CONFINT': (targetDate, values, timeline, confidenceArg, seasonalityArg) => {
|
|
13614
|
+
'FORECAST.ETS.CONFINT': (targetDate, values, timeline, confidenceArg, seasonalityArg, dataCompletionArg, aggregationArg) => {
|
|
13297
13615
|
const target = H$8.accept(targetDate, Types.NUMBER);
|
|
13298
13616
|
const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
|
|
13299
13617
|
const xs = H$8.accept(timeline, Types.ARRAY, undefined, true, true);
|
|
@@ -13301,13 +13619,13 @@ const DistributionFunctions = {
|
|
|
13301
13619
|
? 0.95
|
|
13302
13620
|
: H$8.accept(confidenceArg, Types.NUMBER, 0.95);
|
|
13303
13621
|
if (confidence <= 0 || confidence >= 1) throw FormulaError.NUM;
|
|
13304
|
-
const seasonality = seasonalityArg
|
|
13305
|
-
|
|
13306
|
-
: Math.floor(H$8.accept(seasonalityArg, Types.NUMBER, 1));
|
|
13622
|
+
const seasonality = forecastEtsSeasonality(seasonalityArg);
|
|
13623
|
+
forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
|
|
13307
13624
|
const { y, lastX, step, period } = forecastEtsPrep(ys, xs, seasonality);
|
|
13308
13625
|
if (target < lastX) throw FormulaError.NUM;
|
|
13309
13626
|
const horizon = Math.max(1, Math.round((target - lastX) / step));
|
|
13310
|
-
|
|
13627
|
+
if (!Number.isFinite(horizon)) throw FormulaError.NUM;
|
|
13628
|
+
const { fitted } = fitHoltWinters(y, period);
|
|
13311
13629
|
// RMSE of in-sample residuals — z-scaled to the requested
|
|
13312
13630
|
// confidence level. Excel uses √(horizon) scaling on the
|
|
13313
13631
|
// forecast variance.
|
|
@@ -13320,26 +13638,28 @@ const DistributionFunctions = {
|
|
|
13320
13638
|
}
|
|
13321
13639
|
const rmse = n > 0 ? Math.sqrt(sse / n) : 0;
|
|
13322
13640
|
const z = jStat.normal.inv(1 - (1 - confidence) / 2, 0, 1);
|
|
13323
|
-
|
|
13641
|
+
const halfWidth = z * rmse * Math.sqrt(horizon);
|
|
13642
|
+
if (!Number.isFinite(halfWidth)) throw FormulaError.NUM;
|
|
13643
|
+
return halfWidth;
|
|
13324
13644
|
},
|
|
13325
13645
|
|
|
13326
|
-
'FORECAST.ETS.SEASONALITY': (values, timeline) => {
|
|
13646
|
+
'FORECAST.ETS.SEASONALITY': (values, timeline, dataCompletionArg, aggregationArg) => {
|
|
13327
13647
|
const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
|
|
13328
13648
|
const xs = H$8.accept(timeline, Types.ARRAY, undefined, true, true);
|
|
13329
|
-
|
|
13330
|
-
|
|
13649
|
+
forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
|
|
13650
|
+
const { period } = forecastEtsPrep(ys, xs, 1);
|
|
13651
|
+
return period;
|
|
13331
13652
|
},
|
|
13332
13653
|
|
|
13333
|
-
'FORECAST.ETS.STAT': (values, timeline, statTypeArg, seasonalityArg) => {
|
|
13654
|
+
'FORECAST.ETS.STAT': (values, timeline, statTypeArg, seasonalityArg, dataCompletionArg, aggregationArg) => {
|
|
13334
13655
|
const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
|
|
13335
13656
|
const xs = H$8.accept(timeline, Types.ARRAY, undefined, true, true);
|
|
13336
13657
|
const statType = Math.floor(H$8.accept(statTypeArg, Types.NUMBER));
|
|
13337
13658
|
if (statType < 1 || statType > 8) throw FormulaError.NUM;
|
|
13338
|
-
const seasonality = seasonalityArg
|
|
13339
|
-
|
|
13340
|
-
: Math.floor(H$8.accept(seasonalityArg, Types.NUMBER, 1));
|
|
13659
|
+
const seasonality = forecastEtsSeasonality(seasonalityArg);
|
|
13660
|
+
forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
|
|
13341
13661
|
const { y, period } = forecastEtsPrep(ys, xs, seasonality);
|
|
13342
|
-
const { alpha, beta, gamma, fitted } = fitHoltWinters(y, period
|
|
13662
|
+
const { alpha, beta, gamma, fitted } = fitHoltWinters(y, period);
|
|
13343
13663
|
// Excel STAT codes:
|
|
13344
13664
|
// 1: alpha 2: beta 3: gamma 4: MASE
|
|
13345
13665
|
// 5: SMAPE 6: MAE 7: RMSE 8: detected step size
|
|
@@ -14424,6 +14744,9 @@ const DistributionFunctions = {
|
|
|
14424
14744
|
const prediction = truncated.reduce((sum, value, idx) => sum + value * slopes[idx], 0) + intercept;
|
|
14425
14745
|
return [prediction];
|
|
14426
14746
|
});
|
|
14747
|
+
// A single new_x is a plain number in Excel (and in the Rust
|
|
14748
|
+
// engine); a 1x1 array would anchor an array formula on the cell.
|
|
14749
|
+
if (result.length === 1) return result[0][0];
|
|
14427
14750
|
return result;
|
|
14428
14751
|
},
|
|
14429
14752
|
|
|
@@ -15406,7 +15729,7 @@ const StatisticalFunctions = {
|
|
|
15406
15729
|
COUNTIF: (range, criteria) => {
|
|
15407
15730
|
// do not flatten the array
|
|
15408
15731
|
range = H$7.accept(range, Types.ARRAY, undefined, false, true);
|
|
15409
|
-
const isCriteriaArray = criteria
|
|
15732
|
+
const isCriteriaArray = criteria?.isArray;
|
|
15410
15733
|
criteria = H$7.accept(criteria);
|
|
15411
15734
|
|
|
15412
15735
|
const countMatching = (parsed) => {
|
|
@@ -16721,7 +17044,15 @@ const MathFunctions = {
|
|
|
16721
17044
|
FLOOR: (number, significance) => {
|
|
16722
17045
|
number = H$6.accept(number, Types.NUMBER);
|
|
16723
17046
|
significance = H$6.accept(significance, Types.NUMBER, 1);
|
|
16724
|
-
|
|
17047
|
+
// Excel: a zero significance is #DIV/0! for FLOOR of a non-zero number
|
|
17048
|
+
// (FLOOR(0,0) is 0; CEILING, FLOOR.MATH and FLOOR.PRECISE return 0); a
|
|
17049
|
+
// positive number with a negative significance is #NUM!; a negative
|
|
17050
|
+
// number rounds away from zero unless the significance is negative too
|
|
17051
|
+
// (FLOOR(-2.5,2) = -4, FLOOR(-2.5,-2) = -2).
|
|
17052
|
+
if (significance === 0) {
|
|
17053
|
+
if (number === 0) return 0;
|
|
17054
|
+
throw FormulaError.DIV0;
|
|
17055
|
+
}
|
|
16725
17056
|
if (number > 0 && significance < 0) throw FormulaError.NUM;
|
|
16726
17057
|
const absSignificance = Math.abs(significance);
|
|
16727
17058
|
const quotient = excelQuotient(Math.abs(number), absSignificance);
|
|
@@ -16746,6 +17077,7 @@ const MathFunctions = {
|
|
|
16746
17077
|
// mode can be 0 or any other number, 0 means away from zero
|
|
16747
17078
|
// the official documentation seems wrong
|
|
16748
17079
|
mode = H$6.accept(mode, Types.NUMBER, 0);
|
|
17080
|
+
if (significance === 0) return 0;
|
|
16749
17081
|
// The Mode argument does not affect positive numbers.
|
|
16750
17082
|
if (mode === 0 || number >= 0) {
|
|
16751
17083
|
// away from zero
|
|
@@ -16758,7 +17090,8 @@ const MathFunctions = {
|
|
|
16758
17090
|
"FLOOR.PRECISE": (number, significance) => {
|
|
16759
17091
|
number = H$6.accept(number, Types.NUMBER);
|
|
16760
17092
|
significance = H$6.accept(significance, Types.NUMBER, 1);
|
|
16761
|
-
//
|
|
17093
|
+
// A zero significance is 0 (Excel); otherwise the mathematical floor.
|
|
17094
|
+
if (significance === 0) return 0;
|
|
16762
17095
|
return MathFunctions.FLOOR(number, Math.abs(significance));
|
|
16763
17096
|
},
|
|
16764
17097
|
|
|
@@ -17264,6 +17597,22 @@ const MathFunctions = {
|
|
|
17264
17597
|
return result;
|
|
17265
17598
|
},
|
|
17266
17599
|
|
|
17600
|
+
/**
|
|
17601
|
+
* PERCENTOF(data_subset, data_all) — SUM(data_subset) / SUM(data_all) under
|
|
17602
|
+
* SUM's rules (text / logical / blank cells inside ranges are skipped, error
|
|
17603
|
+
* values propagate, a non-numeric scalar is #VALUE!); a zero whole is
|
|
17604
|
+
* #DIV/0!. Excel 365's helper for GROUPBY / PIVOTBY percent-of-total.
|
|
17605
|
+
*/
|
|
17606
|
+
PERCENTOF: (dataSubset, dataAll) => {
|
|
17607
|
+
if (H$6.isOmitted(dataSubset) || H$6.isOmitted(dataAll)) {
|
|
17608
|
+
throw FormulaError.ARG_MISSING([Types.NUMBER]);
|
|
17609
|
+
}
|
|
17610
|
+
const subset = MathFunctions.SUM(dataSubset);
|
|
17611
|
+
const all = MathFunctions.SUM(dataAll);
|
|
17612
|
+
if (all === 0) throw FormulaError.DIV0;
|
|
17613
|
+
return subset / all;
|
|
17614
|
+
},
|
|
17615
|
+
|
|
17267
17616
|
/**
|
|
17268
17617
|
* This functions requires instance of {@link FormulaParser}.
|
|
17269
17618
|
*/
|
|
@@ -17771,6 +18120,11 @@ const applyToArray$1 = (value, checkFn) => {
|
|
|
17771
18120
|
* Get the number of values that evaluate to true and false.
|
|
17772
18121
|
* Cast Number and "TRUE", "FALSE" to boolean.
|
|
17773
18122
|
* Ignore unrelated values.
|
|
18123
|
+
* An error value — a direct argument (`AND(1/0, FALSE)`) or a cell inside a
|
|
18124
|
+
* reference — is the result, whatever the other arguments decide: Excel never
|
|
18125
|
+
* short-circuits AND/OR/XOR past an error, and the first one in argument order
|
|
18126
|
+
* wins (rowsncolumns/spreadsheet#642 B-08). `flattenParams` only throws error
|
|
18127
|
+
* values itself when a value type is requested.
|
|
17774
18128
|
* @ignore
|
|
17775
18129
|
* @param {any[]} params
|
|
17776
18130
|
* @return {number[]}
|
|
@@ -17778,6 +18132,8 @@ const applyToArray$1 = (value, checkFn) => {
|
|
|
17778
18132
|
function getNumLogicalValue(params) {
|
|
17779
18133
|
let numTrue = 0, numFalse = 0;
|
|
17780
18134
|
H$4.flattenParams(params, null, true, (val, info) => {
|
|
18135
|
+
if (val instanceof FormulaError)
|
|
18136
|
+
throw val;
|
|
17781
18137
|
const type = typeof val;
|
|
17782
18138
|
if (type === "string") {
|
|
17783
18139
|
// Text inside a reference or array is ignored; a direct text
|
|
@@ -19850,6 +20206,10 @@ const ReferenceFunctions = {
|
|
|
19850
20206
|
// Retrieve actual data to determine effective column count
|
|
19851
20207
|
const { val: data } = context.utils.extractRefValue(obj, context.position);
|
|
19852
20208
|
const colCount = Array.isArray(data) && Array.isArray(data[0]) ? data[0].length : 1;
|
|
20209
|
+
// One column (`COLUMN(C:C)`, `COLUMN(B2:B5)`) is a plain number, as
|
|
20210
|
+
// Excel shows it and as the Rust engine returns it; a 1x1 array would
|
|
20211
|
+
// make the host anchor an array formula on the cell.
|
|
20212
|
+
if (colCount <= 1) return fromCol;
|
|
19853
20213
|
const result = [];
|
|
19854
20214
|
for (let i = 0; i < colCount; i++) {
|
|
19855
20215
|
result.push(fromCol + i);
|
|
@@ -19866,7 +20226,11 @@ const ReferenceFunctions = {
|
|
|
19866
20226
|
if (obj == null) {
|
|
19867
20227
|
throw Error("COLUMNS requires one argument");
|
|
19868
20228
|
}
|
|
19869
|
-
if (
|
|
20229
|
+
if (obj instanceof FormulaError) throw obj;
|
|
20230
|
+
// An array (`{1,2;3,4}`, `SEQUENCE(3,2)`, `RANDARRAY(3,2)`, a SORT/FILTER
|
|
20231
|
+
// result) reports its width; a scalar is a 1×1 array (`COLUMNS(5)` = 1).
|
|
20232
|
+
if (Array.isArray(obj)) return obj[0]?.length ?? 0;
|
|
20233
|
+
if (typeof obj != "object") return 1;
|
|
19870
20234
|
if (H$2.isCellRef(obj)) {
|
|
19871
20235
|
return 1;
|
|
19872
20236
|
} else if (H$2.isRangeRef(obj)) {
|
|
@@ -19917,6 +20281,7 @@ const ReferenceFunctions = {
|
|
|
19917
20281
|
|
|
19918
20282
|
// Special
|
|
19919
20283
|
INDEX: (context, ranges, rowNum, colNum, areaNum) => {
|
|
20284
|
+
if (ranges === undefined) throw FormulaError.ARG_MISSING([Types.ARRAY]);
|
|
19920
20285
|
// retrieve values
|
|
19921
20286
|
const isColNumMissing = colNum == null;
|
|
19922
20287
|
const isRowNumMissing = rowNum == null;
|
|
@@ -20299,6 +20664,8 @@ const ReferenceFunctions = {
|
|
|
20299
20664
|
// Retrieve actual data to determine effective row count
|
|
20300
20665
|
const { val: data } = context.utils.extractRefValue(obj, context.position);
|
|
20301
20666
|
const rowCount = Array.isArray(data) ? data.length : 1;
|
|
20667
|
+
// One row (`ROW(3:3)`, `ROW(B2:C2)`) is a plain number; see COLUMN.
|
|
20668
|
+
if (rowCount <= 1) return fromRow;
|
|
20302
20669
|
const result = [];
|
|
20303
20670
|
for (let i = 0; i < rowCount; i++) {
|
|
20304
20671
|
result.push([fromRow + i]); // Vertical array (column vector)
|
|
@@ -20315,7 +20682,11 @@ const ReferenceFunctions = {
|
|
|
20315
20682
|
if (obj == null) {
|
|
20316
20683
|
throw Error("ROWS requires one argument");
|
|
20317
20684
|
}
|
|
20318
|
-
if (
|
|
20685
|
+
if (obj instanceof FormulaError) throw obj;
|
|
20686
|
+
// An array (`{1,2;3,4}`, `SEQUENCE(3,2)`, `RANDARRAY(3,2)`, a SORT/FILTER
|
|
20687
|
+
// result) reports its height; a scalar is a 1×1 array (`ROWS(5)` = 1).
|
|
20688
|
+
if (Array.isArray(obj)) return obj.length;
|
|
20689
|
+
if (typeof obj != "object") return 1;
|
|
20319
20690
|
if (H$2.isCellRef(obj)) {
|
|
20320
20691
|
return 1;
|
|
20321
20692
|
} else if (H$2.isRangeRef(obj)) {
|
|
@@ -21117,6 +21488,44 @@ const ReferenceFunctions = {
|
|
|
21117
21488
|
);
|
|
21118
21489
|
},
|
|
21119
21490
|
|
|
21491
|
+
/**
|
|
21492
|
+
* TRIMRANGE(range, [trim_rows], [trim_cols]) — drops the blank rows /
|
|
21493
|
+
* columns at the edges of a range or array: 0 keeps every row / column,
|
|
21494
|
+
* 1 trims leading blanks, 2 trailing blanks, 3 (default) both. Only truly
|
|
21495
|
+
* empty cells are blank (a "" text result or a 0 keeps its row); trimming
|
|
21496
|
+
* everything away is `#REF!`, as in Excel. The function form of Excel's
|
|
21497
|
+
* `A1.:.E10` / `A1:.E10` / `A1.:E10` trim-ref operators.
|
|
21498
|
+
*/
|
|
21499
|
+
TRIMRANGE: (range, trim_rows, trim_cols) => {
|
|
21500
|
+
if (H$2.isOmitted(range)) throw FormulaError.ARG_MISSING([Types.ARRAY]);
|
|
21501
|
+
const mode = (arg) => {
|
|
21502
|
+
if (H$2.isOmitted(arg)) return 3;
|
|
21503
|
+
const n = Math.trunc(H$2.accept(arg, Types.NUMBER, 3));
|
|
21504
|
+
if (n < 0 || n > 3) throw FormulaError.VALUE;
|
|
21505
|
+
return n;
|
|
21506
|
+
};
|
|
21507
|
+
const trimRows = mode(trim_rows);
|
|
21508
|
+
const trimCols = mode(trim_cols);
|
|
21509
|
+
const m = toMatrix(range);
|
|
21510
|
+
const height = m.length;
|
|
21511
|
+
const width = m[0]?.length ?? 0;
|
|
21512
|
+
const isBlank = (v) => v == null;
|
|
21513
|
+
const blankRow = (r) => m[r].every(isBlank);
|
|
21514
|
+
const blankCol = (c) => m.every((row) => isBlank(row[c]));
|
|
21515
|
+
let rowStart = 0;
|
|
21516
|
+
let rowEnd = height;
|
|
21517
|
+
if (trimRows & 1) while (rowStart < rowEnd && blankRow(rowStart)) rowStart++;
|
|
21518
|
+
if (trimRows & 2) while (rowEnd > rowStart && blankRow(rowEnd - 1)) rowEnd--;
|
|
21519
|
+
let colStart = 0;
|
|
21520
|
+
let colEnd = width;
|
|
21521
|
+
if (trimCols & 1) while (colStart < colEnd && blankCol(colStart)) colStart++;
|
|
21522
|
+
if (trimCols & 2) while (colEnd > colStart && blankCol(colEnd - 1)) colEnd--;
|
|
21523
|
+
if (rowStart >= rowEnd || colStart >= colEnd) throw FormulaError.REF;
|
|
21524
|
+
return m
|
|
21525
|
+
.slice(rowStart, rowEnd)
|
|
21526
|
+
.map((row) => row.slice(colStart, colEnd));
|
|
21527
|
+
},
|
|
21528
|
+
|
|
21120
21529
|
/**
|
|
21121
21530
|
* EXPAND(array, rows, [columns], [pad_with]) — grows an array to
|
|
21122
21531
|
* `rows`×`columns`, filling new cells with `pad_with` (default `#N/A`).
|
|
@@ -21223,21 +21632,33 @@ const ReferenceFunctions = {
|
|
|
21223
21632
|
return result;
|
|
21224
21633
|
},
|
|
21225
21634
|
|
|
21635
|
+
/**
|
|
21636
|
+
* SORTN(range, [n], [display_ties_mode], [sort_column], [is_ascending]) —
|
|
21637
|
+
* Google Sheets. SORT on one key column followed by the first n rows;
|
|
21638
|
+
* display_ties_mode is accepted and ignored. A negative n is #VALUE!. The
|
|
21639
|
+
* rnc engine mirrors these rules.
|
|
21640
|
+
*/
|
|
21226
21641
|
SORTN: (context, range, n, displayTies, sortColumn, isAscending) => {
|
|
21227
|
-
range = H$2.accept(range, Types.ARRAY);
|
|
21642
|
+
range = H$2.accept(range, Types.ARRAY, undefined, false, true);
|
|
21228
21643
|
n = H$2.accept(n, Types.NUMBER, range.length);
|
|
21229
21644
|
displayTies = H$2.accept(displayTies, Types.NUMBER, 0);
|
|
21230
21645
|
sortColumn = H$2.accept(sortColumn, Types.NUMBER, 1);
|
|
21231
21646
|
isAscending = H$2.accept(isAscending, Types.BOOLEAN, true);
|
|
21232
|
-
|
|
21233
|
-
|
|
21234
|
-
|
|
21235
|
-
|
|
21236
|
-
|
|
21237
|
-
|
|
21238
|
-
|
|
21239
|
-
|
|
21240
|
-
|
|
21647
|
+
if (n < 0) throw FormulaError.VALUE;
|
|
21648
|
+
if (!Array.isArray(range) || range.length === 0) return range;
|
|
21649
|
+
// Same stable row sort as SORT's row path (SORT itself re-parses its
|
|
21650
|
+
// arguments as parser params, so it cannot be called with the already
|
|
21651
|
+
// accepted values — doing so made every SORTN call #VALUE!).
|
|
21652
|
+
const width = range[0]?.length ?? 0;
|
|
21653
|
+
if (sortColumn < 1 || sortColumn > width) throw FormulaError.VALUE;
|
|
21654
|
+
const key = sortColumn - 1;
|
|
21655
|
+
const compareValues = makeComparator(context?.locale);
|
|
21656
|
+
const indexed = range.map((row, idx) => ({ row, idx }));
|
|
21657
|
+
indexed.sort((a, b) => {
|
|
21658
|
+
const comparison = compareValues(a.row[key], b.row[key]);
|
|
21659
|
+
return (isAscending ? comparison : -comparison) || a.idx - b.idx;
|
|
21660
|
+
});
|
|
21661
|
+
return indexed.slice(0, n).map((entry) => entry.row);
|
|
21241
21662
|
},
|
|
21242
21663
|
|
|
21243
21664
|
/**
|
|
@@ -25350,6 +25771,10 @@ const FinancialFunctions = {
|
|
|
25350
25771
|
// identifiers. Importers keep the cached result Excel wrote and exporters write
|
|
25351
25772
|
// the formula back unchanged, so a workbook round-trips intact; only a
|
|
25352
25773
|
// recalculation on this engine surfaces the #N/A.
|
|
25774
|
+
//
|
|
25775
|
+
// QUERY is the Google Sheets counterpart: it needs a query-language interpreter
|
|
25776
|
+
// neither engine has, and degrades the same way (the rnc engine registers the
|
|
25777
|
+
// same stub) so a Sheets-authored workbook evaluates identically on both.
|
|
25353
25778
|
const unavailable = () => {
|
|
25354
25779
|
throw FormulaError.NA;
|
|
25355
25780
|
};
|
|
@@ -25369,6 +25794,7 @@ const ExternalFunctions = {
|
|
|
25369
25794
|
EUROCONVERT: unavailable,
|
|
25370
25795
|
TRANSLATE: unavailable,
|
|
25371
25796
|
DETECTLANGUAGE: unavailable,
|
|
25797
|
+
QUERY: unavailable,
|
|
25372
25798
|
};
|
|
25373
25799
|
|
|
25374
25800
|
var api$3 = {};
|
|
@@ -44517,7 +44943,10 @@ class Parser extends EmbeddedActionsParser {
|
|
|
44517
44943
|
) {
|
|
44518
44944
|
return arr[0][0];
|
|
44519
44945
|
}
|
|
44520
|
-
|
|
44946
|
+
const array = this.utils.toArray(arr);
|
|
44947
|
+
return context.async
|
|
44948
|
+
? this.utils.settleArrayItems(array, $.position)
|
|
44949
|
+
: array;
|
|
44521
44950
|
});
|
|
44522
44951
|
});
|
|
44523
44952
|
|
|
@@ -44538,9 +44967,13 @@ class Parser extends EmbeddedActionsParser {
|
|
|
44538
44967
|
const prefix = $.OPTION(() => $.SUBRULE($.plusMinusOp));
|
|
44539
44968
|
const image = $.CONSUME(Number$1).image;
|
|
44540
44969
|
const number = $.ACTION(() => this.utils.toNumber(image));
|
|
44970
|
+
// A signed literal is a plain number: apply the sign directly
|
|
44971
|
+
// rather than through `applyPrefix`, whose async branch would
|
|
44972
|
+
// hand the array a pending Promise (`{-100,60,60}` summed to 120
|
|
44973
|
+
// under `parseAsync`).
|
|
44541
44974
|
if (prefix)
|
|
44542
44975
|
return $.ACTION(() =>
|
|
44543
|
-
this.utils.
|
|
44976
|
+
this.utils.applyLiteralPrefix(prefix, number)
|
|
44544
44977
|
);
|
|
44545
44978
|
return number;
|
|
44546
44979
|
},
|
|
@@ -45177,18 +45610,21 @@ const expandLet = (source) => {
|
|
|
45177
45610
|
|
|
45178
45611
|
|
|
45179
45612
|
const LAMBDA_INTERNAL = "__RNC_LAMBDA__";
|
|
45180
|
-
/** Source spliced in for
|
|
45613
|
+
/** Source spliced in for a parameter the caller left out. */
|
|
45181
45614
|
const OMITTED_INTERNAL = "__RNC_OMITTED__";
|
|
45182
45615
|
const OMITTED_CALL = `${OMITTED_INTERNAL}()`;
|
|
45183
45616
|
/**
|
|
45184
45617
|
* Runtime call of a lambda value: `__RNC_CALL__(lambda, arg1, …)`. Used
|
|
45185
|
-
* when `LAMBDA(...)(...)` cannot be inlined (
|
|
45186
|
-
* evaluator reports `#VALUE!` like Excel instead of a
|
|
45618
|
+
* when `LAMBDA(...)(...)` cannot be inlined (more arguments than
|
|
45619
|
+
* parameters) so the evaluator reports `#VALUE!` like Excel instead of a
|
|
45620
|
+
* parse error.
|
|
45187
45621
|
*/
|
|
45188
45622
|
const CALL_INTERNAL = "__RNC_CALL__";
|
|
45189
45623
|
|
|
45190
|
-
// `[name]` — Excel's optional-parameter
|
|
45624
|
+
// `[name]` — Excel's optional-parameter spelling. The lexer sees it as a
|
|
45191
45625
|
// table-column token; only a bare identifier inside the brackets counts.
|
|
45626
|
+
// The brackets are documentation only: Excel lets a call leave out any
|
|
45627
|
+
// trailing parameter, bracketed or not (ISOMITTED reports it either way).
|
|
45192
45628
|
const OPTIONAL_PARAM = /^\[\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\]$/;
|
|
45193
45629
|
|
|
45194
45630
|
/**
|
|
@@ -45206,8 +45642,6 @@ const paramFromSpan = (tokens, span) => {
|
|
|
45206
45642
|
return null;
|
|
45207
45643
|
};
|
|
45208
45644
|
|
|
45209
|
-
const requiredCount = (params) => params.filter((p) => !p.optional).length;
|
|
45210
|
-
|
|
45211
45645
|
/** Excel writes optional params back as `[name]`; keep that in the value form. */
|
|
45212
45646
|
const paramsToSource = (params) =>
|
|
45213
45647
|
params.map((p) => (p.optional ? `[${p.name}]` : p.name)).join(",");
|
|
@@ -45217,7 +45651,9 @@ const paramsToSource = (params) =>
|
|
|
45217
45651
|
* IIFE inlining: when `LAMBDA(...)(...)` appears, substitute the
|
|
45218
45652
|
* params with the call's arg sources and return the substituted body.
|
|
45219
45653
|
* Returns null if the structure doesn't match (e.g. LAMBDA without
|
|
45220
|
-
* immediate invocation
|
|
45654
|
+
* immediate invocation). A call with more arguments than parameters is
|
|
45655
|
+
* handed to the runtime, which reports `#VALUE!`; a call with fewer binds
|
|
45656
|
+
* the missing parameters to the omitted marker.
|
|
45221
45657
|
*/
|
|
45222
45658
|
const inlineIIFE = (source, tokens, lambdaOpenIdx, lambdaCloseIdx) => {
|
|
45223
45659
|
// Next token after the lambda's close paren must be a bare OpenParen
|
|
@@ -45245,13 +45681,10 @@ const inlineIIFE = (source, tokens, lambdaOpenIdx, lambdaCloseIdx) => {
|
|
|
45245
45681
|
callCloseIdx === callOpenIdx + 1
|
|
45246
45682
|
? []
|
|
45247
45683
|
: splitArgs(tokens, callOpenIdx + 1, callCloseIdx);
|
|
45248
|
-
if (
|
|
45249
|
-
|
|
45250
|
-
|
|
45251
|
-
|
|
45252
|
-
// Arity mismatch — hand the call to the runtime, which applies the
|
|
45253
|
-
// lambda value and reports #VALUE! (Excel's answer) instead of the
|
|
45254
|
-
// parser choking on `(...)(...)`.
|
|
45684
|
+
if (callArgSpans.length > params.length) {
|
|
45685
|
+
// Too many arguments — hand the call to the runtime, which applies
|
|
45686
|
+
// the lambda value and reports #VALUE! (Excel's answer) instead of
|
|
45687
|
+
// the parser choking on `(...)(...)`.
|
|
45255
45688
|
const lambdaValue = transformLambdaToValue(
|
|
45256
45689
|
source,
|
|
45257
45690
|
tokens,
|
|
@@ -45269,9 +45702,9 @@ const inlineIIFE = (source, tokens, lambdaOpenIdx, lambdaCloseIdx) => {
|
|
|
45269
45702
|
};
|
|
45270
45703
|
}
|
|
45271
45704
|
const bindings = params.map(({ name }, i) => {
|
|
45272
|
-
// Raw value — substituteNamesInBody decides the wrap.
|
|
45273
|
-
//
|
|
45274
|
-
//
|
|
45705
|
+
// Raw value — substituteNamesInBody decides the wrap. A parameter the
|
|
45706
|
+
// call left out binds to the omitted marker so ISOMITTED can see it
|
|
45707
|
+
// (and any other use of it is #VALUE!).
|
|
45275
45708
|
const value =
|
|
45276
45709
|
i < callArgSpans.length
|
|
45277
45710
|
? sourceSlice(source, tokens, callArgSpans[i]).trim()
|
|
@@ -45396,14 +45829,15 @@ const isLambdaValue = (v) =>
|
|
|
45396
45829
|
v != null && typeof v === "object" && v.__isLambda === true;
|
|
45397
45830
|
|
|
45398
45831
|
/**
|
|
45399
|
-
* Marker bound to
|
|
45832
|
+
* Marker bound to a parameter the caller left out (Excel allows a call
|
|
45833
|
+
* with fewer arguments than parameters, `[param]` or not). Only
|
|
45400
45834
|
* `ISOMITTED` looks at it; anywhere else it is an invalid operand.
|
|
45401
45835
|
*/
|
|
45402
45836
|
const OMITTED = Object.freeze({ __isOmitted: true });
|
|
45403
45837
|
const isOmittedValue = (v) =>
|
|
45404
45838
|
v != null && typeof v === "object" && v.__isOmitted === true;
|
|
45405
45839
|
|
|
45406
|
-
/** `[name]` → optional
|
|
45840
|
+
/** `[name]` → parameter documented as optional, `name` → plain parameter. */
|
|
45407
45841
|
const parseLambdaParams = (paramsStr) =>
|
|
45408
45842
|
paramsStr
|
|
45409
45843
|
.split(",")
|
|
@@ -45414,9 +45848,43 @@ const parseLambdaParams = (paramsStr) =>
|
|
|
45414
45848
|
return { name: optional ? raw.slice(1, -1).trim() : raw, optional };
|
|
45415
45849
|
});
|
|
45416
45850
|
|
|
45851
|
+
/**
|
|
45852
|
+
* An omitted marker that escapes `ISOMITTED` — a body that returns the
|
|
45853
|
+
* omitted parameter itself, or spreads it into an array — is `#VALUE!`
|
|
45854
|
+
* in the result, per cell inside an array. Arrays are only copied when
|
|
45855
|
+
* one of their cells actually holds the marker.
|
|
45856
|
+
*/
|
|
45857
|
+
const rejectOmittedResult = (result) => {
|
|
45858
|
+
if (isOmittedValue(result)) return FormulaError.VALUE;
|
|
45859
|
+
if (!Array.isArray(result)) return result;
|
|
45860
|
+
let out = null;
|
|
45861
|
+
for (let i = 0; i < result.length; i++) {
|
|
45862
|
+
const cell = rejectOmittedResult(result[i]);
|
|
45863
|
+
if (cell !== result[i]) {
|
|
45864
|
+
if (out === null) out = result.slice();
|
|
45865
|
+
out[i] = cell;
|
|
45866
|
+
}
|
|
45867
|
+
}
|
|
45868
|
+
return out ?? result;
|
|
45869
|
+
};
|
|
45870
|
+
|
|
45417
45871
|
const requiredParamCount = (lambda) =>
|
|
45418
45872
|
lambda.params.filter((_, i) => !(lambda.optional?.[i] ?? false)).length;
|
|
45419
45873
|
|
|
45874
|
+
/**
|
|
45875
|
+
* Excel's "Incorrect Parameters" `#VALUE!` for MAP / REDUCE / SCAN / BYROW /
|
|
45876
|
+
* BYCOL / MAKEARRAY / GROUPBY / PIVOTBY: the helper drives the lambda with
|
|
45877
|
+
* a fixed number of arguments and the lambda must declare a parameter for
|
|
45878
|
+
* each of them. Only a `[name]` parameter may stay unsupplied — the
|
|
45879
|
+
* "fewer arguments than parameters" allowance is for direct calls, not
|
|
45880
|
+
* helpers. Returns the error for the whole helper call, or null when the
|
|
45881
|
+
* counts fit.
|
|
45882
|
+
*/
|
|
45883
|
+
const hofArityError = (lambda, supplied) =>
|
|
45884
|
+
supplied < requiredParamCount(lambda) || supplied > lambda.params.length
|
|
45885
|
+
? FormulaError.VALUE
|
|
45886
|
+
: null;
|
|
45887
|
+
|
|
45420
45888
|
const formatLiteral = (value) => {
|
|
45421
45889
|
if (value === null || value === undefined) return "0";
|
|
45422
45890
|
if (value instanceof FormulaError) {
|
|
@@ -45459,12 +45927,11 @@ const formatLiteral = (value) => {
|
|
|
45459
45927
|
*/
|
|
45460
45928
|
const applyLambda = async (context, lambda, args) => {
|
|
45461
45929
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
45462
|
-
|
|
45463
|
-
|
|
45464
|
-
|
|
45465
|
-
|
|
45466
|
-
|
|
45467
|
-
}
|
|
45930
|
+
// Excel rejects surplus arguments but accepts fewer than declared: the
|
|
45931
|
+
// missing parameters are omitted (ISOMITTED → TRUE, any other use →
|
|
45932
|
+
// #VALUE!). Helpers check their own parameter count with
|
|
45933
|
+
// `hofArityError` before getting here.
|
|
45934
|
+
if (args.length > lambda.params.length) return FormulaError.VALUE;
|
|
45468
45935
|
|
|
45469
45936
|
context.__lambdaDepth = (context.__lambdaDepth ?? 0) + 1;
|
|
45470
45937
|
if (context.__lambdaDepth > MAX_LAMBDA_DEPTH) {
|
|
@@ -45477,14 +45944,14 @@ const applyLambda = async (context, lambda, args) => {
|
|
|
45477
45944
|
try {
|
|
45478
45945
|
const bindings = lambda.params.map((name, i) => ({
|
|
45479
45946
|
name,
|
|
45480
|
-
//
|
|
45947
|
+
// A parameter the call left out binds to the omitted marker so
|
|
45481
45948
|
// ISOMITTED inside the body can report it.
|
|
45482
45949
|
value:
|
|
45483
45950
|
i < args.length ? `(${formatLiteral(args[i])})` : `(${OMITTED_CALL})`,
|
|
45484
45951
|
}));
|
|
45485
45952
|
const substituted = substituteNamesInBody(lambda.body, bindings);
|
|
45486
45953
|
const result = await context.parseAsync(substituted, context.position, true);
|
|
45487
|
-
return result;
|
|
45954
|
+
return rejectOmittedResult(result);
|
|
45488
45955
|
} catch (e) {
|
|
45489
45956
|
if (e instanceof FormulaError) return e;
|
|
45490
45957
|
throw e;
|
|
@@ -45545,35 +46012,58 @@ const LambdaFunctions = {
|
|
|
45545
46012
|
},
|
|
45546
46013
|
|
|
45547
46014
|
/**
|
|
45548
|
-
* Internal: the value spliced in for
|
|
45549
|
-
*
|
|
46015
|
+
* Internal: the value spliced in for a parameter the caller left out
|
|
46016
|
+
* (see the LAMBDA preprocessor / applyLambda).
|
|
45550
46017
|
*/
|
|
45551
46018
|
__RNC_OMITTED__: () => OMITTED,
|
|
45552
46019
|
|
|
45553
46020
|
/**
|
|
45554
46021
|
* Internal: `__RNC_CALL__(lambda, arg1, …)` — the preprocessor emits
|
|
45555
|
-
* this for an inline `LAMBDA(...)(...)`
|
|
45556
|
-
*
|
|
46022
|
+
* this for an inline `LAMBDA(...)(...)` with more arguments than
|
|
46023
|
+
* parameters, so the arity error surfaces as #VALUE!.
|
|
45557
46024
|
*/
|
|
45558
46025
|
__RNC_CALL__: async (context, lambdaArg, ...args) =>
|
|
45559
46026
|
applyLambda(context, unwrap(lambdaArg), args.map(unwrap)),
|
|
45560
46027
|
|
|
45561
46028
|
/**
|
|
45562
|
-
* ISOMITTED(argument) — TRUE when `argument` is
|
|
45563
|
-
*
|
|
46029
|
+
* ISOMITTED(argument) — TRUE when `argument` is a LAMBDA parameter the
|
|
46030
|
+
* caller did not supply (bracketed `[p]` or not), FALSE otherwise.
|
|
45564
46031
|
*/
|
|
45565
46032
|
ISOMITTED: (arg) => isOmittedValue(unwrap(arg)),
|
|
45566
46033
|
|
|
46034
|
+
/**
|
|
46035
|
+
* A well-formed `LET(name, value, ..., body)` never reaches the function
|
|
46036
|
+
* table — the LET preprocessor inlines it. One that does is malformed:
|
|
46037
|
+
* too few arguments is `#N/A` (the too-few-arguments contract shared with
|
|
46038
|
+
* the Rust engine), an even count or a non-name binding is `#VALUE!`.
|
|
46039
|
+
*/
|
|
46040
|
+
LET: (...args) => {
|
|
46041
|
+
if (args.length < 3) return FormulaError.NA;
|
|
46042
|
+
return FormulaError.VALUE;
|
|
46043
|
+
},
|
|
46044
|
+
|
|
46045
|
+
/**
|
|
46046
|
+
* Same for `LAMBDA(...)`: the preprocessor rewrites every LAMBDA with a
|
|
46047
|
+
* body, so only `LAMBDA()` (too few arguments, `#N/A`) and a body-less
|
|
46048
|
+
* `LAMBDA(x)` (a lambda value as the cell result, `#CALC!`) get here.
|
|
46049
|
+
*/
|
|
46050
|
+
LAMBDA: (...args) => {
|
|
46051
|
+
if (args.length === 0) return FormulaError.NA;
|
|
46052
|
+
return FormulaError.CALC;
|
|
46053
|
+
},
|
|
46054
|
+
|
|
45567
46055
|
/**
|
|
45568
46056
|
* MAP(array1, [array2, ...], lambda) — applies lambda to each set
|
|
45569
46057
|
* of corresponding elements. Returns a same-shape 2D array.
|
|
45570
46058
|
*/
|
|
45571
46059
|
MAP: async (context, ...args) => {
|
|
45572
|
-
if (args.length < 2) return FormulaError.
|
|
46060
|
+
if (args.length < 2) return FormulaError.NA;
|
|
45573
46061
|
const lambda = unwrap(args[args.length - 1]);
|
|
45574
46062
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
45575
46063
|
const arrays = args.slice(0, -1).map((a) => as2D(unwrap(a)));
|
|
45576
46064
|
if (arrays.length === 0) return FormulaError.VALUE;
|
|
46065
|
+
const arity = hofArityError(lambda, arrays.length);
|
|
46066
|
+
if (arity) return arity;
|
|
45577
46067
|
const rows = arrays[0].length;
|
|
45578
46068
|
const cols = arrays[0][0]?.length ?? 0;
|
|
45579
46069
|
const out = [];
|
|
@@ -45595,6 +46085,8 @@ const LambdaFunctions = {
|
|
|
45595
46085
|
REDUCE: async (context, initialArg, arrayArg, lambdaArg) => {
|
|
45596
46086
|
const lambda = unwrap(lambdaArg);
|
|
45597
46087
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
46088
|
+
const arity = hofArityError(lambda, 2);
|
|
46089
|
+
if (arity) return arity;
|
|
45598
46090
|
let acc = unwrap(initialArg);
|
|
45599
46091
|
const values = flatten2D(as2D(unwrap(arrayArg)));
|
|
45600
46092
|
for (const v of values) {
|
|
@@ -45612,6 +46104,8 @@ const LambdaFunctions = {
|
|
|
45612
46104
|
SCAN: async (context, initialArg, arrayArg, lambdaArg) => {
|
|
45613
46105
|
const lambda = unwrap(lambdaArg);
|
|
45614
46106
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
46107
|
+
const arity = hofArityError(lambda, 2);
|
|
46108
|
+
if (arity) return arity;
|
|
45615
46109
|
let acc = unwrap(initialArg);
|
|
45616
46110
|
const arr = as2D(unwrap(arrayArg));
|
|
45617
46111
|
const out = [];
|
|
@@ -45633,8 +46127,11 @@ const LambdaFunctions = {
|
|
|
45633
46127
|
* array), returns a column vector of results.
|
|
45634
46128
|
*/
|
|
45635
46129
|
BYROW: async (context, arrayArg, lambdaArg) => {
|
|
46130
|
+
if (lambdaArg === undefined) return FormulaError.NA;
|
|
45636
46131
|
const lambda = unwrap(lambdaArg);
|
|
45637
46132
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
46133
|
+
const arity = hofArityError(lambda, 1);
|
|
46134
|
+
if (arity) return arity;
|
|
45638
46135
|
const arr = as2D(unwrap(arrayArg));
|
|
45639
46136
|
const out = [];
|
|
45640
46137
|
for (const row of arr) {
|
|
@@ -45650,8 +46147,11 @@ const LambdaFunctions = {
|
|
|
45650
46147
|
* 1D array), returns a row vector of results.
|
|
45651
46148
|
*/
|
|
45652
46149
|
BYCOL: async (context, arrayArg, lambdaArg) => {
|
|
46150
|
+
if (lambdaArg === undefined) return FormulaError.NA;
|
|
45653
46151
|
const lambda = unwrap(lambdaArg);
|
|
45654
46152
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
46153
|
+
const arity = hofArityError(lambda, 1);
|
|
46154
|
+
if (arity) return arity;
|
|
45655
46155
|
const arr = as2D(unwrap(arrayArg));
|
|
45656
46156
|
const cols = arr[0]?.length ?? 0;
|
|
45657
46157
|
const out = [];
|
|
@@ -45669,8 +46169,11 @@ const LambdaFunctions = {
|
|
|
45669
46169
|
* lambda(r, c) for each cell. r, c are 1-indexed per Excel.
|
|
45670
46170
|
*/
|
|
45671
46171
|
MAKEARRAY: async (context, rowsArg, colsArg, lambdaArg) => {
|
|
46172
|
+
if (lambdaArg === undefined) return FormulaError.NA;
|
|
45672
46173
|
const lambda = unwrap(lambdaArg);
|
|
45673
46174
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
46175
|
+
const arity = hofArityError(lambda, 2);
|
|
46176
|
+
if (arity) return arity;
|
|
45674
46177
|
const rows = Math.floor(
|
|
45675
46178
|
FormulaHelpers.accept(rowsArg, Types.NUMBER, 0),
|
|
45676
46179
|
);
|
|
@@ -45719,6 +46222,8 @@ const LambdaFunctions = {
|
|
|
45719
46222
|
const valueFields = as2D(unwrap(args[1]));
|
|
45720
46223
|
const lambda = unwrap(args[2]);
|
|
45721
46224
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
46225
|
+
const arity = hofArityError(lambda, 1);
|
|
46226
|
+
if (arity) return arity;
|
|
45722
46227
|
const fieldHeaders = args[3] != null
|
|
45723
46228
|
? Math.floor(FormulaHelpers.accept(args[3], Types.NUMBER, 0))
|
|
45724
46229
|
: 0;
|
|
@@ -45767,6 +46272,8 @@ const LambdaFunctions = {
|
|
|
45767
46272
|
const valueFields = as2D(unwrap(args[2]));
|
|
45768
46273
|
const lambda = unwrap(args[3]);
|
|
45769
46274
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
46275
|
+
const arity = hofArityError(lambda, 1);
|
|
46276
|
+
if (arity) return arity;
|
|
45770
46277
|
|
|
45771
46278
|
const numOpt = (i, d) =>
|
|
45772
46279
|
args[i] != null
|
|
@@ -46223,6 +46730,42 @@ let Utils$1 = class Utils {
|
|
|
46223
46730
|
return Prefix.unaryOp(prefixes, val, isArray);
|
|
46224
46731
|
}
|
|
46225
46732
|
|
|
46733
|
+
/**
|
|
46734
|
+
* Sign a numeric literal (`-1`, `+2` inside an array constant). Nothing
|
|
46735
|
+
* to dereference or await here, so this stays synchronous in both parse
|
|
46736
|
+
* modes.
|
|
46737
|
+
* @param {string} prefix - `+` or `-`
|
|
46738
|
+
* @param {number} number
|
|
46739
|
+
* @return {number}
|
|
46740
|
+
*/
|
|
46741
|
+
applyLiteralPrefix(prefix, number) {
|
|
46742
|
+
return this._applyPrefix([prefix], number, false);
|
|
46743
|
+
}
|
|
46744
|
+
|
|
46745
|
+
/**
|
|
46746
|
+
* Under `parseAsync` an array-constant item built from a sub-expression
|
|
46747
|
+
* (`{SUM(1,-2),3}`) is a pending Promise; an array must only ever carry
|
|
46748
|
+
* values, so settle them. A settled item may itself be a reference
|
|
46749
|
+
* (`{INDEX(A1:C5,1,1),2}`), which the sync arm of `constantArray`
|
|
46750
|
+
* dereferences before the Promise ever existed — do the same here so both
|
|
46751
|
+
* parse modes agree. Returns the array itself when nothing is pending.
|
|
46752
|
+
* @param {Array.<Array>} array
|
|
46753
|
+
* @param {{row: number, col: number, sheet: string}} position
|
|
46754
|
+
* @return {Array.<Array>|Promise.<Array.<Array>>}
|
|
46755
|
+
*/
|
|
46756
|
+
settleArrayItems(array, position) {
|
|
46757
|
+
const isPending = (item) =>
|
|
46758
|
+
item != null && typeof item.then === "function";
|
|
46759
|
+
if (!array.some((row) => row.some(isPending))) return array;
|
|
46760
|
+
const deref = (item) =>
|
|
46761
|
+
item && item.ref ? this.extractRefValue(item, position).val : item;
|
|
46762
|
+
return Promise.all(
|
|
46763
|
+
array.map((row) =>
|
|
46764
|
+
Promise.all(row).then((items) => items.map(deref))
|
|
46765
|
+
)
|
|
46766
|
+
);
|
|
46767
|
+
}
|
|
46768
|
+
|
|
46226
46769
|
async applyPrefixAsync(prefixes, value, position) {
|
|
46227
46770
|
const { val, isArray } = this.extractRefValue(await value, position);
|
|
46228
46771
|
return this._applyPrefix(prefixes, val, isArray);
|
|
@@ -46417,15 +46960,21 @@ let Utils$1 = class Utils {
|
|
|
46417
46960
|
}
|
|
46418
46961
|
}
|
|
46419
46962
|
|
|
46420
|
-
applyIntersect(refs) {
|
|
46963
|
+
applyIntersect(refs, position) {
|
|
46421
46964
|
// console.log('applyIntersect', refs);
|
|
46422
46965
|
if (this.isFormulaError(refs[0])) return refs[0];
|
|
46423
46966
|
if (!refs[0].ref) throw Error(`Expecting a reference, but got ${refs[0]}.`);
|
|
46424
46967
|
// a intersection will keep track of references, value won't be retrieved here.
|
|
46425
46968
|
let maxRow, maxCol, minRow, minCol, sheet, res; // index start from 1
|
|
46969
|
+
// An unqualified operand (`B:B`, sheet undefined) lives on the formula's sheet: it must
|
|
46970
|
+
// intersect an operand that names that sheet explicitly (`Sheet1!A1:C3`, or a defined name
|
|
46971
|
+
// — the host resolves names WITH their sheet). Only a genuinely different sheet is #NULL!.
|
|
46972
|
+
const currentSheet = (position ?? this.context.position)?.sheet;
|
|
46973
|
+
const sheetOf = (ref) => ref.sheet ?? currentSheet;
|
|
46426
46974
|
// first time setup
|
|
46427
46975
|
const ref = refs.shift().ref;
|
|
46428
46976
|
sheet = ref.sheet;
|
|
46977
|
+
const sheetKey = sheetOf(ref);
|
|
46429
46978
|
if (!ref.from) {
|
|
46430
46979
|
// check whole row/col reference
|
|
46431
46980
|
if (ref.row === undefined || ref.col === undefined) {
|
|
@@ -46459,10 +47008,11 @@ let Utils$1 = class Utils {
|
|
|
46459
47008
|
ref.row < minRow ||
|
|
46460
47009
|
ref.col > maxCol ||
|
|
46461
47010
|
ref.col < minCol ||
|
|
46462
|
-
|
|
47011
|
+
sheetKey !== sheetOf(ref)
|
|
46463
47012
|
) {
|
|
46464
47013
|
err = FormulaError.NULL;
|
|
46465
47014
|
}
|
|
47015
|
+
sheet ??= ref.sheet;
|
|
46466
47016
|
maxRow = minRow = ref.row;
|
|
46467
47017
|
maxCol = minCol = ref.col;
|
|
46468
47018
|
} else {
|
|
@@ -46476,10 +47026,11 @@ let Utils$1 = class Utils {
|
|
|
46476
47026
|
refMaxRow < minRow ||
|
|
46477
47027
|
refMinCol > maxCol ||
|
|
46478
47028
|
refMaxCol < minCol ||
|
|
46479
|
-
|
|
47029
|
+
sheetKey !== sheetOf(ref)
|
|
46480
47030
|
) {
|
|
46481
47031
|
err = FormulaError.NULL;
|
|
46482
47032
|
}
|
|
47033
|
+
sheet ??= ref.sheet;
|
|
46483
47034
|
// update
|
|
46484
47035
|
maxRow = Math.min(maxRow, refMaxRow);
|
|
46485
47036
|
minRow = Math.max(minRow, refMinRow);
|
|
@@ -47480,6 +48031,9 @@ class FormulaParser {
|
|
|
47480
48031
|
if (!this.keepLambdaResult && unwrapLambdaValue(result)) {
|
|
47481
48032
|
return FormulaError.CALC;
|
|
47482
48033
|
}
|
|
48034
|
+
// An inlined `LAMBDA(x,y,y)(1)` evaluates straight to the omitted
|
|
48035
|
+
// marker; only ISOMITTED may consume it, so as a result it is #VALUE!.
|
|
48036
|
+
result = rejectOmittedResult(result);
|
|
47483
48037
|
const type = typeof result;
|
|
47484
48038
|
// number
|
|
47485
48039
|
if (type === "number") {
|
|
@@ -48700,6 +49254,7 @@ exports.WildCard = WildCard;
|
|
|
48700
49254
|
exports.allTokens = allTokens;
|
|
48701
49255
|
exports.default = FormulaParser;
|
|
48702
49256
|
exports.implementedFunctionNames = implementedFunctionNames;
|
|
49257
|
+
exports.isNestedAggregateFormula = isNestedAggregateFormula;
|
|
48703
49258
|
exports.lex = lex;
|
|
48704
49259
|
exports.noop = noop;
|
|
48705
49260
|
exports.numberToText = numberToText;
|