@rowsncolumns/fast-formula-parser 6.2.36 → 6.2.37

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
@@ -3965,6 +3990,148 @@ const DEFAULT_SEPARATORS = { decimal: ".", group: "," };
3965
3990
  const separatorsFor = (locale) => {
3966
3991
  return DEFAULT_SEPARATORS;
3967
3992
  };
3993
+ /**
3994
+ * Rewrite the `[$symbol-LCID]` tokens of one section through `rewrite`,
3995
+ * which receives the bracket content (`€-407`) and returns the new content,
3996
+ * or an empty string to drop the token. Quoted literals, escapes and other
3997
+ * bracket directives are left as written.
3998
+ */
3999
+ const rewriteSectionLocaleTokens = (section, rewrite) => {
4000
+ let out = "";
4001
+ let inQuotes = false;
4002
+ let inBrackets = false;
4003
+ for (let i = 0; i < section.length; i++) {
4004
+ const char = section[i];
4005
+ if (inQuotes) {
4006
+ inQuotes = char !== '"';
4007
+ }
4008
+ else if (inBrackets) {
4009
+ inBrackets = char !== "]";
4010
+ }
4011
+ else if (char === '"') {
4012
+ inQuotes = true;
4013
+ }
4014
+ else if (char === "[") {
4015
+ const end = section[i + 1] === "$" ? section.indexOf("]", i) : -1;
4016
+ if (end > 0) {
4017
+ const token = rewrite(section.slice(i + 1, end));
4018
+ if (token)
4019
+ out += `[${token}]`;
4020
+ i = end;
4021
+ continue;
4022
+ }
4023
+ inBrackets = true;
4024
+ }
4025
+ else if (char === "\\" || char === "_" || char === "*") {
4026
+ out += char + (section[i + 1] ?? "");
4027
+ i++;
4028
+ continue;
4029
+ }
4030
+ out += char;
4031
+ }
4032
+ return out;
4033
+ };
4034
+ /** `[$€-407]` → `[$€]`; a bare `[$-407]` goes away. */
4035
+ const currencySymbolOnly = (token) => {
4036
+ const dash = token.indexOf("-");
4037
+ const symbol = dash < 0 ? token : token.slice(0, dash);
4038
+ return symbol.length > 1 ? symbol : "";
4039
+ };
4040
+ const separatorLocales = new Map();
4041
+ /**
4042
+ * Companion numfmt locale carrying `locale`'s calendar names and AM/PM with
4043
+ * the workbook's decimal / group separators, so a date section pinned to
4044
+ * `[$-407]` renders German names but a workbook decimal mark in `ss.00`.
4045
+ * Returns `locale` itself when its separators already match.
4046
+ */
4047
+ const getWorkbookSeparatorLocale = (locale, separators) => {
4048
+ const key = `${locale}\u0000${separators.decimal}\u0000${separators.group}`;
4049
+ const cached = separatorLocales.get(key);
4050
+ if (cached !== undefined)
4051
+ return cached;
4052
+ let overlay = locale;
4053
+ try {
4054
+ const base = getLocale(locale);
4055
+ if (base &&
4056
+ (base.decimal !== separators.decimal || base.group !== separators.group)) {
4057
+ // numfmt locale ids are `[a-z\d]+` with one optional `_` part.
4058
+ const hex = (text) => [...text].map((c) => c.codePointAt(0).toString(16)).join("x");
4059
+ overlay = `sep${hex(separators.decimal)}x${hex(separators.group)}_${locale.replace(/[^a-z\d]/gi, "")}`;
4060
+ addLocale({ ...base, decimal: separators.decimal, group: separators.group }, overlay);
4061
+ }
4062
+ }
4063
+ catch {
4064
+ overlay = locale;
4065
+ }
4066
+ separatorLocales.set(key, overlay);
4067
+ return overlay;
4068
+ };
4069
+ /**
4070
+ * Re-point a date section's `[$symbol-LCID]` token at the companion locale
4071
+ * with the workbook's separators (`$-407` → `$-sep2ex2c_407`). An LCID numfmt
4072
+ * does not know or one carrying calendar bits stays as written.
4073
+ */
4074
+ const workbookSeparatorLocaleToken = (separators) => (token) => {
4075
+ const dash = token.indexOf("-");
4076
+ if (dash < 0)
4077
+ return token;
4078
+ const code = token.slice(dash + 1);
4079
+ const wincode = Number.parseInt(code, 16);
4080
+ if (Number.isFinite(wincode) && wincode & 0xff0000)
4081
+ return token;
4082
+ if (!getLocale(code))
4083
+ return token;
4084
+ return `${token.slice(0, dash + 1)}${getWorkbookSeparatorLocale(code, separators)}`;
4085
+ };
4086
+ const isDateSection = (section) => {
4087
+ try {
4088
+ return isDateFormat(section);
4089
+ }
4090
+ catch {
4091
+ return false;
4092
+ }
4093
+ };
4094
+ /**
4095
+ * Excel's `[$symbol-LCID]` token selects the currency symbol and the
4096
+ * language of day/month names and AM/PM; digit grouping and the decimal
4097
+ * separator follow the workbook locale (`[$€-407] #,##0.00` is
4098
+ * `€ 1,234.50` on a US host). numfmt applies the token's locale to the whole
4099
+ * section, separators included, so the locale part is dropped from every
4100
+ * section that is not a date (number, scientific, fraction, text, …); a
4101
+ * date section keeps the token for its names, re-pointed at a companion
4102
+ * locale that carries the workbook's separators for fractional seconds.
4103
+ * Returns `undefined` when nothing had to change.
4104
+ */
4105
+ const withWorkbookSeparators = (pattern, locale) => {
4106
+ if (!pattern.includes("[$"))
4107
+ return undefined;
4108
+ const sections = splitFormatSections(pattern);
4109
+ let changed = false;
4110
+ let dateTokens;
4111
+ const rewritten = sections.map((section) => {
4112
+ if (!section.includes("[$"))
4113
+ return section;
4114
+ let rewrite = currencySymbolOnly;
4115
+ if (isDateSection(section)) {
4116
+ dateTokens ??= workbookSeparatorLocaleToken(separatorsFor());
4117
+ rewrite = dateTokens;
4118
+ }
4119
+ const stripped = rewriteSectionLocaleTokens(section, rewrite);
4120
+ changed ||= stripped !== section;
4121
+ return stripped;
4122
+ });
4123
+ return changed ? rewritten.join(";") : undefined;
4124
+ };
4125
+ const workbookSeparatorPatternCache = new Map();
4126
+ const withWorkbookSeparatorsCached = (code, locale) => {
4127
+ const key = `${""}\u0000${code}`;
4128
+ let rewritten = workbookSeparatorPatternCache.get(key);
4129
+ if (rewritten === undefined) {
4130
+ rewritten = withWorkbookSeparators(code) ?? code;
4131
+ workbookSeparatorPatternCache.set(key, rewritten);
4132
+ }
4133
+ return rewritten;
4134
+ };
3968
4135
  const ssfFormat = (pattern, value, locale) => {
3969
4136
  try {
3970
4137
  let shortAmPm = shortAmPmPatternCache.get(pattern);
@@ -3972,7 +4139,7 @@ const ssfFormat = (pattern, value, locale) => {
3972
4139
  shortAmPm = withShortAmPmMarker(pattern);
3973
4140
  shortAmPmPatternCache.set(pattern, shortAmPm);
3974
4141
  }
3975
- const code = shortAmPm ?? pattern;
4142
+ const code = withWorkbookSeparatorsCached(shortAmPm ?? pattern, locale);
3976
4143
  let patternLocale = localeForPattern(pattern, locale);
3977
4144
  if (shortAmPm !== undefined && patternLocale) {
3978
4145
  patternLocale = getShortAmPmLocale(patternLocale);
@@ -4675,10 +4842,12 @@ class FormulaHelpersClass {
4675
4842
  !isRaw && Object.prototype.hasOwnProperty.call(param, "value");
4676
4843
  if (hasValueProp) {
4677
4844
  if (param.value == null) {
4678
- if (type === Types.STRING) {
4679
- // Blank cells should behave like empty strings in text formulas
4680
- return "";
4681
- }
4845
+ // A blank cell is not an omitted argument (only omission takes
4846
+ // `defValue`): Excel reads it as "" in a text slot, 0 in a number
4847
+ // slot and FALSE in a logical slot.
4848
+ if (type === Types.STRING) return "";
4849
+ if (type === Types.NUMBER || type === Types.NUMBER_NO_BOOLEAN) return 0;
4850
+ if (type === Types.BOOLEAN) return false;
4682
4851
  param = param.value;
4683
4852
  } else {
4684
4853
  param = param.value;
@@ -4758,6 +4927,7 @@ class FormulaHelpersClass {
4758
4927
  * @param range2
4759
4928
  */
4760
4929
  retrieveRanges(context, range1, range2) {
4930
+ if (range1 === undefined) throw FormulaError.ARG_MISSING([Types.ARRAY]);
4761
4931
  // process args
4762
4932
  range2 = Address.extend(range1, range2);
4763
4933
 
@@ -4774,6 +4944,9 @@ class FormulaHelpersClass {
4774
4944
  }
4775
4945
 
4776
4946
  retrieveArg(context, arg) {
4947
+ // `null` is an explicitly skipped argument (`SUMIF(A1:A5,,B1:B5)`);
4948
+ // `undefined` is one the call never supplied — too few arguments.
4949
+ if (arg === undefined) throw FormulaError.ARG_MISSING([Types.ARRAY]);
4777
4950
  if (arg === null) return { value: 0, isArray: false, omitted: true };
4778
4951
  const res = context.utils.extractRefValue(arg);
4779
4952
  return { value: res.val, isArray: res.isArray, ref: arg.ref };
@@ -6343,33 +6516,48 @@ const TextFunctions = {
6343
6516
  return text.slice(0, m.index) + expanded + text.slice(m.index + m[0].length);
6344
6517
  },
6345
6518
 
6519
+ /**
6520
+ * JOIN(delimiter, value_or_array1, [value_or_array2, …]) — Google Sheets.
6521
+ * Ranges / arrays contribute every element; blanks and empty text are
6522
+ * skipped; values spell as TEXTJOIN spells them (TRUE, 15-digit numbers) and
6523
+ * an error among the values propagates. The rnc engine mirrors these rules.
6524
+ */
6346
6525
  JOIN: (delimiter, ...values) => {
6347
6526
  delimiter = H$a.accept(delimiter, Types.STRING);
6348
6527
  const result = [];
6349
6528
 
6350
- H$a.flattenParams(values, null, true, (item) => {
6351
- if (item !== undefined && item !== null && item !== "") {
6352
- result.push(String(item));
6353
- }
6529
+ H$a.flattenParams(values, Types.STRING, true, (item) => {
6530
+ if (item === undefined || item === null) return;
6531
+ const str = H$a.accept(item, Types.STRING);
6532
+ if (str !== "") result.push(str);
6354
6533
  });
6355
6534
 
6356
6535
  return result.join(delimiter);
6357
6536
  },
6358
6537
 
6538
+ /**
6539
+ * SPLIT(text, delimiter, [split_by_each=TRUE], [remove_empty_text=TRUE]) —
6540
+ * Google Sheets. Pieces stay text (no number coercion); an empty delimiter
6541
+ * is #VALUE!; when every piece is removed the result is empty text. The
6542
+ * rnc engine mirrors these rules.
6543
+ */
6359
6544
  SPLIT: (text, delimiter, splitByEach, removeEmpty) => {
6360
6545
  text = H$a.accept(text, Types.STRING);
6361
6546
  delimiter = H$a.accept(delimiter, Types.STRING);
6362
6547
  splitByEach = H$a.accept(splitByEach, Types.BOOLEAN, true);
6363
6548
  removeEmpty = H$a.accept(removeEmpty, Types.BOOLEAN, true);
6549
+ if (delimiter === "") throw FormulaError.VALUE;
6364
6550
 
6365
6551
  let parts;
6366
6552
  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);
6553
+ // Every character of the delimiter is a separator — a literal character,
6554
+ // never a RegExp class ("d" must not become \d, "b" must not become \b).
6555
+ const separators = new Set(delimiter);
6556
+ parts = [""];
6557
+ for (const ch of text) {
6558
+ if (separators.has(ch)) parts.push("");
6559
+ else parts[parts.length - 1] += ch;
6560
+ }
6373
6561
  } else {
6374
6562
  // Split by the entire delimiter string
6375
6563
  parts = text.split(delimiter);
@@ -6378,6 +6566,7 @@ const TextFunctions = {
6378
6566
  if (removeEmpty) {
6379
6567
  parts = parts.filter((part) => part !== "");
6380
6568
  }
6569
+ if (parts.length === 0) return "";
6381
6570
 
6382
6571
  // Return as a single row array
6383
6572
  return [parts];
@@ -12353,15 +12542,48 @@ const collectNumericValues = (array) => {
12353
12542
  // FORECAST.ETS helpers — Holt-Winters AAA implementation.
12354
12543
  // ---------------------------------------------------------------------
12355
12544
 
12545
+ // Excel's `seasonality` argument: 1 (the default) auto-detects the
12546
+ // period, 0 fits without a seasonal component, 2..8760 forces the
12547
+ // period; anything else is #NUM!. Fractions are truncated.
12548
+ const ETS_MAX_SEASONALITY = 8760;
12549
+ const forecastEtsSeasonality = (seasonalityArg) => {
12550
+ if (seasonalityArg == null) return 1;
12551
+ const value = H$8.accept(seasonalityArg, Types.NUMBER, 1);
12552
+ if (value == null) return 1;
12553
+ const seasonality = Math.floor(value);
12554
+ if (seasonality < 0 || seasonality > ETS_MAX_SEASONALITY) throw FormulaError.NUM;
12555
+ return seasonality;
12556
+ };
12557
+
12558
+ // `data_completion` must be 0 or 1 and `aggregation` 1..7 (AVERAGE,
12559
+ // COUNT, COUNTA, MAX, MEDIAN, MIN, SUM), otherwise #NUM!. Neither
12560
+ // changes the fit: the timeline is used as given (no gap filling,
12561
+ // duplicates are not aggregated).
12562
+ const forecastEtsValidateOptions = (dataCompletionArg, aggregationArg) => {
12563
+ if (dataCompletionArg != null) {
12564
+ const completion = Math.floor(H$8.accept(dataCompletionArg, Types.NUMBER, 1));
12565
+ if (completion !== 0 && completion !== 1) throw FormulaError.NUM;
12566
+ }
12567
+ if (aggregationArg != null) {
12568
+ const aggregation = Math.floor(H$8.accept(aggregationArg, Types.NUMBER, 1));
12569
+ if (aggregation < 1 || aggregation > 7) throw FormulaError.NUM;
12570
+ }
12571
+ };
12572
+
12356
12573
  // Pair up timeline + values, sort by timeline, infer the constant
12357
12574
  // step (Excel requires a uniformly-spaced timeline), and resolve
12358
- // the seasonality parameter (auto-detect when 0/null/missing).
12575
+ // the seasonality parameter (Excel spelling: 1 auto-detects, 0 is no
12576
+ // seasonality, n > 1 is the period).
12359
12577
  const forecastEtsPrep = (ys, xs, seasonality) => {
12360
12578
  if (xs.length !== ys.length) throw FormulaError.NA;
12361
12579
  const pairs = [];
12362
12580
  for (let i = 0; i < xs.length; i++) {
12363
12581
  const x = xs[i];
12364
12582
  const yv = ys[i];
12583
+ // An error anywhere in the series is the result (Excel and the Rust
12584
+ // port's `series_number`); other non-numbers (text, blanks) are skipped.
12585
+ if (x instanceof FormulaError) throw x;
12586
+ if (yv instanceof FormulaError) throw yv;
12365
12587
  if (typeof x !== "number") continue;
12366
12588
  if (typeof yv !== "number") continue;
12367
12589
  pairs.push([x, yv]);
@@ -12382,12 +12604,12 @@ const forecastEtsPrep = (ys, xs, seasonality) => {
12382
12604
  diffs.sort((a, b) => a - b);
12383
12605
  const step = diffs[Math.floor(diffs.length / 2)];
12384
12606
  let period;
12385
- if (seasonality === 1) {
12607
+ if (seasonality === 0) {
12386
12608
  period = 1; // No seasonality (linear Holt).
12387
- } else if (seasonality > 1) {
12388
- period = seasonality;
12389
- } else {
12609
+ } else if (seasonality === 1) {
12390
12610
  period = detectSeasonality(y);
12611
+ } else {
12612
+ period = seasonality;
12391
12613
  }
12392
12614
  return { y, lastX: sortedX[sortedX.length - 1], step, period };
12393
12615
  };
@@ -13267,20 +13489,18 @@ const DistributionFunctions = {
13267
13489
  // trend. We use the same idea — scan candidate periods 2..N/2,
13268
13490
  // capped at 12 (Excel's default cap for monthly data), and pick
13269
13491
  // the one with the lowest in-sample SSE for the fitted Holt-Winters
13270
- // model. Passing `seasonality = 1` forces "no seasonality" (Holt
13271
- // linear).
13492
+ // model. `seasonality` follows Excel: 1 (default) auto-detects, 0
13493
+ // forces "no seasonality" (Holt linear), n > 1 is the period. The
13494
+ // Rust engine (formualizer `stats/forecast_ets.rs`) is a port of this
13495
+ // code, so both engines agree to 1e-9 on the same inputs.
13272
13496
  // -------------------------------------------------------------------
13273
13497
 
13274
13498
  'FORECAST.ETS': (targetDate, values, timeline, seasonalityArg, dataCompletionArg, aggregationArg) => {
13275
13499
  const target = H$8.accept(targetDate, Types.NUMBER);
13276
13500
  const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
13277
13501
  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).
13502
+ const seasonality = forecastEtsSeasonality(seasonalityArg);
13503
+ forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
13284
13504
  const { y, lastX, step, period } = forecastEtsPrep(ys, xs, seasonality);
13285
13505
  if (target < lastX) {
13286
13506
  // Excel returns #NUM! for in-history targets in FORECAST.ETS
@@ -13293,7 +13513,7 @@ const DistributionFunctions = {
13293
13513
  return forecast[horizon - 1];
13294
13514
  },
13295
13515
 
13296
- 'FORECAST.ETS.CONFINT': (targetDate, values, timeline, confidenceArg, seasonalityArg) => {
13516
+ 'FORECAST.ETS.CONFINT': (targetDate, values, timeline, confidenceArg, seasonalityArg, dataCompletionArg, aggregationArg) => {
13297
13517
  const target = H$8.accept(targetDate, Types.NUMBER);
13298
13518
  const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
13299
13519
  const xs = H$8.accept(timeline, Types.ARRAY, undefined, true, true);
@@ -13301,9 +13521,8 @@ const DistributionFunctions = {
13301
13521
  ? 0.95
13302
13522
  : H$8.accept(confidenceArg, Types.NUMBER, 0.95);
13303
13523
  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));
13524
+ const seasonality = forecastEtsSeasonality(seasonalityArg);
13525
+ forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
13307
13526
  const { y, lastX, step, period } = forecastEtsPrep(ys, xs, seasonality);
13308
13527
  if (target < lastX) throw FormulaError.NUM;
13309
13528
  const horizon = Math.max(1, Math.round((target - lastX) / step));
@@ -13323,21 +13542,21 @@ const DistributionFunctions = {
13323
13542
  return z * rmse * Math.sqrt(horizon);
13324
13543
  },
13325
13544
 
13326
- 'FORECAST.ETS.SEASONALITY': (values, timeline) => {
13545
+ 'FORECAST.ETS.SEASONALITY': (values, timeline, dataCompletionArg, aggregationArg) => {
13327
13546
  const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
13328
13547
  const xs = H$8.accept(timeline, Types.ARRAY, undefined, true, true);
13329
- const { y } = forecastEtsPrep(ys, xs, 0);
13330
- return detectSeasonality(y);
13548
+ forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
13549
+ const { period } = forecastEtsPrep(ys, xs, 1);
13550
+ return period;
13331
13551
  },
13332
13552
 
13333
- 'FORECAST.ETS.STAT': (values, timeline, statTypeArg, seasonalityArg) => {
13553
+ 'FORECAST.ETS.STAT': (values, timeline, statTypeArg, seasonalityArg, dataCompletionArg, aggregationArg) => {
13334
13554
  const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
13335
13555
  const xs = H$8.accept(timeline, Types.ARRAY, undefined, true, true);
13336
13556
  const statType = Math.floor(H$8.accept(statTypeArg, Types.NUMBER));
13337
13557
  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));
13558
+ const seasonality = forecastEtsSeasonality(seasonalityArg);
13559
+ forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
13341
13560
  const { y, period } = forecastEtsPrep(ys, xs, seasonality);
13342
13561
  const { alpha, beta, gamma, fitted } = fitHoltWinters(y, period, 0);
13343
13562
  // Excel STAT codes:
@@ -14424,6 +14643,9 @@ const DistributionFunctions = {
14424
14643
  const prediction = truncated.reduce((sum, value, idx) => sum + value * slopes[idx], 0) + intercept;
14425
14644
  return [prediction];
14426
14645
  });
14646
+ // A single new_x is a plain number in Excel (and in the Rust
14647
+ // engine); a 1x1 array would anchor an array formula on the cell.
14648
+ if (result.length === 1) return result[0][0];
14427
14649
  return result;
14428
14650
  },
14429
14651
 
@@ -15406,7 +15628,7 @@ const StatisticalFunctions = {
15406
15628
  COUNTIF: (range, criteria) => {
15407
15629
  // do not flatten the array
15408
15630
  range = H$7.accept(range, Types.ARRAY, undefined, false, true);
15409
- const isCriteriaArray = criteria.isArray;
15631
+ const isCriteriaArray = criteria?.isArray;
15410
15632
  criteria = H$7.accept(criteria);
15411
15633
 
15412
15634
  const countMatching = (parsed) => {
@@ -16721,7 +16943,15 @@ const MathFunctions = {
16721
16943
  FLOOR: (number, significance) => {
16722
16944
  number = H$6.accept(number, Types.NUMBER);
16723
16945
  significance = H$6.accept(significance, Types.NUMBER, 1);
16724
- if (significance === 0) return 0;
16946
+ // Excel: a zero significance is #DIV/0! for FLOOR of a non-zero number
16947
+ // (FLOOR(0,0) is 0; CEILING, FLOOR.MATH and FLOOR.PRECISE return 0); a
16948
+ // positive number with a negative significance is #NUM!; a negative
16949
+ // number rounds away from zero unless the significance is negative too
16950
+ // (FLOOR(-2.5,2) = -4, FLOOR(-2.5,-2) = -2).
16951
+ if (significance === 0) {
16952
+ if (number === 0) return 0;
16953
+ throw FormulaError.DIV0;
16954
+ }
16725
16955
  if (number > 0 && significance < 0) throw FormulaError.NUM;
16726
16956
  const absSignificance = Math.abs(significance);
16727
16957
  const quotient = excelQuotient(Math.abs(number), absSignificance);
@@ -16746,6 +16976,7 @@ const MathFunctions = {
16746
16976
  // mode can be 0 or any other number, 0 means away from zero
16747
16977
  // the official documentation seems wrong
16748
16978
  mode = H$6.accept(mode, Types.NUMBER, 0);
16979
+ if (significance === 0) return 0;
16749
16980
  // The Mode argument does not affect positive numbers.
16750
16981
  if (mode === 0 || number >= 0) {
16751
16982
  // away from zero
@@ -16758,7 +16989,8 @@ const MathFunctions = {
16758
16989
  "FLOOR.PRECISE": (number, significance) => {
16759
16990
  number = H$6.accept(number, Types.NUMBER);
16760
16991
  significance = H$6.accept(significance, Types.NUMBER, 1);
16761
- // always round up
16992
+ // A zero significance is 0 (Excel); otherwise the mathematical floor.
16993
+ if (significance === 0) return 0;
16762
16994
  return MathFunctions.FLOOR(number, Math.abs(significance));
16763
16995
  },
16764
16996
 
@@ -17264,6 +17496,22 @@ const MathFunctions = {
17264
17496
  return result;
17265
17497
  },
17266
17498
 
17499
+ /**
17500
+ * PERCENTOF(data_subset, data_all) — SUM(data_subset) / SUM(data_all) under
17501
+ * SUM's rules (text / logical / blank cells inside ranges are skipped, error
17502
+ * values propagate, a non-numeric scalar is #VALUE!); a zero whole is
17503
+ * #DIV/0!. Excel 365's helper for GROUPBY / PIVOTBY percent-of-total.
17504
+ */
17505
+ PERCENTOF: (dataSubset, dataAll) => {
17506
+ if (H$6.isOmitted(dataSubset) || H$6.isOmitted(dataAll)) {
17507
+ throw FormulaError.ARG_MISSING([Types.NUMBER]);
17508
+ }
17509
+ const subset = MathFunctions.SUM(dataSubset);
17510
+ const all = MathFunctions.SUM(dataAll);
17511
+ if (all === 0) throw FormulaError.DIV0;
17512
+ return subset / all;
17513
+ },
17514
+
17267
17515
  /**
17268
17516
  * This functions requires instance of {@link FormulaParser}.
17269
17517
  */
@@ -19850,6 +20098,10 @@ const ReferenceFunctions = {
19850
20098
  // Retrieve actual data to determine effective column count
19851
20099
  const { val: data } = context.utils.extractRefValue(obj, context.position);
19852
20100
  const colCount = Array.isArray(data) && Array.isArray(data[0]) ? data[0].length : 1;
20101
+ // One column (`COLUMN(C:C)`, `COLUMN(B2:B5)`) is a plain number, as
20102
+ // Excel shows it and as the Rust engine returns it; a 1x1 array would
20103
+ // make the host anchor an array formula on the cell.
20104
+ if (colCount <= 1) return fromCol;
19853
20105
  const result = [];
19854
20106
  for (let i = 0; i < colCount; i++) {
19855
20107
  result.push(fromCol + i);
@@ -19866,7 +20118,11 @@ const ReferenceFunctions = {
19866
20118
  if (obj == null) {
19867
20119
  throw Error("COLUMNS requires one argument");
19868
20120
  }
19869
- if (typeof obj != "object" || Array.isArray(obj)) throw FormulaError.VALUE;
20121
+ if (obj instanceof FormulaError) throw obj;
20122
+ // An array (`{1,2;3,4}`, `SEQUENCE(3,2)`, `RANDARRAY(3,2)`, a SORT/FILTER
20123
+ // result) reports its width; a scalar is a 1×1 array (`COLUMNS(5)` = 1).
20124
+ if (Array.isArray(obj)) return obj[0]?.length ?? 0;
20125
+ if (typeof obj != "object") return 1;
19870
20126
  if (H$2.isCellRef(obj)) {
19871
20127
  return 1;
19872
20128
  } else if (H$2.isRangeRef(obj)) {
@@ -19917,6 +20173,7 @@ const ReferenceFunctions = {
19917
20173
 
19918
20174
  // Special
19919
20175
  INDEX: (context, ranges, rowNum, colNum, areaNum) => {
20176
+ if (ranges === undefined) throw FormulaError.ARG_MISSING([Types.ARRAY]);
19920
20177
  // retrieve values
19921
20178
  const isColNumMissing = colNum == null;
19922
20179
  const isRowNumMissing = rowNum == null;
@@ -20299,6 +20556,8 @@ const ReferenceFunctions = {
20299
20556
  // Retrieve actual data to determine effective row count
20300
20557
  const { val: data } = context.utils.extractRefValue(obj, context.position);
20301
20558
  const rowCount = Array.isArray(data) ? data.length : 1;
20559
+ // One row (`ROW(3:3)`, `ROW(B2:C2)`) is a plain number; see COLUMN.
20560
+ if (rowCount <= 1) return fromRow;
20302
20561
  const result = [];
20303
20562
  for (let i = 0; i < rowCount; i++) {
20304
20563
  result.push([fromRow + i]); // Vertical array (column vector)
@@ -20315,7 +20574,11 @@ const ReferenceFunctions = {
20315
20574
  if (obj == null) {
20316
20575
  throw Error("ROWS requires one argument");
20317
20576
  }
20318
- if (typeof obj != "object" || Array.isArray(obj)) throw FormulaError.VALUE;
20577
+ if (obj instanceof FormulaError) throw obj;
20578
+ // An array (`{1,2;3,4}`, `SEQUENCE(3,2)`, `RANDARRAY(3,2)`, a SORT/FILTER
20579
+ // result) reports its height; a scalar is a 1×1 array (`ROWS(5)` = 1).
20580
+ if (Array.isArray(obj)) return obj.length;
20581
+ if (typeof obj != "object") return 1;
20319
20582
  if (H$2.isCellRef(obj)) {
20320
20583
  return 1;
20321
20584
  } else if (H$2.isRangeRef(obj)) {
@@ -21117,6 +21380,44 @@ const ReferenceFunctions = {
21117
21380
  );
21118
21381
  },
21119
21382
 
21383
+ /**
21384
+ * TRIMRANGE(range, [trim_rows], [trim_cols]) — drops the blank rows /
21385
+ * columns at the edges of a range or array: 0 keeps every row / column,
21386
+ * 1 trims leading blanks, 2 trailing blanks, 3 (default) both. Only truly
21387
+ * empty cells are blank (a "" text result or a 0 keeps its row); trimming
21388
+ * everything away is `#REF!`, as in Excel. The function form of Excel's
21389
+ * `A1.:.E10` / `A1:.E10` / `A1.:E10` trim-ref operators.
21390
+ */
21391
+ TRIMRANGE: (range, trim_rows, trim_cols) => {
21392
+ if (H$2.isOmitted(range)) throw FormulaError.ARG_MISSING([Types.ARRAY]);
21393
+ const mode = (arg) => {
21394
+ if (H$2.isOmitted(arg)) return 3;
21395
+ const n = Math.trunc(H$2.accept(arg, Types.NUMBER, 3));
21396
+ if (n < 0 || n > 3) throw FormulaError.VALUE;
21397
+ return n;
21398
+ };
21399
+ const trimRows = mode(trim_rows);
21400
+ const trimCols = mode(trim_cols);
21401
+ const m = toMatrix(range);
21402
+ const height = m.length;
21403
+ const width = m[0]?.length ?? 0;
21404
+ const isBlank = (v) => v == null;
21405
+ const blankRow = (r) => m[r].every(isBlank);
21406
+ const blankCol = (c) => m.every((row) => isBlank(row[c]));
21407
+ let rowStart = 0;
21408
+ let rowEnd = height;
21409
+ if (trimRows & 1) while (rowStart < rowEnd && blankRow(rowStart)) rowStart++;
21410
+ if (trimRows & 2) while (rowEnd > rowStart && blankRow(rowEnd - 1)) rowEnd--;
21411
+ let colStart = 0;
21412
+ let colEnd = width;
21413
+ if (trimCols & 1) while (colStart < colEnd && blankCol(colStart)) colStart++;
21414
+ if (trimCols & 2) while (colEnd > colStart && blankCol(colEnd - 1)) colEnd--;
21415
+ if (rowStart >= rowEnd || colStart >= colEnd) throw FormulaError.REF;
21416
+ return m
21417
+ .slice(rowStart, rowEnd)
21418
+ .map((row) => row.slice(colStart, colEnd));
21419
+ },
21420
+
21120
21421
  /**
21121
21422
  * EXPAND(array, rows, [columns], [pad_with]) — grows an array to
21122
21423
  * `rows`×`columns`, filling new cells with `pad_with` (default `#N/A`).
@@ -21223,21 +21524,33 @@ const ReferenceFunctions = {
21223
21524
  return result;
21224
21525
  },
21225
21526
 
21527
+ /**
21528
+ * SORTN(range, [n], [display_ties_mode], [sort_column], [is_ascending]) —
21529
+ * Google Sheets. SORT on one key column followed by the first n rows;
21530
+ * display_ties_mode is accepted and ignored. A negative n is #VALUE!. The
21531
+ * rnc engine mirrors these rules.
21532
+ */
21226
21533
  SORTN: (context, range, n, displayTies, sortColumn, isAscending) => {
21227
- range = H$2.accept(range, Types.ARRAY);
21534
+ range = H$2.accept(range, Types.ARRAY, undefined, false, true);
21228
21535
  n = H$2.accept(n, Types.NUMBER, range.length);
21229
21536
  displayTies = H$2.accept(displayTies, Types.NUMBER, 0);
21230
21537
  sortColumn = H$2.accept(sortColumn, Types.NUMBER, 1);
21231
21538
  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);
21539
+ if (n < 0) throw FormulaError.VALUE;
21540
+ if (!Array.isArray(range) || range.length === 0) return range;
21541
+ // Same stable row sort as SORT's row path (SORT itself re-parses its
21542
+ // arguments as parser params, so it cannot be called with the already
21543
+ // accepted values — doing so made every SORTN call #VALUE!).
21544
+ const width = range[0]?.length ?? 0;
21545
+ if (sortColumn < 1 || sortColumn > width) throw FormulaError.VALUE;
21546
+ const key = sortColumn - 1;
21547
+ const compareValues = makeComparator(context?.locale);
21548
+ const indexed = range.map((row, idx) => ({ row, idx }));
21549
+ indexed.sort((a, b) => {
21550
+ const comparison = compareValues(a.row[key], b.row[key]);
21551
+ return (isAscending ? comparison : -comparison) || a.idx - b.idx;
21552
+ });
21553
+ return indexed.slice(0, n).map((entry) => entry.row);
21241
21554
  },
21242
21555
 
21243
21556
  /**
@@ -25350,6 +25663,10 @@ const FinancialFunctions = {
25350
25663
  // identifiers. Importers keep the cached result Excel wrote and exporters write
25351
25664
  // the formula back unchanged, so a workbook round-trips intact; only a
25352
25665
  // recalculation on this engine surfaces the #N/A.
25666
+ //
25667
+ // QUERY is the Google Sheets counterpart: it needs a query-language interpreter
25668
+ // neither engine has, and degrades the same way (the rnc engine registers the
25669
+ // same stub) so a Sheets-authored workbook evaluates identically on both.
25353
25670
  const unavailable = () => {
25354
25671
  throw FormulaError.NA;
25355
25672
  };
@@ -25369,6 +25686,7 @@ const ExternalFunctions = {
25369
25686
  EUROCONVERT: unavailable,
25370
25687
  TRANSLATE: unavailable,
25371
25688
  DETECTLANGUAGE: unavailable,
25689
+ QUERY: unavailable,
25372
25690
  };
25373
25691
 
25374
25692
  var api$3 = {};
@@ -44517,7 +44835,10 @@ class Parser extends EmbeddedActionsParser {
44517
44835
  ) {
44518
44836
  return arr[0][0];
44519
44837
  }
44520
- return this.utils.toArray(arr);
44838
+ const array = this.utils.toArray(arr);
44839
+ return context.async
44840
+ ? this.utils.settleArrayItems(array, $.position)
44841
+ : array;
44521
44842
  });
44522
44843
  });
44523
44844
 
@@ -44538,9 +44859,13 @@ class Parser extends EmbeddedActionsParser {
44538
44859
  const prefix = $.OPTION(() => $.SUBRULE($.plusMinusOp));
44539
44860
  const image = $.CONSUME(Number$1).image;
44540
44861
  const number = $.ACTION(() => this.utils.toNumber(image));
44862
+ // A signed literal is a plain number: apply the sign directly
44863
+ // rather than through `applyPrefix`, whose async branch would
44864
+ // hand the array a pending Promise (`{-100,60,60}` summed to 120
44865
+ // under `parseAsync`).
44541
44866
  if (prefix)
44542
44867
  return $.ACTION(() =>
44543
- this.utils.applyPrefix([prefix], number, $.position)
44868
+ this.utils.applyLiteralPrefix(prefix, number)
44544
44869
  );
44545
44870
  return number;
44546
44871
  },
@@ -45177,18 +45502,21 @@ const expandLet = (source) => {
45177
45502
 
45178
45503
 
45179
45504
  const LAMBDA_INTERNAL = "__RNC_LAMBDA__";
45180
- /** Source spliced in for an optional parameter the caller left out. */
45505
+ /** Source spliced in for a parameter the caller left out. */
45181
45506
  const OMITTED_INTERNAL = "__RNC_OMITTED__";
45182
45507
  const OMITTED_CALL = `${OMITTED_INTERNAL}()`;
45183
45508
  /**
45184
45509
  * 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.
45510
+ * when `LAMBDA(...)(...)` cannot be inlined (more arguments than
45511
+ * parameters) so the evaluator reports `#VALUE!` like Excel instead of a
45512
+ * parse error.
45187
45513
  */
45188
45514
  const CALL_INTERNAL = "__RNC_CALL__";
45189
45515
 
45190
- // `[name]` — Excel's optional-parameter syntax. The lexer sees it as a
45516
+ // `[name]` — Excel's optional-parameter spelling. The lexer sees it as a
45191
45517
  // table-column token; only a bare identifier inside the brackets counts.
45518
+ // The brackets are documentation only: Excel lets a call leave out any
45519
+ // trailing parameter, bracketed or not (ISOMITTED reports it either way).
45192
45520
  const OPTIONAL_PARAM = /^\[\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\]$/;
45193
45521
 
45194
45522
  /**
@@ -45206,8 +45534,6 @@ const paramFromSpan = (tokens, span) => {
45206
45534
  return null;
45207
45535
  };
45208
45536
 
45209
- const requiredCount = (params) => params.filter((p) => !p.optional).length;
45210
-
45211
45537
  /** Excel writes optional params back as `[name]`; keep that in the value form. */
45212
45538
  const paramsToSource = (params) =>
45213
45539
  params.map((p) => (p.optional ? `[${p.name}]` : p.name)).join(",");
@@ -45217,7 +45543,9 @@ const paramsToSource = (params) =>
45217
45543
  * IIFE inlining: when `LAMBDA(...)(...)` appears, substitute the
45218
45544
  * params with the call's arg sources and return the substituted body.
45219
45545
  * Returns null if the structure doesn't match (e.g. LAMBDA without
45220
- * immediate invocation, or arity mismatch).
45546
+ * immediate invocation). A call with more arguments than parameters is
45547
+ * handed to the runtime, which reports `#VALUE!`; a call with fewer binds
45548
+ * the missing parameters to the omitted marker.
45221
45549
  */
45222
45550
  const inlineIIFE = (source, tokens, lambdaOpenIdx, lambdaCloseIdx) => {
45223
45551
  // Next token after the lambda's close paren must be a bare OpenParen
@@ -45245,13 +45573,10 @@ const inlineIIFE = (source, tokens, lambdaOpenIdx, lambdaCloseIdx) => {
45245
45573
  callCloseIdx === callOpenIdx + 1
45246
45574
  ? []
45247
45575
  : 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 `(...)(...)`.
45576
+ if (callArgSpans.length > params.length) {
45577
+ // Too many arguments — hand the call to the runtime, which applies
45578
+ // the lambda value and reports #VALUE! (Excel's answer) instead of
45579
+ // the parser choking on `(...)(...)`.
45255
45580
  const lambdaValue = transformLambdaToValue(
45256
45581
  source,
45257
45582
  tokens,
@@ -45269,9 +45594,9 @@ const inlineIIFE = (source, tokens, lambdaOpenIdx, lambdaCloseIdx) => {
45269
45594
  };
45270
45595
  }
45271
45596
  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.
45597
+ // Raw value — substituteNamesInBody decides the wrap. A parameter the
45598
+ // call left out binds to the omitted marker so ISOMITTED can see it
45599
+ // (and any other use of it is #VALUE!).
45275
45600
  const value =
45276
45601
  i < callArgSpans.length
45277
45602
  ? sourceSlice(source, tokens, callArgSpans[i]).trim()
@@ -45396,14 +45721,15 @@ const isLambdaValue = (v) =>
45396
45721
  v != null && typeof v === "object" && v.__isLambda === true;
45397
45722
 
45398
45723
  /**
45399
- * Marker bound to an optional `[param]` the caller left out. Only
45724
+ * Marker bound to a parameter the caller left out (Excel allows a call
45725
+ * with fewer arguments than parameters, `[param]` or not). Only
45400
45726
  * `ISOMITTED` looks at it; anywhere else it is an invalid operand.
45401
45727
  */
45402
45728
  const OMITTED = Object.freeze({ __isOmitted: true });
45403
45729
  const isOmittedValue = (v) =>
45404
45730
  v != null && typeof v === "object" && v.__isOmitted === true;
45405
45731
 
45406
- /** `[name]` → optional parameter, `name` → required. */
45732
+ /** `[name]` → parameter documented as optional, `name` → plain parameter. */
45407
45733
  const parseLambdaParams = (paramsStr) =>
45408
45734
  paramsStr
45409
45735
  .split(",")
@@ -45414,9 +45740,43 @@ const parseLambdaParams = (paramsStr) =>
45414
45740
  return { name: optional ? raw.slice(1, -1).trim() : raw, optional };
45415
45741
  });
45416
45742
 
45743
+ /**
45744
+ * An omitted marker that escapes `ISOMITTED` — a body that returns the
45745
+ * omitted parameter itself, or spreads it into an array — is `#VALUE!`
45746
+ * in the result, per cell inside an array. Arrays are only copied when
45747
+ * one of their cells actually holds the marker.
45748
+ */
45749
+ const rejectOmittedResult = (result) => {
45750
+ if (isOmittedValue(result)) return FormulaError.VALUE;
45751
+ if (!Array.isArray(result)) return result;
45752
+ let out = null;
45753
+ for (let i = 0; i < result.length; i++) {
45754
+ const cell = rejectOmittedResult(result[i]);
45755
+ if (cell !== result[i]) {
45756
+ if (out === null) out = result.slice();
45757
+ out[i] = cell;
45758
+ }
45759
+ }
45760
+ return out ?? result;
45761
+ };
45762
+
45417
45763
  const requiredParamCount = (lambda) =>
45418
45764
  lambda.params.filter((_, i) => !(lambda.optional?.[i] ?? false)).length;
45419
45765
 
45766
+ /**
45767
+ * Excel's "Incorrect Parameters" `#VALUE!` for MAP / REDUCE / SCAN / BYROW /
45768
+ * BYCOL / MAKEARRAY / GROUPBY / PIVOTBY: the helper drives the lambda with
45769
+ * a fixed number of arguments and the lambda must declare a parameter for
45770
+ * each of them. Only a `[name]` parameter may stay unsupplied — the
45771
+ * "fewer arguments than parameters" allowance is for direct calls, not
45772
+ * helpers. Returns the error for the whole helper call, or null when the
45773
+ * counts fit.
45774
+ */
45775
+ const hofArityError = (lambda, supplied) =>
45776
+ supplied < requiredParamCount(lambda) || supplied > lambda.params.length
45777
+ ? FormulaError.VALUE
45778
+ : null;
45779
+
45420
45780
  const formatLiteral = (value) => {
45421
45781
  if (value === null || value === undefined) return "0";
45422
45782
  if (value instanceof FormulaError) {
@@ -45459,12 +45819,11 @@ const formatLiteral = (value) => {
45459
45819
  */
45460
45820
  const applyLambda = async (context, lambda, args) => {
45461
45821
  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
- }
45822
+ // Excel rejects surplus arguments but accepts fewer than declared: the
45823
+ // missing parameters are omitted (ISOMITTED → TRUE, any other use →
45824
+ // #VALUE!). Helpers check their own parameter count with
45825
+ // `hofArityError` before getting here.
45826
+ if (args.length > lambda.params.length) return FormulaError.VALUE;
45468
45827
 
45469
45828
  context.__lambdaDepth = (context.__lambdaDepth ?? 0) + 1;
45470
45829
  if (context.__lambdaDepth > MAX_LAMBDA_DEPTH) {
@@ -45477,14 +45836,14 @@ const applyLambda = async (context, lambda, args) => {
45477
45836
  try {
45478
45837
  const bindings = lambda.params.map((name, i) => ({
45479
45838
  name,
45480
- // An omitted optional parameter binds to the omitted marker so
45839
+ // A parameter the call left out binds to the omitted marker so
45481
45840
  // ISOMITTED inside the body can report it.
45482
45841
  value:
45483
45842
  i < args.length ? `(${formatLiteral(args[i])})` : `(${OMITTED_CALL})`,
45484
45843
  }));
45485
45844
  const substituted = substituteNamesInBody(lambda.body, bindings);
45486
45845
  const result = await context.parseAsync(substituted, context.position, true);
45487
- return result;
45846
+ return rejectOmittedResult(result);
45488
45847
  } catch (e) {
45489
45848
  if (e instanceof FormulaError) return e;
45490
45849
  throw e;
@@ -45545,35 +45904,58 @@ const LambdaFunctions = {
45545
45904
  },
45546
45905
 
45547
45906
  /**
45548
- * Internal: the value spliced in for an optional `[param]` the caller
45549
- * left out (see the LAMBDA preprocessor / applyLambda).
45907
+ * Internal: the value spliced in for a parameter the caller left out
45908
+ * (see the LAMBDA preprocessor / applyLambda).
45550
45909
  */
45551
45910
  __RNC_OMITTED__: () => OMITTED,
45552
45911
 
45553
45912
  /**
45554
45913
  * 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!.
45914
+ * this for an inline `LAMBDA(...)(...)` with more arguments than
45915
+ * parameters, so the arity error surfaces as #VALUE!.
45557
45916
  */
45558
45917
  __RNC_CALL__: async (context, lambdaArg, ...args) =>
45559
45918
  applyLambda(context, unwrap(lambdaArg), args.map(unwrap)),
45560
45919
 
45561
45920
  /**
45562
- * ISOMITTED(argument) — TRUE when `argument` is an optional LAMBDA
45563
- * parameter the caller did not supply, FALSE otherwise.
45921
+ * ISOMITTED(argument) — TRUE when `argument` is a LAMBDA parameter the
45922
+ * caller did not supply (bracketed `[p]` or not), FALSE otherwise.
45564
45923
  */
45565
45924
  ISOMITTED: (arg) => isOmittedValue(unwrap(arg)),
45566
45925
 
45926
+ /**
45927
+ * A well-formed `LET(name, value, ..., body)` never reaches the function
45928
+ * table — the LET preprocessor inlines it. One that does is malformed:
45929
+ * too few arguments is `#N/A` (the too-few-arguments contract shared with
45930
+ * the Rust engine), an even count or a non-name binding is `#VALUE!`.
45931
+ */
45932
+ LET: (...args) => {
45933
+ if (args.length < 3) return FormulaError.NA;
45934
+ return FormulaError.VALUE;
45935
+ },
45936
+
45937
+ /**
45938
+ * Same for `LAMBDA(...)`: the preprocessor rewrites every LAMBDA with a
45939
+ * body, so only `LAMBDA()` (too few arguments, `#N/A`) and a body-less
45940
+ * `LAMBDA(x)` (a lambda value as the cell result, `#CALC!`) get here.
45941
+ */
45942
+ LAMBDA: (...args) => {
45943
+ if (args.length === 0) return FormulaError.NA;
45944
+ return FormulaError.CALC;
45945
+ },
45946
+
45567
45947
  /**
45568
45948
  * MAP(array1, [array2, ...], lambda) — applies lambda to each set
45569
45949
  * of corresponding elements. Returns a same-shape 2D array.
45570
45950
  */
45571
45951
  MAP: async (context, ...args) => {
45572
- if (args.length < 2) return FormulaError.VALUE;
45952
+ if (args.length < 2) return FormulaError.NA;
45573
45953
  const lambda = unwrap(args[args.length - 1]);
45574
45954
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
45575
45955
  const arrays = args.slice(0, -1).map((a) => as2D(unwrap(a)));
45576
45956
  if (arrays.length === 0) return FormulaError.VALUE;
45957
+ const arity = hofArityError(lambda, arrays.length);
45958
+ if (arity) return arity;
45577
45959
  const rows = arrays[0].length;
45578
45960
  const cols = arrays[0][0]?.length ?? 0;
45579
45961
  const out = [];
@@ -45595,6 +45977,8 @@ const LambdaFunctions = {
45595
45977
  REDUCE: async (context, initialArg, arrayArg, lambdaArg) => {
45596
45978
  const lambda = unwrap(lambdaArg);
45597
45979
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
45980
+ const arity = hofArityError(lambda, 2);
45981
+ if (arity) return arity;
45598
45982
  let acc = unwrap(initialArg);
45599
45983
  const values = flatten2D(as2D(unwrap(arrayArg)));
45600
45984
  for (const v of values) {
@@ -45612,6 +45996,8 @@ const LambdaFunctions = {
45612
45996
  SCAN: async (context, initialArg, arrayArg, lambdaArg) => {
45613
45997
  const lambda = unwrap(lambdaArg);
45614
45998
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
45999
+ const arity = hofArityError(lambda, 2);
46000
+ if (arity) return arity;
45615
46001
  let acc = unwrap(initialArg);
45616
46002
  const arr = as2D(unwrap(arrayArg));
45617
46003
  const out = [];
@@ -45633,8 +46019,11 @@ const LambdaFunctions = {
45633
46019
  * array), returns a column vector of results.
45634
46020
  */
45635
46021
  BYROW: async (context, arrayArg, lambdaArg) => {
46022
+ if (lambdaArg === undefined) return FormulaError.NA;
45636
46023
  const lambda = unwrap(lambdaArg);
45637
46024
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
46025
+ const arity = hofArityError(lambda, 1);
46026
+ if (arity) return arity;
45638
46027
  const arr = as2D(unwrap(arrayArg));
45639
46028
  const out = [];
45640
46029
  for (const row of arr) {
@@ -45650,8 +46039,11 @@ const LambdaFunctions = {
45650
46039
  * 1D array), returns a row vector of results.
45651
46040
  */
45652
46041
  BYCOL: async (context, arrayArg, lambdaArg) => {
46042
+ if (lambdaArg === undefined) return FormulaError.NA;
45653
46043
  const lambda = unwrap(lambdaArg);
45654
46044
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
46045
+ const arity = hofArityError(lambda, 1);
46046
+ if (arity) return arity;
45655
46047
  const arr = as2D(unwrap(arrayArg));
45656
46048
  const cols = arr[0]?.length ?? 0;
45657
46049
  const out = [];
@@ -45669,8 +46061,11 @@ const LambdaFunctions = {
45669
46061
  * lambda(r, c) for each cell. r, c are 1-indexed per Excel.
45670
46062
  */
45671
46063
  MAKEARRAY: async (context, rowsArg, colsArg, lambdaArg) => {
46064
+ if (lambdaArg === undefined) return FormulaError.NA;
45672
46065
  const lambda = unwrap(lambdaArg);
45673
46066
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
46067
+ const arity = hofArityError(lambda, 2);
46068
+ if (arity) return arity;
45674
46069
  const rows = Math.floor(
45675
46070
  FormulaHelpers.accept(rowsArg, Types.NUMBER, 0),
45676
46071
  );
@@ -45719,6 +46114,8 @@ const LambdaFunctions = {
45719
46114
  const valueFields = as2D(unwrap(args[1]));
45720
46115
  const lambda = unwrap(args[2]);
45721
46116
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
46117
+ const arity = hofArityError(lambda, 1);
46118
+ if (arity) return arity;
45722
46119
  const fieldHeaders = args[3] != null
45723
46120
  ? Math.floor(FormulaHelpers.accept(args[3], Types.NUMBER, 0))
45724
46121
  : 0;
@@ -45767,6 +46164,8 @@ const LambdaFunctions = {
45767
46164
  const valueFields = as2D(unwrap(args[2]));
45768
46165
  const lambda = unwrap(args[3]);
45769
46166
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
46167
+ const arity = hofArityError(lambda, 1);
46168
+ if (arity) return arity;
45770
46169
 
45771
46170
  const numOpt = (i, d) =>
45772
46171
  args[i] != null
@@ -46223,6 +46622,42 @@ let Utils$1 = class Utils {
46223
46622
  return Prefix.unaryOp(prefixes, val, isArray);
46224
46623
  }
46225
46624
 
46625
+ /**
46626
+ * Sign a numeric literal (`-1`, `+2` inside an array constant). Nothing
46627
+ * to dereference or await here, so this stays synchronous in both parse
46628
+ * modes.
46629
+ * @param {string} prefix - `+` or `-`
46630
+ * @param {number} number
46631
+ * @return {number}
46632
+ */
46633
+ applyLiteralPrefix(prefix, number) {
46634
+ return this._applyPrefix([prefix], number, false);
46635
+ }
46636
+
46637
+ /**
46638
+ * Under `parseAsync` an array-constant item built from a sub-expression
46639
+ * (`{SUM(1,-2),3}`) is a pending Promise; an array must only ever carry
46640
+ * values, so settle them. A settled item may itself be a reference
46641
+ * (`{INDEX(A1:C5,1,1),2}`), which the sync arm of `constantArray`
46642
+ * dereferences before the Promise ever existed — do the same here so both
46643
+ * parse modes agree. Returns the array itself when nothing is pending.
46644
+ * @param {Array.<Array>} array
46645
+ * @param {{row: number, col: number, sheet: string}} position
46646
+ * @return {Array.<Array>|Promise.<Array.<Array>>}
46647
+ */
46648
+ settleArrayItems(array, position) {
46649
+ const isPending = (item) =>
46650
+ item != null && typeof item.then === "function";
46651
+ if (!array.some((row) => row.some(isPending))) return array;
46652
+ const deref = (item) =>
46653
+ item && item.ref ? this.extractRefValue(item, position).val : item;
46654
+ return Promise.all(
46655
+ array.map((row) =>
46656
+ Promise.all(row).then((items) => items.map(deref))
46657
+ )
46658
+ );
46659
+ }
46660
+
46226
46661
  async applyPrefixAsync(prefixes, value, position) {
46227
46662
  const { val, isArray } = this.extractRefValue(await value, position);
46228
46663
  return this._applyPrefix(prefixes, val, isArray);
@@ -46417,15 +46852,21 @@ let Utils$1 = class Utils {
46417
46852
  }
46418
46853
  }
46419
46854
 
46420
- applyIntersect(refs) {
46855
+ applyIntersect(refs, position) {
46421
46856
  // console.log('applyIntersect', refs);
46422
46857
  if (this.isFormulaError(refs[0])) return refs[0];
46423
46858
  if (!refs[0].ref) throw Error(`Expecting a reference, but got ${refs[0]}.`);
46424
46859
  // a intersection will keep track of references, value won't be retrieved here.
46425
46860
  let maxRow, maxCol, minRow, minCol, sheet, res; // index start from 1
46861
+ // An unqualified operand (`B:B`, sheet undefined) lives on the formula's sheet: it must
46862
+ // intersect an operand that names that sheet explicitly (`Sheet1!A1:C3`, or a defined name
46863
+ // — the host resolves names WITH their sheet). Only a genuinely different sheet is #NULL!.
46864
+ const currentSheet = (position ?? this.context.position)?.sheet;
46865
+ const sheetOf = (ref) => ref.sheet ?? currentSheet;
46426
46866
  // first time setup
46427
46867
  const ref = refs.shift().ref;
46428
46868
  sheet = ref.sheet;
46869
+ const sheetKey = sheetOf(ref);
46429
46870
  if (!ref.from) {
46430
46871
  // check whole row/col reference
46431
46872
  if (ref.row === undefined || ref.col === undefined) {
@@ -46459,10 +46900,11 @@ let Utils$1 = class Utils {
46459
46900
  ref.row < minRow ||
46460
46901
  ref.col > maxCol ||
46461
46902
  ref.col < minCol ||
46462
- sheet !== ref.sheet
46903
+ sheetKey !== sheetOf(ref)
46463
46904
  ) {
46464
46905
  err = FormulaError.NULL;
46465
46906
  }
46907
+ sheet ??= ref.sheet;
46466
46908
  maxRow = minRow = ref.row;
46467
46909
  maxCol = minCol = ref.col;
46468
46910
  } else {
@@ -46476,10 +46918,11 @@ let Utils$1 = class Utils {
46476
46918
  refMaxRow < minRow ||
46477
46919
  refMinCol > maxCol ||
46478
46920
  refMaxCol < minCol ||
46479
- sheet !== ref.sheet
46921
+ sheetKey !== sheetOf(ref)
46480
46922
  ) {
46481
46923
  err = FormulaError.NULL;
46482
46924
  }
46925
+ sheet ??= ref.sheet;
46483
46926
  // update
46484
46927
  maxRow = Math.min(maxRow, refMaxRow);
46485
46928
  minRow = Math.max(minRow, refMinRow);
@@ -47480,6 +47923,9 @@ class FormulaParser {
47480
47923
  if (!this.keepLambdaResult && unwrapLambdaValue(result)) {
47481
47924
  return FormulaError.CALC;
47482
47925
  }
47926
+ // An inlined `LAMBDA(x,y,y)(1)` evaluates straight to the omitted
47927
+ // marker; only ISOMITTED may consume it, so as a result it is #VALUE!.
47928
+ result = rejectOmittedResult(result);
47483
47929
  const type = typeof result;
47484
47930
  // number
47485
47931
  if (type === "number") {
@@ -48700,6 +49146,7 @@ exports.WildCard = WildCard;
48700
49146
  exports.allTokens = allTokens;
48701
49147
  exports.default = FormulaParser;
48702
49148
  exports.implementedFunctionNames = implementedFunctionNames;
49149
+ exports.isNestedAggregateFormula = isNestedAggregateFormula;
48703
49150
  exports.lex = lex;
48704
49151
  exports.noop = noop;
48705
49152
  exports.numberToText = numberToText;