@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 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
- if (type === Types.STRING) {
4679
- // Blank cells should behave like empty strings in text formulas
4680
- return "";
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, null, true, (item) => {
6351
- if (item !== undefined && item !== null && item !== "") {
6352
- result.push(String(item));
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
- // Split by each character in delimiter
6368
- const delimiterChars = delimiter.split("");
6369
- const regex = new RegExp(
6370
- "[" + delimiterChars.map((c) => "\\" + c).join("") + "]"
6371
- );
6372
- parts = text.split(regex);
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-detect when 0/null/missing).
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
- if (pairs.length < 4) throw FormulaError.NA;
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 === 1) {
12686
+ if (seasonality === 0) {
12386
12687
  period = 1; // No seasonality (linear Holt).
12387
- } else if (seasonality > 1) {
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, 0).sse;
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, 0).sse;
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 + the
12435
- // `horizon`-step-ahead forecast. period=1 collapses to Holt linear
12436
- // (no seasonal component).
12437
- const fitHoltWinters = (y, period, horizon) => {
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, horizon);
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
- const runHoltWinters = (y, period, alpha, beta, gamma, horizon) => {
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 level = mean of first cycle.
12467
- // Initial trend = average of per-step changes between first
12468
- // and second cycle, normalized by m.
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 forecast = [];
12500
- for (let h = 1; h <= horizon; h++) {
12501
- const seasonComp = m > 1 ? seasonal[(n + h - 1) % m] : 0;
12502
- forecast.push(level + h * trend + seasonComp);
12503
- }
12504
- return { fitted, forecast, sse: count > 0 ? sse / count : Infinity };
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. Passing `seasonality = 1` forces "no seasonality" (Holt
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 == null
13279
- ? 1
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 { fitted: _f, forecast } = fitHoltWinters(y, period, horizon);
13293
- return forecast[horizon - 1];
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 == null
13305
- ? 1
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
- const { fitted, forecast: _fc } = fitHoltWinters(y, period, horizon);
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
- return z * rmse * Math.sqrt(horizon);
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
- const { y } = forecastEtsPrep(ys, xs, 0);
13330
- return detectSeasonality(y);
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 == null
13339
- ? 1
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, 0);
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.isArray;
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
- if (significance === 0) return 0;
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
- // always round up
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 (typeof obj != "object" || Array.isArray(obj)) throw FormulaError.VALUE;
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 (typeof obj != "object" || Array.isArray(obj)) throw FormulaError.VALUE;
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
- const sorted = ReferenceFunctions.SORT(
21234
- context,
21235
- range,
21236
- sortColumn,
21237
- isAscending,
21238
- false
21239
- );
21240
- return sorted.slice(0, n);
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
- return this.utils.toArray(arr);
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.applyPrefix([prefix], number, $.position)
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 an optional parameter the caller left out. */
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 (arity mismatch) so the
45186
- * evaluator reports `#VALUE!` like Excel instead of a parse error.
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 syntax. The lexer sees it as a
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, or arity mismatch).
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
- callArgSpans.length < requiredCount(params) ||
45250
- callArgSpans.length > params.length
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. An omitted
45273
- // optional parameter binds to the omitted marker so ISOMITTED can
45274
- // see it.
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 an optional `[param]` the caller left out. Only
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 parameter, `name` → required. */
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
- if (
45463
- args.length < requiredParamCount(lambda) ||
45464
- args.length > lambda.params.length
45465
- ) {
45466
- return FormulaError.VALUE;
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
- // An omitted optional parameter binds to the omitted marker so
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 an optional `[param]` the caller
45549
- * left out (see the LAMBDA preprocessor / applyLambda).
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(...)(...)` whose argument count does not
45556
- * match the parameter list, so the arity error surfaces as #VALUE!.
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 an optional LAMBDA
45563
- * parameter the caller did not supply, FALSE otherwise.
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.VALUE;
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
- sheet !== ref.sheet
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
- sheet !== ref.sheet
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;