@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/esm/index.js
CHANGED
|
@@ -3339,6 +3339,15 @@ function formatValue (value, parseData, opts) {
|
|
|
3339
3339
|
return part ? runPart(value, part, opts, l10n) : opts.overflow;
|
|
3340
3340
|
}
|
|
3341
3341
|
|
|
3342
|
+
function isDate (partitions) {
|
|
3343
|
+
return !!(
|
|
3344
|
+
(partitions[0] && partitions[0].date) ||
|
|
3345
|
+
(partitions[1] && partitions[1].date) ||
|
|
3346
|
+
(partitions[2] && partitions[2].date) ||
|
|
3347
|
+
(partitions[3] && partitions[3].date)
|
|
3348
|
+
);
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3342
3351
|
const tokenHandlers = [
|
|
3343
3352
|
[ TOKEN_GENERAL, /^General/i, 0 ],
|
|
3344
3353
|
[ TOKEN_HASH, /^#/, 0 ],
|
|
@@ -3776,6 +3785,22 @@ function format (pattern, value, options = {}) {
|
|
|
3776
3785
|
return formatValue(val, data, opts);
|
|
3777
3786
|
}
|
|
3778
3787
|
|
|
3788
|
+
// FIXME: what is a a section?...
|
|
3789
|
+
/**
|
|
3790
|
+
* Determine if a given format pattern is a date pattern.
|
|
3791
|
+
*
|
|
3792
|
+
* The pattern is considered a date pattern if any of its sections contain a
|
|
3793
|
+
* date symbol (such as `Y` or `H`). Each section is restricted to be
|
|
3794
|
+
* _either_ a number or date format.
|
|
3795
|
+
*
|
|
3796
|
+
* @param {string} pattern - A format pattern in the ECMA-376 number format.
|
|
3797
|
+
* @returns {boolean} True if the specified pattern is date pattern, False otherwise.
|
|
3798
|
+
*/
|
|
3799
|
+
function isDateFormat (pattern) {
|
|
3800
|
+
const data = prepareFormatterData(pattern, false);
|
|
3801
|
+
return isDate(data.partitions);
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3779
3804
|
// Excel indexed colors mapping used for legacy color references
|
|
3780
3805
|
const IndexedColors = Object.freeze([
|
|
3781
3806
|
// 0-7
|
|
@@ -3854,6 +3879,80 @@ const IndexedColors = Object.freeze([
|
|
|
3854
3879
|
|
|
3855
3880
|
const isSingleSectionElapsedTimePattern = (pattern) => !pattern.includes(";") &&
|
|
3856
3881
|
/\[(?:h+|m+|s+)\]/i.test(pattern.replace(/\[\$[^\]]+\]/g, ""));
|
|
3882
|
+
const intlMonthNames = (locale, options) => {
|
|
3883
|
+
const formatter = new Intl.DateTimeFormat(locale, {
|
|
3884
|
+
...options,
|
|
3885
|
+
timeZone: "UTC",
|
|
3886
|
+
});
|
|
3887
|
+
return Array.from({ length: 12 }, (_, month) => formatter
|
|
3888
|
+
.formatToParts(new Date(Date.UTC(2020, month, 1)))
|
|
3889
|
+
.find((part) => part.type === "month")?.value ?? "");
|
|
3890
|
+
};
|
|
3891
|
+
const intlRegisteredLocales = new Set();
|
|
3892
|
+
/**
|
|
3893
|
+
* numfmt ships name tables for a fixed set of languages; a tag it does not know
|
|
3894
|
+
* (et, ms, nn, …) silently renders English month/weekday names and en-US
|
|
3895
|
+
* separators. Excel and `Intl.DateTimeFormat` localize every locale, so an
|
|
3896
|
+
* unknown tag gets a numfmt locale built from Intl on first use: separators
|
|
3897
|
+
* from `Intl.NumberFormat`, the in-date month names (numfmt's single table is
|
|
3898
|
+
* the form used beside a day number — see `getStandaloneMonthLocale`), the
|
|
3899
|
+
* in-date weekday names (the form `dateStyle: "full"` shows) and the AM/PM
|
|
3900
|
+
* designators. Tags Intl itself does not know keep numfmt's fallback.
|
|
3901
|
+
*/
|
|
3902
|
+
const ensureNumfmtLocale = (locale) => {
|
|
3903
|
+
if (intlRegisteredLocales.has(locale))
|
|
3904
|
+
return;
|
|
3905
|
+
intlRegisteredLocales.add(locale);
|
|
3906
|
+
try {
|
|
3907
|
+
if (getLocale(locale))
|
|
3908
|
+
return;
|
|
3909
|
+
const canonical = Intl.DateTimeFormat.supportedLocalesOf([locale])[0];
|
|
3910
|
+
if (!canonical)
|
|
3911
|
+
return;
|
|
3912
|
+
const numberParts = new Intl.NumberFormat(locale).formatToParts(1234567.5);
|
|
3913
|
+
const part = (type) => numberParts.find((p) => p.type === type)?.value;
|
|
3914
|
+
const weekdayNames = (weekday) => {
|
|
3915
|
+
// 2024-01-07 was a Sunday; numfmt indexes `dddd[0]` as Sunday.
|
|
3916
|
+
const fmt = new Intl.DateTimeFormat(locale, {
|
|
3917
|
+
weekday,
|
|
3918
|
+
day: "numeric",
|
|
3919
|
+
month: "long",
|
|
3920
|
+
year: "numeric",
|
|
3921
|
+
timeZone: "UTC",
|
|
3922
|
+
});
|
|
3923
|
+
return Array.from({ length: 7 }, (_, i) => fmt
|
|
3924
|
+
.formatToParts(new Date(Date.UTC(2024, 0, 7 + i)))
|
|
3925
|
+
.find((p) => p.type === "weekday")?.value ?? "");
|
|
3926
|
+
};
|
|
3927
|
+
const dayPeriod = (hour) => new Intl.DateTimeFormat(locale, {
|
|
3928
|
+
hour: "numeric",
|
|
3929
|
+
hour12: true,
|
|
3930
|
+
timeZone: "UTC",
|
|
3931
|
+
})
|
|
3932
|
+
.formatToParts(new Date(Date.UTC(2024, 0, 7, hour)))
|
|
3933
|
+
.find((p) => p.type === "dayPeriod")?.value;
|
|
3934
|
+
const dateParts = new Intl.DateTimeFormat(locale, {
|
|
3935
|
+
timeZone: "UTC",
|
|
3936
|
+
}).formatToParts(new Date(Date.UTC(2024, 0, 7)));
|
|
3937
|
+
const monthIndex = dateParts.findIndex((p) => p.type === "month");
|
|
3938
|
+
const dayIndex = dateParts.findIndex((p) => p.type === "day");
|
|
3939
|
+
const am = dayPeriod(9);
|
|
3940
|
+
const pm = dayPeriod(21);
|
|
3941
|
+
addLocale({
|
|
3942
|
+
group: part("group") ?? "\u00a0",
|
|
3943
|
+
decimal: part("decimal") ?? ".",
|
|
3944
|
+
mmmm: intlMonthNames(locale, { day: "numeric", month: "long" }),
|
|
3945
|
+
mmm: intlMonthNames(locale, { day: "numeric", month: "short" }),
|
|
3946
|
+
dddd: weekdayNames("long"),
|
|
3947
|
+
ddd: weekdayNames("short"),
|
|
3948
|
+
...(am && pm && am !== pm ? { ampm: [am, pm] } : {}),
|
|
3949
|
+
preferMDY: monthIndex >= 0 && dayIndex >= 0 && monthIndex < dayIndex,
|
|
3950
|
+
}, locale);
|
|
3951
|
+
}
|
|
3952
|
+
catch {
|
|
3953
|
+
// Malformed tag: numfmt keeps its own fallback.
|
|
3954
|
+
}
|
|
3955
|
+
};
|
|
3857
3956
|
const localeForPattern = (pattern, locale) => {
|
|
3858
3957
|
return locale;
|
|
3859
3958
|
};
|
|
@@ -3961,14 +4060,158 @@ const DEFAULT_SEPARATORS = { decimal: ".", group: "," };
|
|
|
3961
4060
|
const separatorsFor = (locale) => {
|
|
3962
4061
|
return DEFAULT_SEPARATORS;
|
|
3963
4062
|
};
|
|
4063
|
+
/**
|
|
4064
|
+
* Rewrite the `[$symbol-LCID]` tokens of one section through `rewrite`,
|
|
4065
|
+
* which receives the bracket content (`€-407`) and returns the new content,
|
|
4066
|
+
* or an empty string to drop the token. Quoted literals, escapes and other
|
|
4067
|
+
* bracket directives are left as written.
|
|
4068
|
+
*/
|
|
4069
|
+
const rewriteSectionLocaleTokens = (section, rewrite) => {
|
|
4070
|
+
let out = "";
|
|
4071
|
+
let inQuotes = false;
|
|
4072
|
+
let inBrackets = false;
|
|
4073
|
+
for (let i = 0; i < section.length; i++) {
|
|
4074
|
+
const char = section[i];
|
|
4075
|
+
if (inQuotes) {
|
|
4076
|
+
inQuotes = char !== '"';
|
|
4077
|
+
}
|
|
4078
|
+
else if (inBrackets) {
|
|
4079
|
+
inBrackets = char !== "]";
|
|
4080
|
+
}
|
|
4081
|
+
else if (char === '"') {
|
|
4082
|
+
inQuotes = true;
|
|
4083
|
+
}
|
|
4084
|
+
else if (char === "[") {
|
|
4085
|
+
const end = section[i + 1] === "$" ? section.indexOf("]", i) : -1;
|
|
4086
|
+
if (end > 0) {
|
|
4087
|
+
const token = rewrite(section.slice(i + 1, end));
|
|
4088
|
+
if (token)
|
|
4089
|
+
out += `[${token}]`;
|
|
4090
|
+
i = end;
|
|
4091
|
+
continue;
|
|
4092
|
+
}
|
|
4093
|
+
inBrackets = true;
|
|
4094
|
+
}
|
|
4095
|
+
else if (char === "\\" || char === "_" || char === "*") {
|
|
4096
|
+
out += char + (section[i + 1] ?? "");
|
|
4097
|
+
i++;
|
|
4098
|
+
continue;
|
|
4099
|
+
}
|
|
4100
|
+
out += char;
|
|
4101
|
+
}
|
|
4102
|
+
return out;
|
|
4103
|
+
};
|
|
4104
|
+
/** `[$€-407]` → `[$€]`; a bare `[$-407]` goes away. */
|
|
4105
|
+
const currencySymbolOnly = (token) => {
|
|
4106
|
+
const dash = token.indexOf("-");
|
|
4107
|
+
const symbol = dash < 0 ? token : token.slice(0, dash);
|
|
4108
|
+
return symbol.length > 1 ? symbol : "";
|
|
4109
|
+
};
|
|
4110
|
+
const separatorLocales = new Map();
|
|
4111
|
+
/**
|
|
4112
|
+
* Companion numfmt locale carrying `locale`'s calendar names and AM/PM with
|
|
4113
|
+
* the workbook's decimal / group separators, so a date section pinned to
|
|
4114
|
+
* `[$-407]` renders German names but a workbook decimal mark in `ss.00`.
|
|
4115
|
+
* Returns `locale` itself when its separators already match.
|
|
4116
|
+
*/
|
|
4117
|
+
const getWorkbookSeparatorLocale = (locale, separators) => {
|
|
4118
|
+
const key = `${locale}\u0000${separators.decimal}\u0000${separators.group}`;
|
|
4119
|
+
const cached = separatorLocales.get(key);
|
|
4120
|
+
if (cached !== undefined)
|
|
4121
|
+
return cached;
|
|
4122
|
+
let overlay = locale;
|
|
4123
|
+
try {
|
|
4124
|
+
const base = getLocale(locale);
|
|
4125
|
+
if (base &&
|
|
4126
|
+
(base.decimal !== separators.decimal || base.group !== separators.group)) {
|
|
4127
|
+
// numfmt locale ids are `[a-z\d]+` with one optional `_` part.
|
|
4128
|
+
const hex = (text) => [...text].map((c) => c.codePointAt(0).toString(16)).join("x");
|
|
4129
|
+
overlay = `sep${hex(separators.decimal)}x${hex(separators.group)}_${locale.replace(/[^a-z\d]/gi, "")}`;
|
|
4130
|
+
addLocale({ ...base, decimal: separators.decimal, group: separators.group }, overlay);
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
catch {
|
|
4134
|
+
overlay = locale;
|
|
4135
|
+
}
|
|
4136
|
+
separatorLocales.set(key, overlay);
|
|
4137
|
+
return overlay;
|
|
4138
|
+
};
|
|
4139
|
+
/**
|
|
4140
|
+
* Re-point a date section's `[$symbol-LCID]` token at the companion locale
|
|
4141
|
+
* with the workbook's separators (`$-407` → `$-sep2ex2c_407`). An LCID numfmt
|
|
4142
|
+
* does not know or one carrying calendar bits stays as written.
|
|
4143
|
+
*/
|
|
4144
|
+
const workbookSeparatorLocaleToken = (separators) => (token) => {
|
|
4145
|
+
const dash = token.indexOf("-");
|
|
4146
|
+
if (dash < 0)
|
|
4147
|
+
return token;
|
|
4148
|
+
const code = token.slice(dash + 1);
|
|
4149
|
+
const wincode = Number.parseInt(code, 16);
|
|
4150
|
+
if (Number.isFinite(wincode) && wincode & 0xff0000)
|
|
4151
|
+
return token;
|
|
4152
|
+
if (!getLocale(code))
|
|
4153
|
+
return token;
|
|
4154
|
+
return `${token.slice(0, dash + 1)}${getWorkbookSeparatorLocale(code, separators)}`;
|
|
4155
|
+
};
|
|
4156
|
+
const isDateSection = (section) => {
|
|
4157
|
+
try {
|
|
4158
|
+
return isDateFormat(section);
|
|
4159
|
+
}
|
|
4160
|
+
catch {
|
|
4161
|
+
return false;
|
|
4162
|
+
}
|
|
4163
|
+
};
|
|
4164
|
+
/**
|
|
4165
|
+
* Excel's `[$symbol-LCID]` token selects the currency symbol and the
|
|
4166
|
+
* language of day/month names and AM/PM; digit grouping and the decimal
|
|
4167
|
+
* separator follow the workbook locale (`[$€-407] #,##0.00` is
|
|
4168
|
+
* `€ 1,234.50` on a US host). numfmt applies the token's locale to the whole
|
|
4169
|
+
* section, separators included, so the locale part is dropped from every
|
|
4170
|
+
* section that is not a date (number, scientific, fraction, text, …); a
|
|
4171
|
+
* date section keeps the token for its names, re-pointed at a companion
|
|
4172
|
+
* locale that carries the workbook's separators for fractional seconds.
|
|
4173
|
+
* Returns `undefined` when nothing had to change.
|
|
4174
|
+
*/
|
|
4175
|
+
const withWorkbookSeparators = (pattern, locale) => {
|
|
4176
|
+
if (!pattern.includes("[$"))
|
|
4177
|
+
return undefined;
|
|
4178
|
+
const sections = splitFormatSections(pattern);
|
|
4179
|
+
let changed = false;
|
|
4180
|
+
let dateTokens;
|
|
4181
|
+
const rewritten = sections.map((section) => {
|
|
4182
|
+
if (!section.includes("[$"))
|
|
4183
|
+
return section;
|
|
4184
|
+
let rewrite = currencySymbolOnly;
|
|
4185
|
+
if (isDateSection(section)) {
|
|
4186
|
+
dateTokens ??= workbookSeparatorLocaleToken(separatorsFor());
|
|
4187
|
+
rewrite = dateTokens;
|
|
4188
|
+
}
|
|
4189
|
+
const stripped = rewriteSectionLocaleTokens(section, rewrite);
|
|
4190
|
+
changed ||= stripped !== section;
|
|
4191
|
+
return stripped;
|
|
4192
|
+
});
|
|
4193
|
+
return changed ? rewritten.join(";") : undefined;
|
|
4194
|
+
};
|
|
4195
|
+
const workbookSeparatorPatternCache = new Map();
|
|
4196
|
+
const withWorkbookSeparatorsCached = (code, locale) => {
|
|
4197
|
+
const key = `${""}\u0000${code}`;
|
|
4198
|
+
let rewritten = workbookSeparatorPatternCache.get(key);
|
|
4199
|
+
if (rewritten === undefined) {
|
|
4200
|
+
rewritten = withWorkbookSeparators(code) ?? code;
|
|
4201
|
+
workbookSeparatorPatternCache.set(key, rewritten);
|
|
4202
|
+
}
|
|
4203
|
+
return rewritten;
|
|
4204
|
+
};
|
|
3964
4205
|
const ssfFormat = (pattern, value, locale) => {
|
|
3965
4206
|
try {
|
|
4207
|
+
if (locale)
|
|
4208
|
+
;
|
|
3966
4209
|
let shortAmPm = shortAmPmPatternCache.get(pattern);
|
|
3967
4210
|
if (shortAmPm === undefined && !shortAmPmPatternCache.has(pattern)) {
|
|
3968
4211
|
shortAmPm = withShortAmPmMarker(pattern);
|
|
3969
4212
|
shortAmPmPatternCache.set(pattern, shortAmPm);
|
|
3970
4213
|
}
|
|
3971
|
-
const code = shortAmPm ?? pattern;
|
|
4214
|
+
const code = withWorkbookSeparatorsCached(shortAmPm ?? pattern, locale);
|
|
3972
4215
|
let patternLocale = localeForPattern(pattern, locale);
|
|
3973
4216
|
if (shortAmPm !== undefined && patternLocale) {
|
|
3974
4217
|
patternLocale = getShortAmPmLocale(patternLocale);
|
|
@@ -4671,10 +4914,12 @@ class FormulaHelpersClass {
|
|
|
4671
4914
|
!isRaw && Object.prototype.hasOwnProperty.call(param, "value");
|
|
4672
4915
|
if (hasValueProp) {
|
|
4673
4916
|
if (param.value == null) {
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4917
|
+
// A blank cell is not an omitted argument (only omission takes
|
|
4918
|
+
// `defValue`): Excel reads it as "" in a text slot, 0 in a number
|
|
4919
|
+
// slot and FALSE in a logical slot.
|
|
4920
|
+
if (type === Types.STRING) return "";
|
|
4921
|
+
if (type === Types.NUMBER || type === Types.NUMBER_NO_BOOLEAN) return 0;
|
|
4922
|
+
if (type === Types.BOOLEAN) return false;
|
|
4678
4923
|
param = param.value;
|
|
4679
4924
|
} else {
|
|
4680
4925
|
param = param.value;
|
|
@@ -4754,6 +4999,7 @@ class FormulaHelpersClass {
|
|
|
4754
4999
|
* @param range2
|
|
4755
5000
|
*/
|
|
4756
5001
|
retrieveRanges(context, range1, range2) {
|
|
5002
|
+
if (range1 === undefined) throw FormulaError.ARG_MISSING([Types.ARRAY]);
|
|
4757
5003
|
// process args
|
|
4758
5004
|
range2 = Address.extend(range1, range2);
|
|
4759
5005
|
|
|
@@ -4770,6 +5016,9 @@ class FormulaHelpersClass {
|
|
|
4770
5016
|
}
|
|
4771
5017
|
|
|
4772
5018
|
retrieveArg(context, arg) {
|
|
5019
|
+
// `null` is an explicitly skipped argument (`SUMIF(A1:A5,,B1:B5)`);
|
|
5020
|
+
// `undefined` is one the call never supplied — too few arguments.
|
|
5021
|
+
if (arg === undefined) throw FormulaError.ARG_MISSING([Types.ARRAY]);
|
|
4773
5022
|
if (arg === null) return { value: 0, isArray: false, omitted: true };
|
|
4774
5023
|
const res = context.utils.extractRefValue(arg);
|
|
4775
5024
|
return { value: res.val, isArray: res.isArray, ref: arg.ref };
|
|
@@ -6339,33 +6588,48 @@ const TextFunctions = {
|
|
|
6339
6588
|
return text.slice(0, m.index) + expanded + text.slice(m.index + m[0].length);
|
|
6340
6589
|
},
|
|
6341
6590
|
|
|
6591
|
+
/**
|
|
6592
|
+
* JOIN(delimiter, value_or_array1, [value_or_array2, …]) — Google Sheets.
|
|
6593
|
+
* Ranges / arrays contribute every element; blanks and empty text are
|
|
6594
|
+
* skipped; values spell as TEXTJOIN spells them (TRUE, 15-digit numbers) and
|
|
6595
|
+
* an error among the values propagates. The rnc engine mirrors these rules.
|
|
6596
|
+
*/
|
|
6342
6597
|
JOIN: (delimiter, ...values) => {
|
|
6343
6598
|
delimiter = H$a.accept(delimiter, Types.STRING);
|
|
6344
6599
|
const result = [];
|
|
6345
6600
|
|
|
6346
|
-
H$a.flattenParams(values,
|
|
6347
|
-
if (item
|
|
6348
|
-
|
|
6349
|
-
|
|
6601
|
+
H$a.flattenParams(values, Types.STRING, true, (item) => {
|
|
6602
|
+
if (item === undefined || item === null) return;
|
|
6603
|
+
const str = H$a.accept(item, Types.STRING);
|
|
6604
|
+
if (str !== "") result.push(str);
|
|
6350
6605
|
});
|
|
6351
6606
|
|
|
6352
6607
|
return result.join(delimiter);
|
|
6353
6608
|
},
|
|
6354
6609
|
|
|
6610
|
+
/**
|
|
6611
|
+
* SPLIT(text, delimiter, [split_by_each=TRUE], [remove_empty_text=TRUE]) —
|
|
6612
|
+
* Google Sheets. Pieces stay text (no number coercion); an empty delimiter
|
|
6613
|
+
* is #VALUE!; when every piece is removed the result is empty text. The
|
|
6614
|
+
* rnc engine mirrors these rules.
|
|
6615
|
+
*/
|
|
6355
6616
|
SPLIT: (text, delimiter, splitByEach, removeEmpty) => {
|
|
6356
6617
|
text = H$a.accept(text, Types.STRING);
|
|
6357
6618
|
delimiter = H$a.accept(delimiter, Types.STRING);
|
|
6358
6619
|
splitByEach = H$a.accept(splitByEach, Types.BOOLEAN, true);
|
|
6359
6620
|
removeEmpty = H$a.accept(removeEmpty, Types.BOOLEAN, true);
|
|
6621
|
+
if (delimiter === "") throw FormulaError.VALUE;
|
|
6360
6622
|
|
|
6361
6623
|
let parts;
|
|
6362
6624
|
if (splitByEach) {
|
|
6363
|
-
//
|
|
6364
|
-
|
|
6365
|
-
const
|
|
6366
|
-
|
|
6367
|
-
)
|
|
6368
|
-
|
|
6625
|
+
// Every character of the delimiter is a separator — a literal character,
|
|
6626
|
+
// never a RegExp class ("d" must not become \d, "b" must not become \b).
|
|
6627
|
+
const separators = new Set(delimiter);
|
|
6628
|
+
parts = [""];
|
|
6629
|
+
for (const ch of text) {
|
|
6630
|
+
if (separators.has(ch)) parts.push("");
|
|
6631
|
+
else parts[parts.length - 1] += ch;
|
|
6632
|
+
}
|
|
6369
6633
|
} else {
|
|
6370
6634
|
// Split by the entire delimiter string
|
|
6371
6635
|
parts = text.split(delimiter);
|
|
@@ -6374,6 +6638,7 @@ const TextFunctions = {
|
|
|
6374
6638
|
if (removeEmpty) {
|
|
6375
6639
|
parts = parts.filter((part) => part !== "");
|
|
6376
6640
|
}
|
|
6641
|
+
if (parts.length === 0) return "";
|
|
6377
6642
|
|
|
6378
6643
|
// Return as a single row array
|
|
6379
6644
|
return [parts];
|
|
@@ -12349,20 +12614,56 @@ const collectNumericValues = (array) => {
|
|
|
12349
12614
|
// FORECAST.ETS helpers — Holt-Winters AAA implementation.
|
|
12350
12615
|
// ---------------------------------------------------------------------
|
|
12351
12616
|
|
|
12617
|
+
// Excel's `seasonality` argument: 1 (the default) auto-detects the
|
|
12618
|
+
// period, 0 fits without a seasonal component, 2..8760 forces the
|
|
12619
|
+
// period; anything else is #NUM!. Fractions are truncated.
|
|
12620
|
+
const ETS_MAX_SEASONALITY = 8760;
|
|
12621
|
+
const forecastEtsSeasonality = (seasonalityArg) => {
|
|
12622
|
+
if (seasonalityArg == null) return 1;
|
|
12623
|
+
const value = H$8.accept(seasonalityArg, Types.NUMBER, 1);
|
|
12624
|
+
if (value == null) return 1;
|
|
12625
|
+
const seasonality = Math.floor(value);
|
|
12626
|
+
if (seasonality < 0 || seasonality > ETS_MAX_SEASONALITY) throw FormulaError.NUM;
|
|
12627
|
+
return seasonality;
|
|
12628
|
+
};
|
|
12629
|
+
|
|
12630
|
+
// `data_completion` must be 0 or 1 and `aggregation` 1..7 (AVERAGE,
|
|
12631
|
+
// COUNT, COUNTA, MAX, MEDIAN, MIN, SUM), otherwise #NUM!. Neither
|
|
12632
|
+
// changes the fit: the timeline is used as given (no gap filling,
|
|
12633
|
+
// duplicates are not aggregated).
|
|
12634
|
+
const forecastEtsValidateOptions = (dataCompletionArg, aggregationArg) => {
|
|
12635
|
+
if (dataCompletionArg != null) {
|
|
12636
|
+
const completion = Math.floor(H$8.accept(dataCompletionArg, Types.NUMBER, 1));
|
|
12637
|
+
if (completion !== 0 && completion !== 1) throw FormulaError.NUM;
|
|
12638
|
+
}
|
|
12639
|
+
if (aggregationArg != null) {
|
|
12640
|
+
const aggregation = Math.floor(H$8.accept(aggregationArg, Types.NUMBER, 1));
|
|
12641
|
+
if (aggregation < 1 || aggregation > 7) throw FormulaError.NUM;
|
|
12642
|
+
}
|
|
12643
|
+
};
|
|
12644
|
+
|
|
12352
12645
|
// Pair up timeline + values, sort by timeline, infer the constant
|
|
12353
12646
|
// step (Excel requires a uniformly-spaced timeline), and resolve
|
|
12354
|
-
// the seasonality parameter (auto-
|
|
12647
|
+
// the seasonality parameter (Excel spelling: 1 auto-detects, 0 is no
|
|
12648
|
+
// seasonality, n > 1 is the period).
|
|
12355
12649
|
const forecastEtsPrep = (ys, xs, seasonality) => {
|
|
12356
12650
|
if (xs.length !== ys.length) throw FormulaError.NA;
|
|
12357
12651
|
const pairs = [];
|
|
12358
12652
|
for (let i = 0; i < xs.length; i++) {
|
|
12359
12653
|
const x = xs[i];
|
|
12360
12654
|
const yv = ys[i];
|
|
12655
|
+
// An error anywhere in the series is the result (Excel and the Rust
|
|
12656
|
+
// port's `series_number`); other non-numbers (text, blanks) are skipped.
|
|
12657
|
+
if (x instanceof FormulaError) throw x;
|
|
12658
|
+
if (yv instanceof FormulaError) throw yv;
|
|
12361
12659
|
if (typeof x !== "number") continue;
|
|
12362
12660
|
if (typeof yv !== "number") continue;
|
|
12363
12661
|
pairs.push([x, yv]);
|
|
12364
12662
|
}
|
|
12365
|
-
|
|
12663
|
+
// Excel fits from two pairs (there is no four-pair floor); fewer is
|
|
12664
|
+
// #N/A, and two pairs on the same timeline point have no step (#NUM!
|
|
12665
|
+
// below). rowsncolumns/spreadsheet#642 B-01.
|
|
12666
|
+
if (pairs.length < 2) throw FormulaError.NA;
|
|
12366
12667
|
pairs.sort((a, b) => a[0] - b[0]);
|
|
12367
12668
|
const sortedX = pairs.map(p => p[0]);
|
|
12368
12669
|
const y = pairs.map(p => p[1]);
|
|
@@ -12378,12 +12679,12 @@ const forecastEtsPrep = (ys, xs, seasonality) => {
|
|
|
12378
12679
|
diffs.sort((a, b) => a - b);
|
|
12379
12680
|
const step = diffs[Math.floor(diffs.length / 2)];
|
|
12380
12681
|
let period;
|
|
12381
|
-
if (seasonality ===
|
|
12682
|
+
if (seasonality === 0) {
|
|
12382
12683
|
period = 1; // No seasonality (linear Holt).
|
|
12383
|
-
} else if (seasonality
|
|
12384
|
-
period = seasonality;
|
|
12385
|
-
} else {
|
|
12684
|
+
} else if (seasonality === 1) {
|
|
12386
12685
|
period = detectSeasonality(y);
|
|
12686
|
+
} else {
|
|
12687
|
+
period = seasonality;
|
|
12387
12688
|
}
|
|
12388
12689
|
return { y, lastX: sortedX[sortedX.length - 1], step, period };
|
|
12389
12690
|
};
|
|
@@ -12400,7 +12701,7 @@ const detectSeasonality = (y) => {
|
|
|
12400
12701
|
const n = y.length;
|
|
12401
12702
|
if (n < 4) return 1;
|
|
12402
12703
|
const maxPeriod = Math.min(Math.floor(n / 2), 12);
|
|
12403
|
-
const baseline = runHoltWinters(y, 1, 0.5, 0.1, 0
|
|
12704
|
+
const baseline = runHoltWinters(y, 1, 0.5, 0.1, 0).sse;
|
|
12404
12705
|
let bestPeriod = 1;
|
|
12405
12706
|
let bestSse = baseline;
|
|
12406
12707
|
for (let m = 2; m <= maxPeriod; m++) {
|
|
@@ -12411,7 +12712,7 @@ const detectSeasonality = (y) => {
|
|
|
12411
12712
|
for (const alpha of [0.3, 0.7]) {
|
|
12412
12713
|
for (const beta of [0.1, 0.3]) {
|
|
12413
12714
|
for (const gamma of [0.3, 0.7]) {
|
|
12414
|
-
const sse = runHoltWinters(y, m, alpha, beta, gamma
|
|
12715
|
+
const sse = runHoltWinters(y, m, alpha, beta, gamma).sse;
|
|
12415
12716
|
if (sse < mSse) mSse = sse;
|
|
12416
12717
|
}
|
|
12417
12718
|
}
|
|
@@ -12427,11 +12728,10 @@ const detectSeasonality = (y) => {
|
|
|
12427
12728
|
};
|
|
12428
12729
|
|
|
12429
12730
|
// Fit Holt-Winters additive model with the supplied (or auto-fit) α,
|
|
12430
|
-
// β, γ smoothing parameters. Returns fitted in-sample values
|
|
12431
|
-
// `
|
|
12432
|
-
// (no seasonal component).
|
|
12433
|
-
const fitHoltWinters = (y, period
|
|
12434
|
-
y.length;
|
|
12731
|
+
// β, γ smoothing parameters. Returns the fitted in-sample values and the
|
|
12732
|
+
// terminal state (`forecastAt(h)` extrapolates it to any horizon in O(1)).
|
|
12733
|
+
// period=1 collapses to Holt linear (no seasonal component).
|
|
12734
|
+
const fitHoltWinters = (y, period) => {
|
|
12435
12735
|
// Grid search the smoothing params. The 5×5×5 grid is coarse but
|
|
12436
12736
|
// close enough to the SSE-minimizing optimum that Excel's results
|
|
12437
12737
|
// round-match within ~1% on the seasonal demo series.
|
|
@@ -12441,7 +12741,7 @@ const fitHoltWinters = (y, period, horizon) => {
|
|
|
12441
12741
|
for (const alpha of grid) {
|
|
12442
12742
|
for (const beta of grid) {
|
|
12443
12743
|
for (const gamma of candidateGammas) {
|
|
12444
|
-
const result = runHoltWinters(y, period, alpha, beta, gamma
|
|
12744
|
+
const result = runHoltWinters(y, period, alpha, beta, gamma);
|
|
12445
12745
|
if (best == null || result.sse < best.sse) {
|
|
12446
12746
|
best = { alpha, beta, gamma, ...result };
|
|
12447
12747
|
}
|
|
@@ -12451,7 +12751,25 @@ const fitHoltWinters = (y, period, horizon) => {
|
|
|
12451
12751
|
return best;
|
|
12452
12752
|
};
|
|
12453
12753
|
|
|
12454
|
-
|
|
12754
|
+
// The forecast `h` whole steps past the last observation from a run's
|
|
12755
|
+
// terminal state: level + h·trend + seasonal[(n + h - 1) mod m]. O(1) in
|
|
12756
|
+
// `h` — the old per-step forecast array made `FORECAST.ETS(1E15, …)`
|
|
12757
|
+
// hang the tab (and trapped the Rust engine's wasm with a capacity
|
|
12758
|
+
// overflow that poisoned the whole store). rowsncolumns/spreadsheet#642 B-06.
|
|
12759
|
+
const holtWintersForecastAt = ({ n, m, level, trend, seasonal }, h) => {
|
|
12760
|
+
const seasonComp = m > 1 ? seasonal[(n + h - 1) % m] : 0;
|
|
12761
|
+
return level + h * trend + seasonComp;
|
|
12762
|
+
};
|
|
12763
|
+
|
|
12764
|
+
// One additive Holt-Winters pass with fixed smoothing parameters. The
|
|
12765
|
+
// initial level is the level one step *before* the series (first value
|
|
12766
|
+
// or first-cycle mean, minus the trend), so the one-step-ahead prediction
|
|
12767
|
+
// of the first observation is the observation itself and a perfectly
|
|
12768
|
+
// linear history has zero residual — an initial level *at* the first
|
|
12769
|
+
// value predicted y₀ + trend for y₀, and that spurious residual pulled
|
|
12770
|
+
// the grid search off the exact continuation (10,20,…,100 → 109.56
|
|
12771
|
+
// instead of Excel's 110). rowsncolumns/spreadsheet#642 B-01.
|
|
12772
|
+
const runHoltWinters = (y, period, alpha, beta, gamma) => {
|
|
12455
12773
|
const n = y.length;
|
|
12456
12774
|
const m = period;
|
|
12457
12775
|
const fitted = new Array(n).fill(null);
|
|
@@ -12459,21 +12777,21 @@ const runHoltWinters = (y, period, alpha, beta, gamma, horizon) => {
|
|
|
12459
12777
|
const seasonal = new Array(m).fill(0);
|
|
12460
12778
|
if (m > 1 && n >= 2 * m) {
|
|
12461
12779
|
// Initial seasonal indices = first-cycle deviations from the
|
|
12462
|
-
// first-cycle mean. Initial
|
|
12463
|
-
//
|
|
12464
|
-
//
|
|
12780
|
+
// first-cycle mean. Initial trend = average of per-step changes
|
|
12781
|
+
// between first and second cycle, normalized by m. Initial level
|
|
12782
|
+
// = mean of first cycle, one trend step back.
|
|
12465
12783
|
let sum1 = 0;
|
|
12466
12784
|
for (let i = 0; i < m; i++) sum1 += y[i];
|
|
12467
12785
|
const mean1 = sum1 / m;
|
|
12468
|
-
level = mean1;
|
|
12469
12786
|
let trendSum = 0;
|
|
12470
12787
|
for (let i = 0; i < m; i++) trendSum += (y[i + m] - y[i]) / m;
|
|
12471
12788
|
trend = trendSum / m;
|
|
12789
|
+
level = mean1 - trend;
|
|
12472
12790
|
for (let i = 0; i < m; i++) seasonal[i] = y[i] - mean1;
|
|
12473
12791
|
} else {
|
|
12474
|
-
// Holt linear initialization.
|
|
12475
|
-
level = y[0];
|
|
12792
|
+
// Holt linear initialization, one trend step back.
|
|
12476
12793
|
trend = n > 1 ? y[1] - y[0] : 0;
|
|
12794
|
+
level = y[0] - trend;
|
|
12477
12795
|
}
|
|
12478
12796
|
let sse = 0;
|
|
12479
12797
|
let count = 0;
|
|
@@ -12492,12 +12810,12 @@ const runHoltWinters = (y, period, alpha, beta, gamma, horizon) => {
|
|
|
12492
12810
|
seasonal[seasonIdx] = gamma * (y[t] - level) + (1 - gamma) * seasonComp;
|
|
12493
12811
|
}
|
|
12494
12812
|
}
|
|
12495
|
-
const
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
|
|
12813
|
+
const state = { n, m, level, trend, seasonal };
|
|
12814
|
+
return {
|
|
12815
|
+
fitted,
|
|
12816
|
+
sse: count > 0 ? sse / count : Infinity,
|
|
12817
|
+
forecastAt: (h) => holtWintersForecastAt(state, h),
|
|
12818
|
+
};
|
|
12501
12819
|
};
|
|
12502
12820
|
|
|
12503
12821
|
const percentileInclusive = (values, k) => {
|
|
@@ -13263,20 +13581,18 @@ const DistributionFunctions = {
|
|
|
13263
13581
|
// trend. We use the same idea — scan candidate periods 2..N/2,
|
|
13264
13582
|
// capped at 12 (Excel's default cap for monthly data), and pick
|
|
13265
13583
|
// the one with the lowest in-sample SSE for the fitted Holt-Winters
|
|
13266
|
-
// model.
|
|
13267
|
-
// linear).
|
|
13584
|
+
// model. `seasonality` follows Excel: 1 (default) auto-detects, 0
|
|
13585
|
+
// forces "no seasonality" (Holt linear), n > 1 is the period. The
|
|
13586
|
+
// Rust engine (formualizer `stats/forecast_ets.rs`) is a port of this
|
|
13587
|
+
// code, so both engines agree to 1e-9 on the same inputs.
|
|
13268
13588
|
// -------------------------------------------------------------------
|
|
13269
13589
|
|
|
13270
13590
|
'FORECAST.ETS': (targetDate, values, timeline, seasonalityArg, dataCompletionArg, aggregationArg) => {
|
|
13271
13591
|
const target = H$8.accept(targetDate, Types.NUMBER);
|
|
13272
13592
|
const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
|
|
13273
13593
|
const xs = H$8.accept(timeline, Types.ARRAY, undefined, true, true);
|
|
13274
|
-
const seasonality = seasonalityArg
|
|
13275
|
-
|
|
13276
|
-
: Math.floor(H$8.accept(seasonalityArg, Types.NUMBER, 1));
|
|
13277
|
-
// dataCompletion + aggregation honored only at the prep step —
|
|
13278
|
-
// gap-filling uses the previous value (matches Excel's
|
|
13279
|
-
// dataCompletion=1 default).
|
|
13594
|
+
const seasonality = forecastEtsSeasonality(seasonalityArg);
|
|
13595
|
+
forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
|
|
13280
13596
|
const { y, lastX, step, period } = forecastEtsPrep(ys, xs, seasonality);
|
|
13281
13597
|
if (target < lastX) {
|
|
13282
13598
|
// Excel returns #NUM! for in-history targets in FORECAST.ETS
|
|
@@ -13284,12 +13600,14 @@ const DistributionFunctions = {
|
|
|
13284
13600
|
throw FormulaError.NUM;
|
|
13285
13601
|
}
|
|
13286
13602
|
const horizon = Math.round((target - lastX) / step);
|
|
13603
|
+
if (!Number.isFinite(horizon)) throw FormulaError.NUM;
|
|
13287
13604
|
if (horizon < 1) return y[y.length - 1];
|
|
13288
|
-
const
|
|
13289
|
-
|
|
13605
|
+
const forecast = fitHoltWinters(y, period).forecastAt(horizon);
|
|
13606
|
+
if (!Number.isFinite(forecast)) throw FormulaError.NUM;
|
|
13607
|
+
return forecast;
|
|
13290
13608
|
},
|
|
13291
13609
|
|
|
13292
|
-
'FORECAST.ETS.CONFINT': (targetDate, values, timeline, confidenceArg, seasonalityArg) => {
|
|
13610
|
+
'FORECAST.ETS.CONFINT': (targetDate, values, timeline, confidenceArg, seasonalityArg, dataCompletionArg, aggregationArg) => {
|
|
13293
13611
|
const target = H$8.accept(targetDate, Types.NUMBER);
|
|
13294
13612
|
const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
|
|
13295
13613
|
const xs = H$8.accept(timeline, Types.ARRAY, undefined, true, true);
|
|
@@ -13297,13 +13615,13 @@ const DistributionFunctions = {
|
|
|
13297
13615
|
? 0.95
|
|
13298
13616
|
: H$8.accept(confidenceArg, Types.NUMBER, 0.95);
|
|
13299
13617
|
if (confidence <= 0 || confidence >= 1) throw FormulaError.NUM;
|
|
13300
|
-
const seasonality = seasonalityArg
|
|
13301
|
-
|
|
13302
|
-
: Math.floor(H$8.accept(seasonalityArg, Types.NUMBER, 1));
|
|
13618
|
+
const seasonality = forecastEtsSeasonality(seasonalityArg);
|
|
13619
|
+
forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
|
|
13303
13620
|
const { y, lastX, step, period } = forecastEtsPrep(ys, xs, seasonality);
|
|
13304
13621
|
if (target < lastX) throw FormulaError.NUM;
|
|
13305
13622
|
const horizon = Math.max(1, Math.round((target - lastX) / step));
|
|
13306
|
-
|
|
13623
|
+
if (!Number.isFinite(horizon)) throw FormulaError.NUM;
|
|
13624
|
+
const { fitted } = fitHoltWinters(y, period);
|
|
13307
13625
|
// RMSE of in-sample residuals — z-scaled to the requested
|
|
13308
13626
|
// confidence level. Excel uses √(horizon) scaling on the
|
|
13309
13627
|
// forecast variance.
|
|
@@ -13316,26 +13634,28 @@ const DistributionFunctions = {
|
|
|
13316
13634
|
}
|
|
13317
13635
|
const rmse = n > 0 ? Math.sqrt(sse / n) : 0;
|
|
13318
13636
|
const z = jStat.normal.inv(1 - (1 - confidence) / 2, 0, 1);
|
|
13319
|
-
|
|
13637
|
+
const halfWidth = z * rmse * Math.sqrt(horizon);
|
|
13638
|
+
if (!Number.isFinite(halfWidth)) throw FormulaError.NUM;
|
|
13639
|
+
return halfWidth;
|
|
13320
13640
|
},
|
|
13321
13641
|
|
|
13322
|
-
'FORECAST.ETS.SEASONALITY': (values, timeline) => {
|
|
13642
|
+
'FORECAST.ETS.SEASONALITY': (values, timeline, dataCompletionArg, aggregationArg) => {
|
|
13323
13643
|
const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
|
|
13324
13644
|
const xs = H$8.accept(timeline, Types.ARRAY, undefined, true, true);
|
|
13325
|
-
|
|
13326
|
-
|
|
13645
|
+
forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
|
|
13646
|
+
const { period } = forecastEtsPrep(ys, xs, 1);
|
|
13647
|
+
return period;
|
|
13327
13648
|
},
|
|
13328
13649
|
|
|
13329
|
-
'FORECAST.ETS.STAT': (values, timeline, statTypeArg, seasonalityArg) => {
|
|
13650
|
+
'FORECAST.ETS.STAT': (values, timeline, statTypeArg, seasonalityArg, dataCompletionArg, aggregationArg) => {
|
|
13330
13651
|
const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
|
|
13331
13652
|
const xs = H$8.accept(timeline, Types.ARRAY, undefined, true, true);
|
|
13332
13653
|
const statType = Math.floor(H$8.accept(statTypeArg, Types.NUMBER));
|
|
13333
13654
|
if (statType < 1 || statType > 8) throw FormulaError.NUM;
|
|
13334
|
-
const seasonality = seasonalityArg
|
|
13335
|
-
|
|
13336
|
-
: Math.floor(H$8.accept(seasonalityArg, Types.NUMBER, 1));
|
|
13655
|
+
const seasonality = forecastEtsSeasonality(seasonalityArg);
|
|
13656
|
+
forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
|
|
13337
13657
|
const { y, period } = forecastEtsPrep(ys, xs, seasonality);
|
|
13338
|
-
const { alpha, beta, gamma, fitted } = fitHoltWinters(y, period
|
|
13658
|
+
const { alpha, beta, gamma, fitted } = fitHoltWinters(y, period);
|
|
13339
13659
|
// Excel STAT codes:
|
|
13340
13660
|
// 1: alpha 2: beta 3: gamma 4: MASE
|
|
13341
13661
|
// 5: SMAPE 6: MAE 7: RMSE 8: detected step size
|
|
@@ -14420,6 +14740,9 @@ const DistributionFunctions = {
|
|
|
14420
14740
|
const prediction = truncated.reduce((sum, value, idx) => sum + value * slopes[idx], 0) + intercept;
|
|
14421
14741
|
return [prediction];
|
|
14422
14742
|
});
|
|
14743
|
+
// A single new_x is a plain number in Excel (and in the Rust
|
|
14744
|
+
// engine); a 1x1 array would anchor an array formula on the cell.
|
|
14745
|
+
if (result.length === 1) return result[0][0];
|
|
14423
14746
|
return result;
|
|
14424
14747
|
},
|
|
14425
14748
|
|
|
@@ -15402,7 +15725,7 @@ const StatisticalFunctions = {
|
|
|
15402
15725
|
COUNTIF: (range, criteria) => {
|
|
15403
15726
|
// do not flatten the array
|
|
15404
15727
|
range = H$7.accept(range, Types.ARRAY, undefined, false, true);
|
|
15405
|
-
const isCriteriaArray = criteria
|
|
15728
|
+
const isCriteriaArray = criteria?.isArray;
|
|
15406
15729
|
criteria = H$7.accept(criteria);
|
|
15407
15730
|
|
|
15408
15731
|
const countMatching = (parsed) => {
|
|
@@ -16717,7 +17040,15 @@ const MathFunctions = {
|
|
|
16717
17040
|
FLOOR: (number, significance) => {
|
|
16718
17041
|
number = H$6.accept(number, Types.NUMBER);
|
|
16719
17042
|
significance = H$6.accept(significance, Types.NUMBER, 1);
|
|
16720
|
-
|
|
17043
|
+
// Excel: a zero significance is #DIV/0! for FLOOR of a non-zero number
|
|
17044
|
+
// (FLOOR(0,0) is 0; CEILING, FLOOR.MATH and FLOOR.PRECISE return 0); a
|
|
17045
|
+
// positive number with a negative significance is #NUM!; a negative
|
|
17046
|
+
// number rounds away from zero unless the significance is negative too
|
|
17047
|
+
// (FLOOR(-2.5,2) = -4, FLOOR(-2.5,-2) = -2).
|
|
17048
|
+
if (significance === 0) {
|
|
17049
|
+
if (number === 0) return 0;
|
|
17050
|
+
throw FormulaError.DIV0;
|
|
17051
|
+
}
|
|
16721
17052
|
if (number > 0 && significance < 0) throw FormulaError.NUM;
|
|
16722
17053
|
const absSignificance = Math.abs(significance);
|
|
16723
17054
|
const quotient = excelQuotient(Math.abs(number), absSignificance);
|
|
@@ -16742,6 +17073,7 @@ const MathFunctions = {
|
|
|
16742
17073
|
// mode can be 0 or any other number, 0 means away from zero
|
|
16743
17074
|
// the official documentation seems wrong
|
|
16744
17075
|
mode = H$6.accept(mode, Types.NUMBER, 0);
|
|
17076
|
+
if (significance === 0) return 0;
|
|
16745
17077
|
// The Mode argument does not affect positive numbers.
|
|
16746
17078
|
if (mode === 0 || number >= 0) {
|
|
16747
17079
|
// away from zero
|
|
@@ -16754,7 +17086,8 @@ const MathFunctions = {
|
|
|
16754
17086
|
"FLOOR.PRECISE": (number, significance) => {
|
|
16755
17087
|
number = H$6.accept(number, Types.NUMBER);
|
|
16756
17088
|
significance = H$6.accept(significance, Types.NUMBER, 1);
|
|
16757
|
-
//
|
|
17089
|
+
// A zero significance is 0 (Excel); otherwise the mathematical floor.
|
|
17090
|
+
if (significance === 0) return 0;
|
|
16758
17091
|
return MathFunctions.FLOOR(number, Math.abs(significance));
|
|
16759
17092
|
},
|
|
16760
17093
|
|
|
@@ -17260,6 +17593,22 @@ const MathFunctions = {
|
|
|
17260
17593
|
return result;
|
|
17261
17594
|
},
|
|
17262
17595
|
|
|
17596
|
+
/**
|
|
17597
|
+
* PERCENTOF(data_subset, data_all) — SUM(data_subset) / SUM(data_all) under
|
|
17598
|
+
* SUM's rules (text / logical / blank cells inside ranges are skipped, error
|
|
17599
|
+
* values propagate, a non-numeric scalar is #VALUE!); a zero whole is
|
|
17600
|
+
* #DIV/0!. Excel 365's helper for GROUPBY / PIVOTBY percent-of-total.
|
|
17601
|
+
*/
|
|
17602
|
+
PERCENTOF: (dataSubset, dataAll) => {
|
|
17603
|
+
if (H$6.isOmitted(dataSubset) || H$6.isOmitted(dataAll)) {
|
|
17604
|
+
throw FormulaError.ARG_MISSING([Types.NUMBER]);
|
|
17605
|
+
}
|
|
17606
|
+
const subset = MathFunctions.SUM(dataSubset);
|
|
17607
|
+
const all = MathFunctions.SUM(dataAll);
|
|
17608
|
+
if (all === 0) throw FormulaError.DIV0;
|
|
17609
|
+
return subset / all;
|
|
17610
|
+
},
|
|
17611
|
+
|
|
17263
17612
|
/**
|
|
17264
17613
|
* This functions requires instance of {@link FormulaParser}.
|
|
17265
17614
|
*/
|
|
@@ -17767,6 +18116,11 @@ const applyToArray$1 = (value, checkFn) => {
|
|
|
17767
18116
|
* Get the number of values that evaluate to true and false.
|
|
17768
18117
|
* Cast Number and "TRUE", "FALSE" to boolean.
|
|
17769
18118
|
* Ignore unrelated values.
|
|
18119
|
+
* An error value — a direct argument (`AND(1/0, FALSE)`) or a cell inside a
|
|
18120
|
+
* reference — is the result, whatever the other arguments decide: Excel never
|
|
18121
|
+
* short-circuits AND/OR/XOR past an error, and the first one in argument order
|
|
18122
|
+
* wins (rowsncolumns/spreadsheet#642 B-08). `flattenParams` only throws error
|
|
18123
|
+
* values itself when a value type is requested.
|
|
17770
18124
|
* @ignore
|
|
17771
18125
|
* @param {any[]} params
|
|
17772
18126
|
* @return {number[]}
|
|
@@ -17774,6 +18128,8 @@ const applyToArray$1 = (value, checkFn) => {
|
|
|
17774
18128
|
function getNumLogicalValue(params) {
|
|
17775
18129
|
let numTrue = 0, numFalse = 0;
|
|
17776
18130
|
H$4.flattenParams(params, null, true, (val, info) => {
|
|
18131
|
+
if (val instanceof FormulaError)
|
|
18132
|
+
throw val;
|
|
17777
18133
|
const type = typeof val;
|
|
17778
18134
|
if (type === "string") {
|
|
17779
18135
|
// Text inside a reference or array is ignored; a direct text
|
|
@@ -19846,6 +20202,10 @@ const ReferenceFunctions = {
|
|
|
19846
20202
|
// Retrieve actual data to determine effective column count
|
|
19847
20203
|
const { val: data } = context.utils.extractRefValue(obj, context.position);
|
|
19848
20204
|
const colCount = Array.isArray(data) && Array.isArray(data[0]) ? data[0].length : 1;
|
|
20205
|
+
// One column (`COLUMN(C:C)`, `COLUMN(B2:B5)`) is a plain number, as
|
|
20206
|
+
// Excel shows it and as the Rust engine returns it; a 1x1 array would
|
|
20207
|
+
// make the host anchor an array formula on the cell.
|
|
20208
|
+
if (colCount <= 1) return fromCol;
|
|
19849
20209
|
const result = [];
|
|
19850
20210
|
for (let i = 0; i < colCount; i++) {
|
|
19851
20211
|
result.push(fromCol + i);
|
|
@@ -19862,7 +20222,11 @@ const ReferenceFunctions = {
|
|
|
19862
20222
|
if (obj == null) {
|
|
19863
20223
|
throw Error("COLUMNS requires one argument");
|
|
19864
20224
|
}
|
|
19865
|
-
if (
|
|
20225
|
+
if (obj instanceof FormulaError) throw obj;
|
|
20226
|
+
// An array (`{1,2;3,4}`, `SEQUENCE(3,2)`, `RANDARRAY(3,2)`, a SORT/FILTER
|
|
20227
|
+
// result) reports its width; a scalar is a 1×1 array (`COLUMNS(5)` = 1).
|
|
20228
|
+
if (Array.isArray(obj)) return obj[0]?.length ?? 0;
|
|
20229
|
+
if (typeof obj != "object") return 1;
|
|
19866
20230
|
if (H$2.isCellRef(obj)) {
|
|
19867
20231
|
return 1;
|
|
19868
20232
|
} else if (H$2.isRangeRef(obj)) {
|
|
@@ -19913,6 +20277,7 @@ const ReferenceFunctions = {
|
|
|
19913
20277
|
|
|
19914
20278
|
// Special
|
|
19915
20279
|
INDEX: (context, ranges, rowNum, colNum, areaNum) => {
|
|
20280
|
+
if (ranges === undefined) throw FormulaError.ARG_MISSING([Types.ARRAY]);
|
|
19916
20281
|
// retrieve values
|
|
19917
20282
|
const isColNumMissing = colNum == null;
|
|
19918
20283
|
const isRowNumMissing = rowNum == null;
|
|
@@ -20295,6 +20660,8 @@ const ReferenceFunctions = {
|
|
|
20295
20660
|
// Retrieve actual data to determine effective row count
|
|
20296
20661
|
const { val: data } = context.utils.extractRefValue(obj, context.position);
|
|
20297
20662
|
const rowCount = Array.isArray(data) ? data.length : 1;
|
|
20663
|
+
// One row (`ROW(3:3)`, `ROW(B2:C2)`) is a plain number; see COLUMN.
|
|
20664
|
+
if (rowCount <= 1) return fromRow;
|
|
20298
20665
|
const result = [];
|
|
20299
20666
|
for (let i = 0; i < rowCount; i++) {
|
|
20300
20667
|
result.push([fromRow + i]); // Vertical array (column vector)
|
|
@@ -20311,7 +20678,11 @@ const ReferenceFunctions = {
|
|
|
20311
20678
|
if (obj == null) {
|
|
20312
20679
|
throw Error("ROWS requires one argument");
|
|
20313
20680
|
}
|
|
20314
|
-
if (
|
|
20681
|
+
if (obj instanceof FormulaError) throw obj;
|
|
20682
|
+
// An array (`{1,2;3,4}`, `SEQUENCE(3,2)`, `RANDARRAY(3,2)`, a SORT/FILTER
|
|
20683
|
+
// result) reports its height; a scalar is a 1×1 array (`ROWS(5)` = 1).
|
|
20684
|
+
if (Array.isArray(obj)) return obj.length;
|
|
20685
|
+
if (typeof obj != "object") return 1;
|
|
20315
20686
|
if (H$2.isCellRef(obj)) {
|
|
20316
20687
|
return 1;
|
|
20317
20688
|
} else if (H$2.isRangeRef(obj)) {
|
|
@@ -21113,6 +21484,44 @@ const ReferenceFunctions = {
|
|
|
21113
21484
|
);
|
|
21114
21485
|
},
|
|
21115
21486
|
|
|
21487
|
+
/**
|
|
21488
|
+
* TRIMRANGE(range, [trim_rows], [trim_cols]) — drops the blank rows /
|
|
21489
|
+
* columns at the edges of a range or array: 0 keeps every row / column,
|
|
21490
|
+
* 1 trims leading blanks, 2 trailing blanks, 3 (default) both. Only truly
|
|
21491
|
+
* empty cells are blank (a "" text result or a 0 keeps its row); trimming
|
|
21492
|
+
* everything away is `#REF!`, as in Excel. The function form of Excel's
|
|
21493
|
+
* `A1.:.E10` / `A1:.E10` / `A1.:E10` trim-ref operators.
|
|
21494
|
+
*/
|
|
21495
|
+
TRIMRANGE: (range, trim_rows, trim_cols) => {
|
|
21496
|
+
if (H$2.isOmitted(range)) throw FormulaError.ARG_MISSING([Types.ARRAY]);
|
|
21497
|
+
const mode = (arg) => {
|
|
21498
|
+
if (H$2.isOmitted(arg)) return 3;
|
|
21499
|
+
const n = Math.trunc(H$2.accept(arg, Types.NUMBER, 3));
|
|
21500
|
+
if (n < 0 || n > 3) throw FormulaError.VALUE;
|
|
21501
|
+
return n;
|
|
21502
|
+
};
|
|
21503
|
+
const trimRows = mode(trim_rows);
|
|
21504
|
+
const trimCols = mode(trim_cols);
|
|
21505
|
+
const m = toMatrix(range);
|
|
21506
|
+
const height = m.length;
|
|
21507
|
+
const width = m[0]?.length ?? 0;
|
|
21508
|
+
const isBlank = (v) => v == null;
|
|
21509
|
+
const blankRow = (r) => m[r].every(isBlank);
|
|
21510
|
+
const blankCol = (c) => m.every((row) => isBlank(row[c]));
|
|
21511
|
+
let rowStart = 0;
|
|
21512
|
+
let rowEnd = height;
|
|
21513
|
+
if (trimRows & 1) while (rowStart < rowEnd && blankRow(rowStart)) rowStart++;
|
|
21514
|
+
if (trimRows & 2) while (rowEnd > rowStart && blankRow(rowEnd - 1)) rowEnd--;
|
|
21515
|
+
let colStart = 0;
|
|
21516
|
+
let colEnd = width;
|
|
21517
|
+
if (trimCols & 1) while (colStart < colEnd && blankCol(colStart)) colStart++;
|
|
21518
|
+
if (trimCols & 2) while (colEnd > colStart && blankCol(colEnd - 1)) colEnd--;
|
|
21519
|
+
if (rowStart >= rowEnd || colStart >= colEnd) throw FormulaError.REF;
|
|
21520
|
+
return m
|
|
21521
|
+
.slice(rowStart, rowEnd)
|
|
21522
|
+
.map((row) => row.slice(colStart, colEnd));
|
|
21523
|
+
},
|
|
21524
|
+
|
|
21116
21525
|
/**
|
|
21117
21526
|
* EXPAND(array, rows, [columns], [pad_with]) — grows an array to
|
|
21118
21527
|
* `rows`×`columns`, filling new cells with `pad_with` (default `#N/A`).
|
|
@@ -21219,21 +21628,33 @@ const ReferenceFunctions = {
|
|
|
21219
21628
|
return result;
|
|
21220
21629
|
},
|
|
21221
21630
|
|
|
21631
|
+
/**
|
|
21632
|
+
* SORTN(range, [n], [display_ties_mode], [sort_column], [is_ascending]) —
|
|
21633
|
+
* Google Sheets. SORT on one key column followed by the first n rows;
|
|
21634
|
+
* display_ties_mode is accepted and ignored. A negative n is #VALUE!. The
|
|
21635
|
+
* rnc engine mirrors these rules.
|
|
21636
|
+
*/
|
|
21222
21637
|
SORTN: (context, range, n, displayTies, sortColumn, isAscending) => {
|
|
21223
|
-
range = H$2.accept(range, Types.ARRAY);
|
|
21638
|
+
range = H$2.accept(range, Types.ARRAY, undefined, false, true);
|
|
21224
21639
|
n = H$2.accept(n, Types.NUMBER, range.length);
|
|
21225
21640
|
displayTies = H$2.accept(displayTies, Types.NUMBER, 0);
|
|
21226
21641
|
sortColumn = H$2.accept(sortColumn, Types.NUMBER, 1);
|
|
21227
21642
|
isAscending = H$2.accept(isAscending, Types.BOOLEAN, true);
|
|
21228
|
-
|
|
21229
|
-
|
|
21230
|
-
|
|
21231
|
-
|
|
21232
|
-
|
|
21233
|
-
|
|
21234
|
-
|
|
21235
|
-
|
|
21236
|
-
|
|
21643
|
+
if (n < 0) throw FormulaError.VALUE;
|
|
21644
|
+
if (!Array.isArray(range) || range.length === 0) return range;
|
|
21645
|
+
// Same stable row sort as SORT's row path (SORT itself re-parses its
|
|
21646
|
+
// arguments as parser params, so it cannot be called with the already
|
|
21647
|
+
// accepted values — doing so made every SORTN call #VALUE!).
|
|
21648
|
+
const width = range[0]?.length ?? 0;
|
|
21649
|
+
if (sortColumn < 1 || sortColumn > width) throw FormulaError.VALUE;
|
|
21650
|
+
const key = sortColumn - 1;
|
|
21651
|
+
const compareValues = makeComparator(context?.locale);
|
|
21652
|
+
const indexed = range.map((row, idx) => ({ row, idx }));
|
|
21653
|
+
indexed.sort((a, b) => {
|
|
21654
|
+
const comparison = compareValues(a.row[key], b.row[key]);
|
|
21655
|
+
return (isAscending ? comparison : -comparison) || a.idx - b.idx;
|
|
21656
|
+
});
|
|
21657
|
+
return indexed.slice(0, n).map((entry) => entry.row);
|
|
21237
21658
|
},
|
|
21238
21659
|
|
|
21239
21660
|
/**
|
|
@@ -25346,6 +25767,10 @@ const FinancialFunctions = {
|
|
|
25346
25767
|
// identifiers. Importers keep the cached result Excel wrote and exporters write
|
|
25347
25768
|
// the formula back unchanged, so a workbook round-trips intact; only a
|
|
25348
25769
|
// recalculation on this engine surfaces the #N/A.
|
|
25770
|
+
//
|
|
25771
|
+
// QUERY is the Google Sheets counterpart: it needs a query-language interpreter
|
|
25772
|
+
// neither engine has, and degrades the same way (the rnc engine registers the
|
|
25773
|
+
// same stub) so a Sheets-authored workbook evaluates identically on both.
|
|
25349
25774
|
const unavailable = () => {
|
|
25350
25775
|
throw FormulaError.NA;
|
|
25351
25776
|
};
|
|
@@ -25365,6 +25790,7 @@ const ExternalFunctions = {
|
|
|
25365
25790
|
EUROCONVERT: unavailable,
|
|
25366
25791
|
TRANSLATE: unavailable,
|
|
25367
25792
|
DETECTLANGUAGE: unavailable,
|
|
25793
|
+
QUERY: unavailable,
|
|
25368
25794
|
};
|
|
25369
25795
|
|
|
25370
25796
|
var api$3 = {};
|
|
@@ -44513,7 +44939,10 @@ class Parser extends EmbeddedActionsParser {
|
|
|
44513
44939
|
) {
|
|
44514
44940
|
return arr[0][0];
|
|
44515
44941
|
}
|
|
44516
|
-
|
|
44942
|
+
const array = this.utils.toArray(arr);
|
|
44943
|
+
return context.async
|
|
44944
|
+
? this.utils.settleArrayItems(array, $.position)
|
|
44945
|
+
: array;
|
|
44517
44946
|
});
|
|
44518
44947
|
});
|
|
44519
44948
|
|
|
@@ -44534,9 +44963,13 @@ class Parser extends EmbeddedActionsParser {
|
|
|
44534
44963
|
const prefix = $.OPTION(() => $.SUBRULE($.plusMinusOp));
|
|
44535
44964
|
const image = $.CONSUME(Number$1).image;
|
|
44536
44965
|
const number = $.ACTION(() => this.utils.toNumber(image));
|
|
44966
|
+
// A signed literal is a plain number: apply the sign directly
|
|
44967
|
+
// rather than through `applyPrefix`, whose async branch would
|
|
44968
|
+
// hand the array a pending Promise (`{-100,60,60}` summed to 120
|
|
44969
|
+
// under `parseAsync`).
|
|
44537
44970
|
if (prefix)
|
|
44538
44971
|
return $.ACTION(() =>
|
|
44539
|
-
this.utils.
|
|
44972
|
+
this.utils.applyLiteralPrefix(prefix, number)
|
|
44540
44973
|
);
|
|
44541
44974
|
return number;
|
|
44542
44975
|
},
|
|
@@ -45173,18 +45606,21 @@ const expandLet = (source) => {
|
|
|
45173
45606
|
|
|
45174
45607
|
|
|
45175
45608
|
const LAMBDA_INTERNAL = "__RNC_LAMBDA__";
|
|
45176
|
-
/** Source spliced in for
|
|
45609
|
+
/** Source spliced in for a parameter the caller left out. */
|
|
45177
45610
|
const OMITTED_INTERNAL = "__RNC_OMITTED__";
|
|
45178
45611
|
const OMITTED_CALL = `${OMITTED_INTERNAL}()`;
|
|
45179
45612
|
/**
|
|
45180
45613
|
* Runtime call of a lambda value: `__RNC_CALL__(lambda, arg1, …)`. Used
|
|
45181
|
-
* when `LAMBDA(...)(...)` cannot be inlined (
|
|
45182
|
-
* evaluator reports `#VALUE!` like Excel instead of a
|
|
45614
|
+
* when `LAMBDA(...)(...)` cannot be inlined (more arguments than
|
|
45615
|
+
* parameters) so the evaluator reports `#VALUE!` like Excel instead of a
|
|
45616
|
+
* parse error.
|
|
45183
45617
|
*/
|
|
45184
45618
|
const CALL_INTERNAL = "__RNC_CALL__";
|
|
45185
45619
|
|
|
45186
|
-
// `[name]` — Excel's optional-parameter
|
|
45620
|
+
// `[name]` — Excel's optional-parameter spelling. The lexer sees it as a
|
|
45187
45621
|
// table-column token; only a bare identifier inside the brackets counts.
|
|
45622
|
+
// The brackets are documentation only: Excel lets a call leave out any
|
|
45623
|
+
// trailing parameter, bracketed or not (ISOMITTED reports it either way).
|
|
45188
45624
|
const OPTIONAL_PARAM = /^\[\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\]$/;
|
|
45189
45625
|
|
|
45190
45626
|
/**
|
|
@@ -45202,8 +45638,6 @@ const paramFromSpan = (tokens, span) => {
|
|
|
45202
45638
|
return null;
|
|
45203
45639
|
};
|
|
45204
45640
|
|
|
45205
|
-
const requiredCount = (params) => params.filter((p) => !p.optional).length;
|
|
45206
|
-
|
|
45207
45641
|
/** Excel writes optional params back as `[name]`; keep that in the value form. */
|
|
45208
45642
|
const paramsToSource = (params) =>
|
|
45209
45643
|
params.map((p) => (p.optional ? `[${p.name}]` : p.name)).join(",");
|
|
@@ -45213,7 +45647,9 @@ const paramsToSource = (params) =>
|
|
|
45213
45647
|
* IIFE inlining: when `LAMBDA(...)(...)` appears, substitute the
|
|
45214
45648
|
* params with the call's arg sources and return the substituted body.
|
|
45215
45649
|
* Returns null if the structure doesn't match (e.g. LAMBDA without
|
|
45216
|
-
* immediate invocation
|
|
45650
|
+
* immediate invocation). A call with more arguments than parameters is
|
|
45651
|
+
* handed to the runtime, which reports `#VALUE!`; a call with fewer binds
|
|
45652
|
+
* the missing parameters to the omitted marker.
|
|
45217
45653
|
*/
|
|
45218
45654
|
const inlineIIFE = (source, tokens, lambdaOpenIdx, lambdaCloseIdx) => {
|
|
45219
45655
|
// Next token after the lambda's close paren must be a bare OpenParen
|
|
@@ -45241,13 +45677,10 @@ const inlineIIFE = (source, tokens, lambdaOpenIdx, lambdaCloseIdx) => {
|
|
|
45241
45677
|
callCloseIdx === callOpenIdx + 1
|
|
45242
45678
|
? []
|
|
45243
45679
|
: splitArgs(tokens, callOpenIdx + 1, callCloseIdx);
|
|
45244
|
-
if (
|
|
45245
|
-
|
|
45246
|
-
|
|
45247
|
-
|
|
45248
|
-
// Arity mismatch — hand the call to the runtime, which applies the
|
|
45249
|
-
// lambda value and reports #VALUE! (Excel's answer) instead of the
|
|
45250
|
-
// parser choking on `(...)(...)`.
|
|
45680
|
+
if (callArgSpans.length > params.length) {
|
|
45681
|
+
// Too many arguments — hand the call to the runtime, which applies
|
|
45682
|
+
// the lambda value and reports #VALUE! (Excel's answer) instead of
|
|
45683
|
+
// the parser choking on `(...)(...)`.
|
|
45251
45684
|
const lambdaValue = transformLambdaToValue(
|
|
45252
45685
|
source,
|
|
45253
45686
|
tokens,
|
|
@@ -45265,9 +45698,9 @@ const inlineIIFE = (source, tokens, lambdaOpenIdx, lambdaCloseIdx) => {
|
|
|
45265
45698
|
};
|
|
45266
45699
|
}
|
|
45267
45700
|
const bindings = params.map(({ name }, i) => {
|
|
45268
|
-
// Raw value — substituteNamesInBody decides the wrap.
|
|
45269
|
-
//
|
|
45270
|
-
//
|
|
45701
|
+
// Raw value — substituteNamesInBody decides the wrap. A parameter the
|
|
45702
|
+
// call left out binds to the omitted marker so ISOMITTED can see it
|
|
45703
|
+
// (and any other use of it is #VALUE!).
|
|
45271
45704
|
const value =
|
|
45272
45705
|
i < callArgSpans.length
|
|
45273
45706
|
? sourceSlice(source, tokens, callArgSpans[i]).trim()
|
|
@@ -45392,14 +45825,15 @@ const isLambdaValue = (v) =>
|
|
|
45392
45825
|
v != null && typeof v === "object" && v.__isLambda === true;
|
|
45393
45826
|
|
|
45394
45827
|
/**
|
|
45395
|
-
* Marker bound to
|
|
45828
|
+
* Marker bound to a parameter the caller left out (Excel allows a call
|
|
45829
|
+
* with fewer arguments than parameters, `[param]` or not). Only
|
|
45396
45830
|
* `ISOMITTED` looks at it; anywhere else it is an invalid operand.
|
|
45397
45831
|
*/
|
|
45398
45832
|
const OMITTED = Object.freeze({ __isOmitted: true });
|
|
45399
45833
|
const isOmittedValue = (v) =>
|
|
45400
45834
|
v != null && typeof v === "object" && v.__isOmitted === true;
|
|
45401
45835
|
|
|
45402
|
-
/** `[name]` → optional
|
|
45836
|
+
/** `[name]` → parameter documented as optional, `name` → plain parameter. */
|
|
45403
45837
|
const parseLambdaParams = (paramsStr) =>
|
|
45404
45838
|
paramsStr
|
|
45405
45839
|
.split(",")
|
|
@@ -45410,9 +45844,43 @@ const parseLambdaParams = (paramsStr) =>
|
|
|
45410
45844
|
return { name: optional ? raw.slice(1, -1).trim() : raw, optional };
|
|
45411
45845
|
});
|
|
45412
45846
|
|
|
45847
|
+
/**
|
|
45848
|
+
* An omitted marker that escapes `ISOMITTED` — a body that returns the
|
|
45849
|
+
* omitted parameter itself, or spreads it into an array — is `#VALUE!`
|
|
45850
|
+
* in the result, per cell inside an array. Arrays are only copied when
|
|
45851
|
+
* one of their cells actually holds the marker.
|
|
45852
|
+
*/
|
|
45853
|
+
const rejectOmittedResult = (result) => {
|
|
45854
|
+
if (isOmittedValue(result)) return FormulaError.VALUE;
|
|
45855
|
+
if (!Array.isArray(result)) return result;
|
|
45856
|
+
let out = null;
|
|
45857
|
+
for (let i = 0; i < result.length; i++) {
|
|
45858
|
+
const cell = rejectOmittedResult(result[i]);
|
|
45859
|
+
if (cell !== result[i]) {
|
|
45860
|
+
if (out === null) out = result.slice();
|
|
45861
|
+
out[i] = cell;
|
|
45862
|
+
}
|
|
45863
|
+
}
|
|
45864
|
+
return out ?? result;
|
|
45865
|
+
};
|
|
45866
|
+
|
|
45413
45867
|
const requiredParamCount = (lambda) =>
|
|
45414
45868
|
lambda.params.filter((_, i) => !(lambda.optional?.[i] ?? false)).length;
|
|
45415
45869
|
|
|
45870
|
+
/**
|
|
45871
|
+
* Excel's "Incorrect Parameters" `#VALUE!` for MAP / REDUCE / SCAN / BYROW /
|
|
45872
|
+
* BYCOL / MAKEARRAY / GROUPBY / PIVOTBY: the helper drives the lambda with
|
|
45873
|
+
* a fixed number of arguments and the lambda must declare a parameter for
|
|
45874
|
+
* each of them. Only a `[name]` parameter may stay unsupplied — the
|
|
45875
|
+
* "fewer arguments than parameters" allowance is for direct calls, not
|
|
45876
|
+
* helpers. Returns the error for the whole helper call, or null when the
|
|
45877
|
+
* counts fit.
|
|
45878
|
+
*/
|
|
45879
|
+
const hofArityError = (lambda, supplied) =>
|
|
45880
|
+
supplied < requiredParamCount(lambda) || supplied > lambda.params.length
|
|
45881
|
+
? FormulaError.VALUE
|
|
45882
|
+
: null;
|
|
45883
|
+
|
|
45416
45884
|
const formatLiteral = (value) => {
|
|
45417
45885
|
if (value === null || value === undefined) return "0";
|
|
45418
45886
|
if (value instanceof FormulaError) {
|
|
@@ -45455,12 +45923,11 @@ const formatLiteral = (value) => {
|
|
|
45455
45923
|
*/
|
|
45456
45924
|
const applyLambda = async (context, lambda, args) => {
|
|
45457
45925
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
45458
|
-
|
|
45459
|
-
|
|
45460
|
-
|
|
45461
|
-
|
|
45462
|
-
|
|
45463
|
-
}
|
|
45926
|
+
// Excel rejects surplus arguments but accepts fewer than declared: the
|
|
45927
|
+
// missing parameters are omitted (ISOMITTED → TRUE, any other use →
|
|
45928
|
+
// #VALUE!). Helpers check their own parameter count with
|
|
45929
|
+
// `hofArityError` before getting here.
|
|
45930
|
+
if (args.length > lambda.params.length) return FormulaError.VALUE;
|
|
45464
45931
|
|
|
45465
45932
|
context.__lambdaDepth = (context.__lambdaDepth ?? 0) + 1;
|
|
45466
45933
|
if (context.__lambdaDepth > MAX_LAMBDA_DEPTH) {
|
|
@@ -45473,14 +45940,14 @@ const applyLambda = async (context, lambda, args) => {
|
|
|
45473
45940
|
try {
|
|
45474
45941
|
const bindings = lambda.params.map((name, i) => ({
|
|
45475
45942
|
name,
|
|
45476
|
-
//
|
|
45943
|
+
// A parameter the call left out binds to the omitted marker so
|
|
45477
45944
|
// ISOMITTED inside the body can report it.
|
|
45478
45945
|
value:
|
|
45479
45946
|
i < args.length ? `(${formatLiteral(args[i])})` : `(${OMITTED_CALL})`,
|
|
45480
45947
|
}));
|
|
45481
45948
|
const substituted = substituteNamesInBody(lambda.body, bindings);
|
|
45482
45949
|
const result = await context.parseAsync(substituted, context.position, true);
|
|
45483
|
-
return result;
|
|
45950
|
+
return rejectOmittedResult(result);
|
|
45484
45951
|
} catch (e) {
|
|
45485
45952
|
if (e instanceof FormulaError) return e;
|
|
45486
45953
|
throw e;
|
|
@@ -45541,35 +46008,58 @@ const LambdaFunctions = {
|
|
|
45541
46008
|
},
|
|
45542
46009
|
|
|
45543
46010
|
/**
|
|
45544
|
-
* Internal: the value spliced in for
|
|
45545
|
-
*
|
|
46011
|
+
* Internal: the value spliced in for a parameter the caller left out
|
|
46012
|
+
* (see the LAMBDA preprocessor / applyLambda).
|
|
45546
46013
|
*/
|
|
45547
46014
|
__RNC_OMITTED__: () => OMITTED,
|
|
45548
46015
|
|
|
45549
46016
|
/**
|
|
45550
46017
|
* Internal: `__RNC_CALL__(lambda, arg1, …)` — the preprocessor emits
|
|
45551
|
-
* this for an inline `LAMBDA(...)(...)`
|
|
45552
|
-
*
|
|
46018
|
+
* this for an inline `LAMBDA(...)(...)` with more arguments than
|
|
46019
|
+
* parameters, so the arity error surfaces as #VALUE!.
|
|
45553
46020
|
*/
|
|
45554
46021
|
__RNC_CALL__: async (context, lambdaArg, ...args) =>
|
|
45555
46022
|
applyLambda(context, unwrap(lambdaArg), args.map(unwrap)),
|
|
45556
46023
|
|
|
45557
46024
|
/**
|
|
45558
|
-
* ISOMITTED(argument) — TRUE when `argument` is
|
|
45559
|
-
*
|
|
46025
|
+
* ISOMITTED(argument) — TRUE when `argument` is a LAMBDA parameter the
|
|
46026
|
+
* caller did not supply (bracketed `[p]` or not), FALSE otherwise.
|
|
45560
46027
|
*/
|
|
45561
46028
|
ISOMITTED: (arg) => isOmittedValue(unwrap(arg)),
|
|
45562
46029
|
|
|
46030
|
+
/**
|
|
46031
|
+
* A well-formed `LET(name, value, ..., body)` never reaches the function
|
|
46032
|
+
* table — the LET preprocessor inlines it. One that does is malformed:
|
|
46033
|
+
* too few arguments is `#N/A` (the too-few-arguments contract shared with
|
|
46034
|
+
* the Rust engine), an even count or a non-name binding is `#VALUE!`.
|
|
46035
|
+
*/
|
|
46036
|
+
LET: (...args) => {
|
|
46037
|
+
if (args.length < 3) return FormulaError.NA;
|
|
46038
|
+
return FormulaError.VALUE;
|
|
46039
|
+
},
|
|
46040
|
+
|
|
46041
|
+
/**
|
|
46042
|
+
* Same for `LAMBDA(...)`: the preprocessor rewrites every LAMBDA with a
|
|
46043
|
+
* body, so only `LAMBDA()` (too few arguments, `#N/A`) and a body-less
|
|
46044
|
+
* `LAMBDA(x)` (a lambda value as the cell result, `#CALC!`) get here.
|
|
46045
|
+
*/
|
|
46046
|
+
LAMBDA: (...args) => {
|
|
46047
|
+
if (args.length === 0) return FormulaError.NA;
|
|
46048
|
+
return FormulaError.CALC;
|
|
46049
|
+
},
|
|
46050
|
+
|
|
45563
46051
|
/**
|
|
45564
46052
|
* MAP(array1, [array2, ...], lambda) — applies lambda to each set
|
|
45565
46053
|
* of corresponding elements. Returns a same-shape 2D array.
|
|
45566
46054
|
*/
|
|
45567
46055
|
MAP: async (context, ...args) => {
|
|
45568
|
-
if (args.length < 2) return FormulaError.
|
|
46056
|
+
if (args.length < 2) return FormulaError.NA;
|
|
45569
46057
|
const lambda = unwrap(args[args.length - 1]);
|
|
45570
46058
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
45571
46059
|
const arrays = args.slice(0, -1).map((a) => as2D(unwrap(a)));
|
|
45572
46060
|
if (arrays.length === 0) return FormulaError.VALUE;
|
|
46061
|
+
const arity = hofArityError(lambda, arrays.length);
|
|
46062
|
+
if (arity) return arity;
|
|
45573
46063
|
const rows = arrays[0].length;
|
|
45574
46064
|
const cols = arrays[0][0]?.length ?? 0;
|
|
45575
46065
|
const out = [];
|
|
@@ -45591,6 +46081,8 @@ const LambdaFunctions = {
|
|
|
45591
46081
|
REDUCE: async (context, initialArg, arrayArg, lambdaArg) => {
|
|
45592
46082
|
const lambda = unwrap(lambdaArg);
|
|
45593
46083
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
46084
|
+
const arity = hofArityError(lambda, 2);
|
|
46085
|
+
if (arity) return arity;
|
|
45594
46086
|
let acc = unwrap(initialArg);
|
|
45595
46087
|
const values = flatten2D(as2D(unwrap(arrayArg)));
|
|
45596
46088
|
for (const v of values) {
|
|
@@ -45608,6 +46100,8 @@ const LambdaFunctions = {
|
|
|
45608
46100
|
SCAN: async (context, initialArg, arrayArg, lambdaArg) => {
|
|
45609
46101
|
const lambda = unwrap(lambdaArg);
|
|
45610
46102
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
46103
|
+
const arity = hofArityError(lambda, 2);
|
|
46104
|
+
if (arity) return arity;
|
|
45611
46105
|
let acc = unwrap(initialArg);
|
|
45612
46106
|
const arr = as2D(unwrap(arrayArg));
|
|
45613
46107
|
const out = [];
|
|
@@ -45629,8 +46123,11 @@ const LambdaFunctions = {
|
|
|
45629
46123
|
* array), returns a column vector of results.
|
|
45630
46124
|
*/
|
|
45631
46125
|
BYROW: async (context, arrayArg, lambdaArg) => {
|
|
46126
|
+
if (lambdaArg === undefined) return FormulaError.NA;
|
|
45632
46127
|
const lambda = unwrap(lambdaArg);
|
|
45633
46128
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
46129
|
+
const arity = hofArityError(lambda, 1);
|
|
46130
|
+
if (arity) return arity;
|
|
45634
46131
|
const arr = as2D(unwrap(arrayArg));
|
|
45635
46132
|
const out = [];
|
|
45636
46133
|
for (const row of arr) {
|
|
@@ -45646,8 +46143,11 @@ const LambdaFunctions = {
|
|
|
45646
46143
|
* 1D array), returns a row vector of results.
|
|
45647
46144
|
*/
|
|
45648
46145
|
BYCOL: async (context, arrayArg, lambdaArg) => {
|
|
46146
|
+
if (lambdaArg === undefined) return FormulaError.NA;
|
|
45649
46147
|
const lambda = unwrap(lambdaArg);
|
|
45650
46148
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
46149
|
+
const arity = hofArityError(lambda, 1);
|
|
46150
|
+
if (arity) return arity;
|
|
45651
46151
|
const arr = as2D(unwrap(arrayArg));
|
|
45652
46152
|
const cols = arr[0]?.length ?? 0;
|
|
45653
46153
|
const out = [];
|
|
@@ -45665,8 +46165,11 @@ const LambdaFunctions = {
|
|
|
45665
46165
|
* lambda(r, c) for each cell. r, c are 1-indexed per Excel.
|
|
45666
46166
|
*/
|
|
45667
46167
|
MAKEARRAY: async (context, rowsArg, colsArg, lambdaArg) => {
|
|
46168
|
+
if (lambdaArg === undefined) return FormulaError.NA;
|
|
45668
46169
|
const lambda = unwrap(lambdaArg);
|
|
45669
46170
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
46171
|
+
const arity = hofArityError(lambda, 2);
|
|
46172
|
+
if (arity) return arity;
|
|
45670
46173
|
const rows = Math.floor(
|
|
45671
46174
|
FormulaHelpers.accept(rowsArg, Types.NUMBER, 0),
|
|
45672
46175
|
);
|
|
@@ -45715,6 +46218,8 @@ const LambdaFunctions = {
|
|
|
45715
46218
|
const valueFields = as2D(unwrap(args[1]));
|
|
45716
46219
|
const lambda = unwrap(args[2]);
|
|
45717
46220
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
46221
|
+
const arity = hofArityError(lambda, 1);
|
|
46222
|
+
if (arity) return arity;
|
|
45718
46223
|
const fieldHeaders = args[3] != null
|
|
45719
46224
|
? Math.floor(FormulaHelpers.accept(args[3], Types.NUMBER, 0))
|
|
45720
46225
|
: 0;
|
|
@@ -45763,6 +46268,8 @@ const LambdaFunctions = {
|
|
|
45763
46268
|
const valueFields = as2D(unwrap(args[2]));
|
|
45764
46269
|
const lambda = unwrap(args[3]);
|
|
45765
46270
|
if (!isLambdaValue(lambda)) return FormulaError.VALUE;
|
|
46271
|
+
const arity = hofArityError(lambda, 1);
|
|
46272
|
+
if (arity) return arity;
|
|
45766
46273
|
|
|
45767
46274
|
const numOpt = (i, d) =>
|
|
45768
46275
|
args[i] != null
|
|
@@ -46219,6 +46726,42 @@ let Utils$1 = class Utils {
|
|
|
46219
46726
|
return Prefix.unaryOp(prefixes, val, isArray);
|
|
46220
46727
|
}
|
|
46221
46728
|
|
|
46729
|
+
/**
|
|
46730
|
+
* Sign a numeric literal (`-1`, `+2` inside an array constant). Nothing
|
|
46731
|
+
* to dereference or await here, so this stays synchronous in both parse
|
|
46732
|
+
* modes.
|
|
46733
|
+
* @param {string} prefix - `+` or `-`
|
|
46734
|
+
* @param {number} number
|
|
46735
|
+
* @return {number}
|
|
46736
|
+
*/
|
|
46737
|
+
applyLiteralPrefix(prefix, number) {
|
|
46738
|
+
return this._applyPrefix([prefix], number, false);
|
|
46739
|
+
}
|
|
46740
|
+
|
|
46741
|
+
/**
|
|
46742
|
+
* Under `parseAsync` an array-constant item built from a sub-expression
|
|
46743
|
+
* (`{SUM(1,-2),3}`) is a pending Promise; an array must only ever carry
|
|
46744
|
+
* values, so settle them. A settled item may itself be a reference
|
|
46745
|
+
* (`{INDEX(A1:C5,1,1),2}`), which the sync arm of `constantArray`
|
|
46746
|
+
* dereferences before the Promise ever existed — do the same here so both
|
|
46747
|
+
* parse modes agree. Returns the array itself when nothing is pending.
|
|
46748
|
+
* @param {Array.<Array>} array
|
|
46749
|
+
* @param {{row: number, col: number, sheet: string}} position
|
|
46750
|
+
* @return {Array.<Array>|Promise.<Array.<Array>>}
|
|
46751
|
+
*/
|
|
46752
|
+
settleArrayItems(array, position) {
|
|
46753
|
+
const isPending = (item) =>
|
|
46754
|
+
item != null && typeof item.then === "function";
|
|
46755
|
+
if (!array.some((row) => row.some(isPending))) return array;
|
|
46756
|
+
const deref = (item) =>
|
|
46757
|
+
item && item.ref ? this.extractRefValue(item, position).val : item;
|
|
46758
|
+
return Promise.all(
|
|
46759
|
+
array.map((row) =>
|
|
46760
|
+
Promise.all(row).then((items) => items.map(deref))
|
|
46761
|
+
)
|
|
46762
|
+
);
|
|
46763
|
+
}
|
|
46764
|
+
|
|
46222
46765
|
async applyPrefixAsync(prefixes, value, position) {
|
|
46223
46766
|
const { val, isArray } = this.extractRefValue(await value, position);
|
|
46224
46767
|
return this._applyPrefix(prefixes, val, isArray);
|
|
@@ -46413,15 +46956,21 @@ let Utils$1 = class Utils {
|
|
|
46413
46956
|
}
|
|
46414
46957
|
}
|
|
46415
46958
|
|
|
46416
|
-
applyIntersect(refs) {
|
|
46959
|
+
applyIntersect(refs, position) {
|
|
46417
46960
|
// console.log('applyIntersect', refs);
|
|
46418
46961
|
if (this.isFormulaError(refs[0])) return refs[0];
|
|
46419
46962
|
if (!refs[0].ref) throw Error(`Expecting a reference, but got ${refs[0]}.`);
|
|
46420
46963
|
// a intersection will keep track of references, value won't be retrieved here.
|
|
46421
46964
|
let maxRow, maxCol, minRow, minCol, sheet, res; // index start from 1
|
|
46965
|
+
// An unqualified operand (`B:B`, sheet undefined) lives on the formula's sheet: it must
|
|
46966
|
+
// intersect an operand that names that sheet explicitly (`Sheet1!A1:C3`, or a defined name
|
|
46967
|
+
// — the host resolves names WITH their sheet). Only a genuinely different sheet is #NULL!.
|
|
46968
|
+
const currentSheet = (position ?? this.context.position)?.sheet;
|
|
46969
|
+
const sheetOf = (ref) => ref.sheet ?? currentSheet;
|
|
46422
46970
|
// first time setup
|
|
46423
46971
|
const ref = refs.shift().ref;
|
|
46424
46972
|
sheet = ref.sheet;
|
|
46973
|
+
const sheetKey = sheetOf(ref);
|
|
46425
46974
|
if (!ref.from) {
|
|
46426
46975
|
// check whole row/col reference
|
|
46427
46976
|
if (ref.row === undefined || ref.col === undefined) {
|
|
@@ -46455,10 +47004,11 @@ let Utils$1 = class Utils {
|
|
|
46455
47004
|
ref.row < minRow ||
|
|
46456
47005
|
ref.col > maxCol ||
|
|
46457
47006
|
ref.col < minCol ||
|
|
46458
|
-
|
|
47007
|
+
sheetKey !== sheetOf(ref)
|
|
46459
47008
|
) {
|
|
46460
47009
|
err = FormulaError.NULL;
|
|
46461
47010
|
}
|
|
47011
|
+
sheet ??= ref.sheet;
|
|
46462
47012
|
maxRow = minRow = ref.row;
|
|
46463
47013
|
maxCol = minCol = ref.col;
|
|
46464
47014
|
} else {
|
|
@@ -46472,10 +47022,11 @@ let Utils$1 = class Utils {
|
|
|
46472
47022
|
refMaxRow < minRow ||
|
|
46473
47023
|
refMinCol > maxCol ||
|
|
46474
47024
|
refMaxCol < minCol ||
|
|
46475
|
-
|
|
47025
|
+
sheetKey !== sheetOf(ref)
|
|
46476
47026
|
) {
|
|
46477
47027
|
err = FormulaError.NULL;
|
|
46478
47028
|
}
|
|
47029
|
+
sheet ??= ref.sheet;
|
|
46479
47030
|
// update
|
|
46480
47031
|
maxRow = Math.min(maxRow, refMaxRow);
|
|
46481
47032
|
minRow = Math.max(minRow, refMinRow);
|
|
@@ -47476,6 +48027,9 @@ class FormulaParser {
|
|
|
47476
48027
|
if (!this.keepLambdaResult && unwrapLambdaValue(result)) {
|
|
47477
48028
|
return FormulaError.CALC;
|
|
47478
48029
|
}
|
|
48030
|
+
// An inlined `LAMBDA(x,y,y)(1)` evaluates straight to the omitted
|
|
48031
|
+
// marker; only ISOMITTED may consume it, so as a result it is #VALUE!.
|
|
48032
|
+
result = rejectOmittedResult(result);
|
|
47479
48033
|
const type = typeof result;
|
|
47480
48034
|
// number
|
|
47481
48035
|
if (type === "number") {
|
|
@@ -48681,5 +49235,5 @@ class DepParser {
|
|
|
48681
49235
|
// Export a Set of implemented function names for O(1) lookup
|
|
48682
49236
|
const implementedFunctionNames = FormulaParser.getImplementedFunctionNames();
|
|
48683
49237
|
|
|
48684
|
-
export { Address, Criteria, DepParser, Factorials, FormulaError, FormulaHelpers, MAX_COLUMN, MAX_ROW, ReversedTypes, Types, UnknownChar, WildCard, allTokens, FormulaParser as default, implementedFunctionNames, lex, noop, numberToText, parseNumericText, tokenVocabulary$1 as tokenVocabulary, tolerantLex };
|
|
49238
|
+
export { Address, Criteria, DepParser, Factorials, FormulaError, FormulaHelpers, MAX_COLUMN, MAX_ROW, ReversedTypes, Types, UnknownChar, WildCard, allTokens, FormulaParser as default, implementedFunctionNames, isNestedAggregateFormula, lex, noop, numberToText, parseNumericText, tokenVocabulary$1 as tokenVocabulary, tolerantLex };
|
|
48685
49239
|
//# sourceMappingURL=index.js.map
|