@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/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
- if (type === Types.STRING) {
4675
- // Blank cells should behave like empty strings in text formulas
4676
- return "";
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, null, true, (item) => {
6347
- if (item !== undefined && item !== null && item !== "") {
6348
- result.push(String(item));
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
- // Split by each character in delimiter
6364
- const delimiterChars = delimiter.split("");
6365
- const regex = new RegExp(
6366
- "[" + delimiterChars.map((c) => "\\" + c).join("") + "]"
6367
- );
6368
- parts = text.split(regex);
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-detect when 0/null/missing).
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
- if (pairs.length < 4) throw FormulaError.NA;
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 === 1) {
12682
+ if (seasonality === 0) {
12382
12683
  period = 1; // No seasonality (linear Holt).
12383
- } else if (seasonality > 1) {
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, 0).sse;
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, 0).sse;
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 + the
12431
- // `horizon`-step-ahead forecast. period=1 collapses to Holt linear
12432
- // (no seasonal component).
12433
- const fitHoltWinters = (y, period, horizon) => {
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, horizon);
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
- const runHoltWinters = (y, period, alpha, beta, gamma, horizon) => {
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 level = mean of first cycle.
12463
- // Initial trend = average of per-step changes between first
12464
- // and second cycle, normalized by m.
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 forecast = [];
12496
- for (let h = 1; h <= horizon; h++) {
12497
- const seasonComp = m > 1 ? seasonal[(n + h - 1) % m] : 0;
12498
- forecast.push(level + h * trend + seasonComp);
12499
- }
12500
- return { fitted, forecast, sse: count > 0 ? sse / count : Infinity };
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. Passing `seasonality = 1` forces "no seasonality" (Holt
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 == null
13275
- ? 1
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 { fitted: _f, forecast } = fitHoltWinters(y, period, horizon);
13289
- return forecast[horizon - 1];
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 == null
13301
- ? 1
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
- const { fitted, forecast: _fc } = fitHoltWinters(y, period, horizon);
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
- return z * rmse * Math.sqrt(horizon);
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
- const { y } = forecastEtsPrep(ys, xs, 0);
13326
- return detectSeasonality(y);
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 == null
13335
- ? 1
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, 0);
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.isArray;
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
- if (significance === 0) return 0;
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
- // always round up
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 (typeof obj != "object" || Array.isArray(obj)) throw FormulaError.VALUE;
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 (typeof obj != "object" || Array.isArray(obj)) throw FormulaError.VALUE;
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
- const sorted = ReferenceFunctions.SORT(
21230
- context,
21231
- range,
21232
- sortColumn,
21233
- isAscending,
21234
- false
21235
- );
21236
- return sorted.slice(0, n);
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
- return this.utils.toArray(arr);
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.applyPrefix([prefix], number, $.position)
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 an optional parameter the caller left out. */
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 (arity mismatch) so the
45182
- * evaluator reports `#VALUE!` like Excel instead of a parse error.
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 syntax. The lexer sees it as a
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, or arity mismatch).
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
- callArgSpans.length < requiredCount(params) ||
45246
- callArgSpans.length > params.length
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. An omitted
45269
- // optional parameter binds to the omitted marker so ISOMITTED can
45270
- // see it.
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 an optional `[param]` the caller left out. Only
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 parameter, `name` → required. */
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
- if (
45459
- args.length < requiredParamCount(lambda) ||
45460
- args.length > lambda.params.length
45461
- ) {
45462
- return FormulaError.VALUE;
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
- // An omitted optional parameter binds to the omitted marker so
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 an optional `[param]` the caller
45545
- * left out (see the LAMBDA preprocessor / applyLambda).
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(...)(...)` whose argument count does not
45552
- * match the parameter list, so the arity error surfaces as #VALUE!.
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 an optional LAMBDA
45559
- * parameter the caller did not supply, FALSE otherwise.
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.VALUE;
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
- sheet !== ref.sheet
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
- sheet !== ref.sheet
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