@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/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
@@ -3961,6 +3986,148 @@ const DEFAULT_SEPARATORS = { decimal: ".", group: "," };
3961
3986
  const separatorsFor = (locale) => {
3962
3987
  return DEFAULT_SEPARATORS;
3963
3988
  };
3989
+ /**
3990
+ * Rewrite the `[$symbol-LCID]` tokens of one section through `rewrite`,
3991
+ * which receives the bracket content (`€-407`) and returns the new content,
3992
+ * or an empty string to drop the token. Quoted literals, escapes and other
3993
+ * bracket directives are left as written.
3994
+ */
3995
+ const rewriteSectionLocaleTokens = (section, rewrite) => {
3996
+ let out = "";
3997
+ let inQuotes = false;
3998
+ let inBrackets = false;
3999
+ for (let i = 0; i < section.length; i++) {
4000
+ const char = section[i];
4001
+ if (inQuotes) {
4002
+ inQuotes = char !== '"';
4003
+ }
4004
+ else if (inBrackets) {
4005
+ inBrackets = char !== "]";
4006
+ }
4007
+ else if (char === '"') {
4008
+ inQuotes = true;
4009
+ }
4010
+ else if (char === "[") {
4011
+ const end = section[i + 1] === "$" ? section.indexOf("]", i) : -1;
4012
+ if (end > 0) {
4013
+ const token = rewrite(section.slice(i + 1, end));
4014
+ if (token)
4015
+ out += `[${token}]`;
4016
+ i = end;
4017
+ continue;
4018
+ }
4019
+ inBrackets = true;
4020
+ }
4021
+ else if (char === "\\" || char === "_" || char === "*") {
4022
+ out += char + (section[i + 1] ?? "");
4023
+ i++;
4024
+ continue;
4025
+ }
4026
+ out += char;
4027
+ }
4028
+ return out;
4029
+ };
4030
+ /** `[$€-407]` → `[$€]`; a bare `[$-407]` goes away. */
4031
+ const currencySymbolOnly = (token) => {
4032
+ const dash = token.indexOf("-");
4033
+ const symbol = dash < 0 ? token : token.slice(0, dash);
4034
+ return symbol.length > 1 ? symbol : "";
4035
+ };
4036
+ const separatorLocales = new Map();
4037
+ /**
4038
+ * Companion numfmt locale carrying `locale`'s calendar names and AM/PM with
4039
+ * the workbook's decimal / group separators, so a date section pinned to
4040
+ * `[$-407]` renders German names but a workbook decimal mark in `ss.00`.
4041
+ * Returns `locale` itself when its separators already match.
4042
+ */
4043
+ const getWorkbookSeparatorLocale = (locale, separators) => {
4044
+ const key = `${locale}\u0000${separators.decimal}\u0000${separators.group}`;
4045
+ const cached = separatorLocales.get(key);
4046
+ if (cached !== undefined)
4047
+ return cached;
4048
+ let overlay = locale;
4049
+ try {
4050
+ const base = getLocale(locale);
4051
+ if (base &&
4052
+ (base.decimal !== separators.decimal || base.group !== separators.group)) {
4053
+ // numfmt locale ids are `[a-z\d]+` with one optional `_` part.
4054
+ const hex = (text) => [...text].map((c) => c.codePointAt(0).toString(16)).join("x");
4055
+ overlay = `sep${hex(separators.decimal)}x${hex(separators.group)}_${locale.replace(/[^a-z\d]/gi, "")}`;
4056
+ addLocale({ ...base, decimal: separators.decimal, group: separators.group }, overlay);
4057
+ }
4058
+ }
4059
+ catch {
4060
+ overlay = locale;
4061
+ }
4062
+ separatorLocales.set(key, overlay);
4063
+ return overlay;
4064
+ };
4065
+ /**
4066
+ * Re-point a date section's `[$symbol-LCID]` token at the companion locale
4067
+ * with the workbook's separators (`$-407` → `$-sep2ex2c_407`). An LCID numfmt
4068
+ * does not know or one carrying calendar bits stays as written.
4069
+ */
4070
+ const workbookSeparatorLocaleToken = (separators) => (token) => {
4071
+ const dash = token.indexOf("-");
4072
+ if (dash < 0)
4073
+ return token;
4074
+ const code = token.slice(dash + 1);
4075
+ const wincode = Number.parseInt(code, 16);
4076
+ if (Number.isFinite(wincode) && wincode & 0xff0000)
4077
+ return token;
4078
+ if (!getLocale(code))
4079
+ return token;
4080
+ return `${token.slice(0, dash + 1)}${getWorkbookSeparatorLocale(code, separators)}`;
4081
+ };
4082
+ const isDateSection = (section) => {
4083
+ try {
4084
+ return isDateFormat(section);
4085
+ }
4086
+ catch {
4087
+ return false;
4088
+ }
4089
+ };
4090
+ /**
4091
+ * Excel's `[$symbol-LCID]` token selects the currency symbol and the
4092
+ * language of day/month names and AM/PM; digit grouping and the decimal
4093
+ * separator follow the workbook locale (`[$€-407] #,##0.00` is
4094
+ * `€ 1,234.50` on a US host). numfmt applies the token's locale to the whole
4095
+ * section, separators included, so the locale part is dropped from every
4096
+ * section that is not a date (number, scientific, fraction, text, …); a
4097
+ * date section keeps the token for its names, re-pointed at a companion
4098
+ * locale that carries the workbook's separators for fractional seconds.
4099
+ * Returns `undefined` when nothing had to change.
4100
+ */
4101
+ const withWorkbookSeparators = (pattern, locale) => {
4102
+ if (!pattern.includes("[$"))
4103
+ return undefined;
4104
+ const sections = splitFormatSections(pattern);
4105
+ let changed = false;
4106
+ let dateTokens;
4107
+ const rewritten = sections.map((section) => {
4108
+ if (!section.includes("[$"))
4109
+ return section;
4110
+ let rewrite = currencySymbolOnly;
4111
+ if (isDateSection(section)) {
4112
+ dateTokens ??= workbookSeparatorLocaleToken(separatorsFor());
4113
+ rewrite = dateTokens;
4114
+ }
4115
+ const stripped = rewriteSectionLocaleTokens(section, rewrite);
4116
+ changed ||= stripped !== section;
4117
+ return stripped;
4118
+ });
4119
+ return changed ? rewritten.join(";") : undefined;
4120
+ };
4121
+ const workbookSeparatorPatternCache = new Map();
4122
+ const withWorkbookSeparatorsCached = (code, locale) => {
4123
+ const key = `${""}\u0000${code}`;
4124
+ let rewritten = workbookSeparatorPatternCache.get(key);
4125
+ if (rewritten === undefined) {
4126
+ rewritten = withWorkbookSeparators(code) ?? code;
4127
+ workbookSeparatorPatternCache.set(key, rewritten);
4128
+ }
4129
+ return rewritten;
4130
+ };
3964
4131
  const ssfFormat = (pattern, value, locale) => {
3965
4132
  try {
3966
4133
  let shortAmPm = shortAmPmPatternCache.get(pattern);
@@ -3968,7 +4135,7 @@ const ssfFormat = (pattern, value, locale) => {
3968
4135
  shortAmPm = withShortAmPmMarker(pattern);
3969
4136
  shortAmPmPatternCache.set(pattern, shortAmPm);
3970
4137
  }
3971
- const code = shortAmPm ?? pattern;
4138
+ const code = withWorkbookSeparatorsCached(shortAmPm ?? pattern, locale);
3972
4139
  let patternLocale = localeForPattern(pattern, locale);
3973
4140
  if (shortAmPm !== undefined && patternLocale) {
3974
4141
  patternLocale = getShortAmPmLocale(patternLocale);
@@ -4671,10 +4838,12 @@ class FormulaHelpersClass {
4671
4838
  !isRaw && Object.prototype.hasOwnProperty.call(param, "value");
4672
4839
  if (hasValueProp) {
4673
4840
  if (param.value == null) {
4674
- if (type === Types.STRING) {
4675
- // Blank cells should behave like empty strings in text formulas
4676
- return "";
4677
- }
4841
+ // A blank cell is not an omitted argument (only omission takes
4842
+ // `defValue`): Excel reads it as "" in a text slot, 0 in a number
4843
+ // slot and FALSE in a logical slot.
4844
+ if (type === Types.STRING) return "";
4845
+ if (type === Types.NUMBER || type === Types.NUMBER_NO_BOOLEAN) return 0;
4846
+ if (type === Types.BOOLEAN) return false;
4678
4847
  param = param.value;
4679
4848
  } else {
4680
4849
  param = param.value;
@@ -4754,6 +4923,7 @@ class FormulaHelpersClass {
4754
4923
  * @param range2
4755
4924
  */
4756
4925
  retrieveRanges(context, range1, range2) {
4926
+ if (range1 === undefined) throw FormulaError.ARG_MISSING([Types.ARRAY]);
4757
4927
  // process args
4758
4928
  range2 = Address.extend(range1, range2);
4759
4929
 
@@ -4770,6 +4940,9 @@ class FormulaHelpersClass {
4770
4940
  }
4771
4941
 
4772
4942
  retrieveArg(context, arg) {
4943
+ // `null` is an explicitly skipped argument (`SUMIF(A1:A5,,B1:B5)`);
4944
+ // `undefined` is one the call never supplied — too few arguments.
4945
+ if (arg === undefined) throw FormulaError.ARG_MISSING([Types.ARRAY]);
4773
4946
  if (arg === null) return { value: 0, isArray: false, omitted: true };
4774
4947
  const res = context.utils.extractRefValue(arg);
4775
4948
  return { value: res.val, isArray: res.isArray, ref: arg.ref };
@@ -6339,33 +6512,48 @@ const TextFunctions = {
6339
6512
  return text.slice(0, m.index) + expanded + text.slice(m.index + m[0].length);
6340
6513
  },
6341
6514
 
6515
+ /**
6516
+ * JOIN(delimiter, value_or_array1, [value_or_array2, …]) — Google Sheets.
6517
+ * Ranges / arrays contribute every element; blanks and empty text are
6518
+ * skipped; values spell as TEXTJOIN spells them (TRUE, 15-digit numbers) and
6519
+ * an error among the values propagates. The rnc engine mirrors these rules.
6520
+ */
6342
6521
  JOIN: (delimiter, ...values) => {
6343
6522
  delimiter = H$a.accept(delimiter, Types.STRING);
6344
6523
  const result = [];
6345
6524
 
6346
- H$a.flattenParams(values, null, true, (item) => {
6347
- if (item !== undefined && item !== null && item !== "") {
6348
- result.push(String(item));
6349
- }
6525
+ H$a.flattenParams(values, Types.STRING, true, (item) => {
6526
+ if (item === undefined || item === null) return;
6527
+ const str = H$a.accept(item, Types.STRING);
6528
+ if (str !== "") result.push(str);
6350
6529
  });
6351
6530
 
6352
6531
  return result.join(delimiter);
6353
6532
  },
6354
6533
 
6534
+ /**
6535
+ * SPLIT(text, delimiter, [split_by_each=TRUE], [remove_empty_text=TRUE]) —
6536
+ * Google Sheets. Pieces stay text (no number coercion); an empty delimiter
6537
+ * is #VALUE!; when every piece is removed the result is empty text. The
6538
+ * rnc engine mirrors these rules.
6539
+ */
6355
6540
  SPLIT: (text, delimiter, splitByEach, removeEmpty) => {
6356
6541
  text = H$a.accept(text, Types.STRING);
6357
6542
  delimiter = H$a.accept(delimiter, Types.STRING);
6358
6543
  splitByEach = H$a.accept(splitByEach, Types.BOOLEAN, true);
6359
6544
  removeEmpty = H$a.accept(removeEmpty, Types.BOOLEAN, true);
6545
+ if (delimiter === "") throw FormulaError.VALUE;
6360
6546
 
6361
6547
  let parts;
6362
6548
  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);
6549
+ // Every character of the delimiter is a separator — a literal character,
6550
+ // never a RegExp class ("d" must not become \d, "b" must not become \b).
6551
+ const separators = new Set(delimiter);
6552
+ parts = [""];
6553
+ for (const ch of text) {
6554
+ if (separators.has(ch)) parts.push("");
6555
+ else parts[parts.length - 1] += ch;
6556
+ }
6369
6557
  } else {
6370
6558
  // Split by the entire delimiter string
6371
6559
  parts = text.split(delimiter);
@@ -6374,6 +6562,7 @@ const TextFunctions = {
6374
6562
  if (removeEmpty) {
6375
6563
  parts = parts.filter((part) => part !== "");
6376
6564
  }
6565
+ if (parts.length === 0) return "";
6377
6566
 
6378
6567
  // Return as a single row array
6379
6568
  return [parts];
@@ -12349,15 +12538,48 @@ const collectNumericValues = (array) => {
12349
12538
  // FORECAST.ETS helpers — Holt-Winters AAA implementation.
12350
12539
  // ---------------------------------------------------------------------
12351
12540
 
12541
+ // Excel's `seasonality` argument: 1 (the default) auto-detects the
12542
+ // period, 0 fits without a seasonal component, 2..8760 forces the
12543
+ // period; anything else is #NUM!. Fractions are truncated.
12544
+ const ETS_MAX_SEASONALITY = 8760;
12545
+ const forecastEtsSeasonality = (seasonalityArg) => {
12546
+ if (seasonalityArg == null) return 1;
12547
+ const value = H$8.accept(seasonalityArg, Types.NUMBER, 1);
12548
+ if (value == null) return 1;
12549
+ const seasonality = Math.floor(value);
12550
+ if (seasonality < 0 || seasonality > ETS_MAX_SEASONALITY) throw FormulaError.NUM;
12551
+ return seasonality;
12552
+ };
12553
+
12554
+ // `data_completion` must be 0 or 1 and `aggregation` 1..7 (AVERAGE,
12555
+ // COUNT, COUNTA, MAX, MEDIAN, MIN, SUM), otherwise #NUM!. Neither
12556
+ // changes the fit: the timeline is used as given (no gap filling,
12557
+ // duplicates are not aggregated).
12558
+ const forecastEtsValidateOptions = (dataCompletionArg, aggregationArg) => {
12559
+ if (dataCompletionArg != null) {
12560
+ const completion = Math.floor(H$8.accept(dataCompletionArg, Types.NUMBER, 1));
12561
+ if (completion !== 0 && completion !== 1) throw FormulaError.NUM;
12562
+ }
12563
+ if (aggregationArg != null) {
12564
+ const aggregation = Math.floor(H$8.accept(aggregationArg, Types.NUMBER, 1));
12565
+ if (aggregation < 1 || aggregation > 7) throw FormulaError.NUM;
12566
+ }
12567
+ };
12568
+
12352
12569
  // Pair up timeline + values, sort by timeline, infer the constant
12353
12570
  // step (Excel requires a uniformly-spaced timeline), and resolve
12354
- // the seasonality parameter (auto-detect when 0/null/missing).
12571
+ // the seasonality parameter (Excel spelling: 1 auto-detects, 0 is no
12572
+ // seasonality, n > 1 is the period).
12355
12573
  const forecastEtsPrep = (ys, xs, seasonality) => {
12356
12574
  if (xs.length !== ys.length) throw FormulaError.NA;
12357
12575
  const pairs = [];
12358
12576
  for (let i = 0; i < xs.length; i++) {
12359
12577
  const x = xs[i];
12360
12578
  const yv = ys[i];
12579
+ // An error anywhere in the series is the result (Excel and the Rust
12580
+ // port's `series_number`); other non-numbers (text, blanks) are skipped.
12581
+ if (x instanceof FormulaError) throw x;
12582
+ if (yv instanceof FormulaError) throw yv;
12361
12583
  if (typeof x !== "number") continue;
12362
12584
  if (typeof yv !== "number") continue;
12363
12585
  pairs.push([x, yv]);
@@ -12378,12 +12600,12 @@ const forecastEtsPrep = (ys, xs, seasonality) => {
12378
12600
  diffs.sort((a, b) => a - b);
12379
12601
  const step = diffs[Math.floor(diffs.length / 2)];
12380
12602
  let period;
12381
- if (seasonality === 1) {
12603
+ if (seasonality === 0) {
12382
12604
  period = 1; // No seasonality (linear Holt).
12383
- } else if (seasonality > 1) {
12384
- period = seasonality;
12385
- } else {
12605
+ } else if (seasonality === 1) {
12386
12606
  period = detectSeasonality(y);
12607
+ } else {
12608
+ period = seasonality;
12387
12609
  }
12388
12610
  return { y, lastX: sortedX[sortedX.length - 1], step, period };
12389
12611
  };
@@ -13263,20 +13485,18 @@ const DistributionFunctions = {
13263
13485
  // trend. We use the same idea — scan candidate periods 2..N/2,
13264
13486
  // capped at 12 (Excel's default cap for monthly data), and pick
13265
13487
  // the one with the lowest in-sample SSE for the fitted Holt-Winters
13266
- // model. Passing `seasonality = 1` forces "no seasonality" (Holt
13267
- // linear).
13488
+ // model. `seasonality` follows Excel: 1 (default) auto-detects, 0
13489
+ // forces "no seasonality" (Holt linear), n > 1 is the period. The
13490
+ // Rust engine (formualizer `stats/forecast_ets.rs`) is a port of this
13491
+ // code, so both engines agree to 1e-9 on the same inputs.
13268
13492
  // -------------------------------------------------------------------
13269
13493
 
13270
13494
  'FORECAST.ETS': (targetDate, values, timeline, seasonalityArg, dataCompletionArg, aggregationArg) => {
13271
13495
  const target = H$8.accept(targetDate, Types.NUMBER);
13272
13496
  const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
13273
13497
  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).
13498
+ const seasonality = forecastEtsSeasonality(seasonalityArg);
13499
+ forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
13280
13500
  const { y, lastX, step, period } = forecastEtsPrep(ys, xs, seasonality);
13281
13501
  if (target < lastX) {
13282
13502
  // Excel returns #NUM! for in-history targets in FORECAST.ETS
@@ -13289,7 +13509,7 @@ const DistributionFunctions = {
13289
13509
  return forecast[horizon - 1];
13290
13510
  },
13291
13511
 
13292
- 'FORECAST.ETS.CONFINT': (targetDate, values, timeline, confidenceArg, seasonalityArg) => {
13512
+ 'FORECAST.ETS.CONFINT': (targetDate, values, timeline, confidenceArg, seasonalityArg, dataCompletionArg, aggregationArg) => {
13293
13513
  const target = H$8.accept(targetDate, Types.NUMBER);
13294
13514
  const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
13295
13515
  const xs = H$8.accept(timeline, Types.ARRAY, undefined, true, true);
@@ -13297,9 +13517,8 @@ const DistributionFunctions = {
13297
13517
  ? 0.95
13298
13518
  : H$8.accept(confidenceArg, Types.NUMBER, 0.95);
13299
13519
  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));
13520
+ const seasonality = forecastEtsSeasonality(seasonalityArg);
13521
+ forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
13303
13522
  const { y, lastX, step, period } = forecastEtsPrep(ys, xs, seasonality);
13304
13523
  if (target < lastX) throw FormulaError.NUM;
13305
13524
  const horizon = Math.max(1, Math.round((target - lastX) / step));
@@ -13319,21 +13538,21 @@ const DistributionFunctions = {
13319
13538
  return z * rmse * Math.sqrt(horizon);
13320
13539
  },
13321
13540
 
13322
- 'FORECAST.ETS.SEASONALITY': (values, timeline) => {
13541
+ 'FORECAST.ETS.SEASONALITY': (values, timeline, dataCompletionArg, aggregationArg) => {
13323
13542
  const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
13324
13543
  const xs = H$8.accept(timeline, Types.ARRAY, undefined, true, true);
13325
- const { y } = forecastEtsPrep(ys, xs, 0);
13326
- return detectSeasonality(y);
13544
+ forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
13545
+ const { period } = forecastEtsPrep(ys, xs, 1);
13546
+ return period;
13327
13547
  },
13328
13548
 
13329
- 'FORECAST.ETS.STAT': (values, timeline, statTypeArg, seasonalityArg) => {
13549
+ 'FORECAST.ETS.STAT': (values, timeline, statTypeArg, seasonalityArg, dataCompletionArg, aggregationArg) => {
13330
13550
  const ys = H$8.accept(values, Types.ARRAY, undefined, true, true);
13331
13551
  const xs = H$8.accept(timeline, Types.ARRAY, undefined, true, true);
13332
13552
  const statType = Math.floor(H$8.accept(statTypeArg, Types.NUMBER));
13333
13553
  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));
13554
+ const seasonality = forecastEtsSeasonality(seasonalityArg);
13555
+ forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
13337
13556
  const { y, period } = forecastEtsPrep(ys, xs, seasonality);
13338
13557
  const { alpha, beta, gamma, fitted } = fitHoltWinters(y, period, 0);
13339
13558
  // Excel STAT codes:
@@ -14420,6 +14639,9 @@ const DistributionFunctions = {
14420
14639
  const prediction = truncated.reduce((sum, value, idx) => sum + value * slopes[idx], 0) + intercept;
14421
14640
  return [prediction];
14422
14641
  });
14642
+ // A single new_x is a plain number in Excel (and in the Rust
14643
+ // engine); a 1x1 array would anchor an array formula on the cell.
14644
+ if (result.length === 1) return result[0][0];
14423
14645
  return result;
14424
14646
  },
14425
14647
 
@@ -15402,7 +15624,7 @@ const StatisticalFunctions = {
15402
15624
  COUNTIF: (range, criteria) => {
15403
15625
  // do not flatten the array
15404
15626
  range = H$7.accept(range, Types.ARRAY, undefined, false, true);
15405
- const isCriteriaArray = criteria.isArray;
15627
+ const isCriteriaArray = criteria?.isArray;
15406
15628
  criteria = H$7.accept(criteria);
15407
15629
 
15408
15630
  const countMatching = (parsed) => {
@@ -16717,7 +16939,15 @@ const MathFunctions = {
16717
16939
  FLOOR: (number, significance) => {
16718
16940
  number = H$6.accept(number, Types.NUMBER);
16719
16941
  significance = H$6.accept(significance, Types.NUMBER, 1);
16720
- if (significance === 0) return 0;
16942
+ // Excel: a zero significance is #DIV/0! for FLOOR of a non-zero number
16943
+ // (FLOOR(0,0) is 0; CEILING, FLOOR.MATH and FLOOR.PRECISE return 0); a
16944
+ // positive number with a negative significance is #NUM!; a negative
16945
+ // number rounds away from zero unless the significance is negative too
16946
+ // (FLOOR(-2.5,2) = -4, FLOOR(-2.5,-2) = -2).
16947
+ if (significance === 0) {
16948
+ if (number === 0) return 0;
16949
+ throw FormulaError.DIV0;
16950
+ }
16721
16951
  if (number > 0 && significance < 0) throw FormulaError.NUM;
16722
16952
  const absSignificance = Math.abs(significance);
16723
16953
  const quotient = excelQuotient(Math.abs(number), absSignificance);
@@ -16742,6 +16972,7 @@ const MathFunctions = {
16742
16972
  // mode can be 0 or any other number, 0 means away from zero
16743
16973
  // the official documentation seems wrong
16744
16974
  mode = H$6.accept(mode, Types.NUMBER, 0);
16975
+ if (significance === 0) return 0;
16745
16976
  // The Mode argument does not affect positive numbers.
16746
16977
  if (mode === 0 || number >= 0) {
16747
16978
  // away from zero
@@ -16754,7 +16985,8 @@ const MathFunctions = {
16754
16985
  "FLOOR.PRECISE": (number, significance) => {
16755
16986
  number = H$6.accept(number, Types.NUMBER);
16756
16987
  significance = H$6.accept(significance, Types.NUMBER, 1);
16757
- // always round up
16988
+ // A zero significance is 0 (Excel); otherwise the mathematical floor.
16989
+ if (significance === 0) return 0;
16758
16990
  return MathFunctions.FLOOR(number, Math.abs(significance));
16759
16991
  },
16760
16992
 
@@ -17260,6 +17492,22 @@ const MathFunctions = {
17260
17492
  return result;
17261
17493
  },
17262
17494
 
17495
+ /**
17496
+ * PERCENTOF(data_subset, data_all) — SUM(data_subset) / SUM(data_all) under
17497
+ * SUM's rules (text / logical / blank cells inside ranges are skipped, error
17498
+ * values propagate, a non-numeric scalar is #VALUE!); a zero whole is
17499
+ * #DIV/0!. Excel 365's helper for GROUPBY / PIVOTBY percent-of-total.
17500
+ */
17501
+ PERCENTOF: (dataSubset, dataAll) => {
17502
+ if (H$6.isOmitted(dataSubset) || H$6.isOmitted(dataAll)) {
17503
+ throw FormulaError.ARG_MISSING([Types.NUMBER]);
17504
+ }
17505
+ const subset = MathFunctions.SUM(dataSubset);
17506
+ const all = MathFunctions.SUM(dataAll);
17507
+ if (all === 0) throw FormulaError.DIV0;
17508
+ return subset / all;
17509
+ },
17510
+
17263
17511
  /**
17264
17512
  * This functions requires instance of {@link FormulaParser}.
17265
17513
  */
@@ -19846,6 +20094,10 @@ const ReferenceFunctions = {
19846
20094
  // Retrieve actual data to determine effective column count
19847
20095
  const { val: data } = context.utils.extractRefValue(obj, context.position);
19848
20096
  const colCount = Array.isArray(data) && Array.isArray(data[0]) ? data[0].length : 1;
20097
+ // One column (`COLUMN(C:C)`, `COLUMN(B2:B5)`) is a plain number, as
20098
+ // Excel shows it and as the Rust engine returns it; a 1x1 array would
20099
+ // make the host anchor an array formula on the cell.
20100
+ if (colCount <= 1) return fromCol;
19849
20101
  const result = [];
19850
20102
  for (let i = 0; i < colCount; i++) {
19851
20103
  result.push(fromCol + i);
@@ -19862,7 +20114,11 @@ const ReferenceFunctions = {
19862
20114
  if (obj == null) {
19863
20115
  throw Error("COLUMNS requires one argument");
19864
20116
  }
19865
- if (typeof obj != "object" || Array.isArray(obj)) throw FormulaError.VALUE;
20117
+ if (obj instanceof FormulaError) throw obj;
20118
+ // An array (`{1,2;3,4}`, `SEQUENCE(3,2)`, `RANDARRAY(3,2)`, a SORT/FILTER
20119
+ // result) reports its width; a scalar is a 1×1 array (`COLUMNS(5)` = 1).
20120
+ if (Array.isArray(obj)) return obj[0]?.length ?? 0;
20121
+ if (typeof obj != "object") return 1;
19866
20122
  if (H$2.isCellRef(obj)) {
19867
20123
  return 1;
19868
20124
  } else if (H$2.isRangeRef(obj)) {
@@ -19913,6 +20169,7 @@ const ReferenceFunctions = {
19913
20169
 
19914
20170
  // Special
19915
20171
  INDEX: (context, ranges, rowNum, colNum, areaNum) => {
20172
+ if (ranges === undefined) throw FormulaError.ARG_MISSING([Types.ARRAY]);
19916
20173
  // retrieve values
19917
20174
  const isColNumMissing = colNum == null;
19918
20175
  const isRowNumMissing = rowNum == null;
@@ -20295,6 +20552,8 @@ const ReferenceFunctions = {
20295
20552
  // Retrieve actual data to determine effective row count
20296
20553
  const { val: data } = context.utils.extractRefValue(obj, context.position);
20297
20554
  const rowCount = Array.isArray(data) ? data.length : 1;
20555
+ // One row (`ROW(3:3)`, `ROW(B2:C2)`) is a plain number; see COLUMN.
20556
+ if (rowCount <= 1) return fromRow;
20298
20557
  const result = [];
20299
20558
  for (let i = 0; i < rowCount; i++) {
20300
20559
  result.push([fromRow + i]); // Vertical array (column vector)
@@ -20311,7 +20570,11 @@ const ReferenceFunctions = {
20311
20570
  if (obj == null) {
20312
20571
  throw Error("ROWS requires one argument");
20313
20572
  }
20314
- if (typeof obj != "object" || Array.isArray(obj)) throw FormulaError.VALUE;
20573
+ if (obj instanceof FormulaError) throw obj;
20574
+ // An array (`{1,2;3,4}`, `SEQUENCE(3,2)`, `RANDARRAY(3,2)`, a SORT/FILTER
20575
+ // result) reports its height; a scalar is a 1×1 array (`ROWS(5)` = 1).
20576
+ if (Array.isArray(obj)) return obj.length;
20577
+ if (typeof obj != "object") return 1;
20315
20578
  if (H$2.isCellRef(obj)) {
20316
20579
  return 1;
20317
20580
  } else if (H$2.isRangeRef(obj)) {
@@ -21113,6 +21376,44 @@ const ReferenceFunctions = {
21113
21376
  );
21114
21377
  },
21115
21378
 
21379
+ /**
21380
+ * TRIMRANGE(range, [trim_rows], [trim_cols]) — drops the blank rows /
21381
+ * columns at the edges of a range or array: 0 keeps every row / column,
21382
+ * 1 trims leading blanks, 2 trailing blanks, 3 (default) both. Only truly
21383
+ * empty cells are blank (a "" text result or a 0 keeps its row); trimming
21384
+ * everything away is `#REF!`, as in Excel. The function form of Excel's
21385
+ * `A1.:.E10` / `A1:.E10` / `A1.:E10` trim-ref operators.
21386
+ */
21387
+ TRIMRANGE: (range, trim_rows, trim_cols) => {
21388
+ if (H$2.isOmitted(range)) throw FormulaError.ARG_MISSING([Types.ARRAY]);
21389
+ const mode = (arg) => {
21390
+ if (H$2.isOmitted(arg)) return 3;
21391
+ const n = Math.trunc(H$2.accept(arg, Types.NUMBER, 3));
21392
+ if (n < 0 || n > 3) throw FormulaError.VALUE;
21393
+ return n;
21394
+ };
21395
+ const trimRows = mode(trim_rows);
21396
+ const trimCols = mode(trim_cols);
21397
+ const m = toMatrix(range);
21398
+ const height = m.length;
21399
+ const width = m[0]?.length ?? 0;
21400
+ const isBlank = (v) => v == null;
21401
+ const blankRow = (r) => m[r].every(isBlank);
21402
+ const blankCol = (c) => m.every((row) => isBlank(row[c]));
21403
+ let rowStart = 0;
21404
+ let rowEnd = height;
21405
+ if (trimRows & 1) while (rowStart < rowEnd && blankRow(rowStart)) rowStart++;
21406
+ if (trimRows & 2) while (rowEnd > rowStart && blankRow(rowEnd - 1)) rowEnd--;
21407
+ let colStart = 0;
21408
+ let colEnd = width;
21409
+ if (trimCols & 1) while (colStart < colEnd && blankCol(colStart)) colStart++;
21410
+ if (trimCols & 2) while (colEnd > colStart && blankCol(colEnd - 1)) colEnd--;
21411
+ if (rowStart >= rowEnd || colStart >= colEnd) throw FormulaError.REF;
21412
+ return m
21413
+ .slice(rowStart, rowEnd)
21414
+ .map((row) => row.slice(colStart, colEnd));
21415
+ },
21416
+
21116
21417
  /**
21117
21418
  * EXPAND(array, rows, [columns], [pad_with]) — grows an array to
21118
21419
  * `rows`×`columns`, filling new cells with `pad_with` (default `#N/A`).
@@ -21219,21 +21520,33 @@ const ReferenceFunctions = {
21219
21520
  return result;
21220
21521
  },
21221
21522
 
21523
+ /**
21524
+ * SORTN(range, [n], [display_ties_mode], [sort_column], [is_ascending]) —
21525
+ * Google Sheets. SORT on one key column followed by the first n rows;
21526
+ * display_ties_mode is accepted and ignored. A negative n is #VALUE!. The
21527
+ * rnc engine mirrors these rules.
21528
+ */
21222
21529
  SORTN: (context, range, n, displayTies, sortColumn, isAscending) => {
21223
- range = H$2.accept(range, Types.ARRAY);
21530
+ range = H$2.accept(range, Types.ARRAY, undefined, false, true);
21224
21531
  n = H$2.accept(n, Types.NUMBER, range.length);
21225
21532
  displayTies = H$2.accept(displayTies, Types.NUMBER, 0);
21226
21533
  sortColumn = H$2.accept(sortColumn, Types.NUMBER, 1);
21227
21534
  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);
21535
+ if (n < 0) throw FormulaError.VALUE;
21536
+ if (!Array.isArray(range) || range.length === 0) return range;
21537
+ // Same stable row sort as SORT's row path (SORT itself re-parses its
21538
+ // arguments as parser params, so it cannot be called with the already
21539
+ // accepted values — doing so made every SORTN call #VALUE!).
21540
+ const width = range[0]?.length ?? 0;
21541
+ if (sortColumn < 1 || sortColumn > width) throw FormulaError.VALUE;
21542
+ const key = sortColumn - 1;
21543
+ const compareValues = makeComparator(context?.locale);
21544
+ const indexed = range.map((row, idx) => ({ row, idx }));
21545
+ indexed.sort((a, b) => {
21546
+ const comparison = compareValues(a.row[key], b.row[key]);
21547
+ return (isAscending ? comparison : -comparison) || a.idx - b.idx;
21548
+ });
21549
+ return indexed.slice(0, n).map((entry) => entry.row);
21237
21550
  },
21238
21551
 
21239
21552
  /**
@@ -25346,6 +25659,10 @@ const FinancialFunctions = {
25346
25659
  // identifiers. Importers keep the cached result Excel wrote and exporters write
25347
25660
  // the formula back unchanged, so a workbook round-trips intact; only a
25348
25661
  // recalculation on this engine surfaces the #N/A.
25662
+ //
25663
+ // QUERY is the Google Sheets counterpart: it needs a query-language interpreter
25664
+ // neither engine has, and degrades the same way (the rnc engine registers the
25665
+ // same stub) so a Sheets-authored workbook evaluates identically on both.
25349
25666
  const unavailable = () => {
25350
25667
  throw FormulaError.NA;
25351
25668
  };
@@ -25365,6 +25682,7 @@ const ExternalFunctions = {
25365
25682
  EUROCONVERT: unavailable,
25366
25683
  TRANSLATE: unavailable,
25367
25684
  DETECTLANGUAGE: unavailable,
25685
+ QUERY: unavailable,
25368
25686
  };
25369
25687
 
25370
25688
  var api$3 = {};
@@ -44513,7 +44831,10 @@ class Parser extends EmbeddedActionsParser {
44513
44831
  ) {
44514
44832
  return arr[0][0];
44515
44833
  }
44516
- return this.utils.toArray(arr);
44834
+ const array = this.utils.toArray(arr);
44835
+ return context.async
44836
+ ? this.utils.settleArrayItems(array, $.position)
44837
+ : array;
44517
44838
  });
44518
44839
  });
44519
44840
 
@@ -44534,9 +44855,13 @@ class Parser extends EmbeddedActionsParser {
44534
44855
  const prefix = $.OPTION(() => $.SUBRULE($.plusMinusOp));
44535
44856
  const image = $.CONSUME(Number$1).image;
44536
44857
  const number = $.ACTION(() => this.utils.toNumber(image));
44858
+ // A signed literal is a plain number: apply the sign directly
44859
+ // rather than through `applyPrefix`, whose async branch would
44860
+ // hand the array a pending Promise (`{-100,60,60}` summed to 120
44861
+ // under `parseAsync`).
44537
44862
  if (prefix)
44538
44863
  return $.ACTION(() =>
44539
- this.utils.applyPrefix([prefix], number, $.position)
44864
+ this.utils.applyLiteralPrefix(prefix, number)
44540
44865
  );
44541
44866
  return number;
44542
44867
  },
@@ -45173,18 +45498,21 @@ const expandLet = (source) => {
45173
45498
 
45174
45499
 
45175
45500
  const LAMBDA_INTERNAL = "__RNC_LAMBDA__";
45176
- /** Source spliced in for an optional parameter the caller left out. */
45501
+ /** Source spliced in for a parameter the caller left out. */
45177
45502
  const OMITTED_INTERNAL = "__RNC_OMITTED__";
45178
45503
  const OMITTED_CALL = `${OMITTED_INTERNAL}()`;
45179
45504
  /**
45180
45505
  * 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.
45506
+ * when `LAMBDA(...)(...)` cannot be inlined (more arguments than
45507
+ * parameters) so the evaluator reports `#VALUE!` like Excel instead of a
45508
+ * parse error.
45183
45509
  */
45184
45510
  const CALL_INTERNAL = "__RNC_CALL__";
45185
45511
 
45186
- // `[name]` — Excel's optional-parameter syntax. The lexer sees it as a
45512
+ // `[name]` — Excel's optional-parameter spelling. The lexer sees it as a
45187
45513
  // table-column token; only a bare identifier inside the brackets counts.
45514
+ // The brackets are documentation only: Excel lets a call leave out any
45515
+ // trailing parameter, bracketed or not (ISOMITTED reports it either way).
45188
45516
  const OPTIONAL_PARAM = /^\[\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\]$/;
45189
45517
 
45190
45518
  /**
@@ -45202,8 +45530,6 @@ const paramFromSpan = (tokens, span) => {
45202
45530
  return null;
45203
45531
  };
45204
45532
 
45205
- const requiredCount = (params) => params.filter((p) => !p.optional).length;
45206
-
45207
45533
  /** Excel writes optional params back as `[name]`; keep that in the value form. */
45208
45534
  const paramsToSource = (params) =>
45209
45535
  params.map((p) => (p.optional ? `[${p.name}]` : p.name)).join(",");
@@ -45213,7 +45539,9 @@ const paramsToSource = (params) =>
45213
45539
  * IIFE inlining: when `LAMBDA(...)(...)` appears, substitute the
45214
45540
  * params with the call's arg sources and return the substituted body.
45215
45541
  * Returns null if the structure doesn't match (e.g. LAMBDA without
45216
- * immediate invocation, or arity mismatch).
45542
+ * immediate invocation). A call with more arguments than parameters is
45543
+ * handed to the runtime, which reports `#VALUE!`; a call with fewer binds
45544
+ * the missing parameters to the omitted marker.
45217
45545
  */
45218
45546
  const inlineIIFE = (source, tokens, lambdaOpenIdx, lambdaCloseIdx) => {
45219
45547
  // Next token after the lambda's close paren must be a bare OpenParen
@@ -45241,13 +45569,10 @@ const inlineIIFE = (source, tokens, lambdaOpenIdx, lambdaCloseIdx) => {
45241
45569
  callCloseIdx === callOpenIdx + 1
45242
45570
  ? []
45243
45571
  : 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 `(...)(...)`.
45572
+ if (callArgSpans.length > params.length) {
45573
+ // Too many arguments — hand the call to the runtime, which applies
45574
+ // the lambda value and reports #VALUE! (Excel's answer) instead of
45575
+ // the parser choking on `(...)(...)`.
45251
45576
  const lambdaValue = transformLambdaToValue(
45252
45577
  source,
45253
45578
  tokens,
@@ -45265,9 +45590,9 @@ const inlineIIFE = (source, tokens, lambdaOpenIdx, lambdaCloseIdx) => {
45265
45590
  };
45266
45591
  }
45267
45592
  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.
45593
+ // Raw value — substituteNamesInBody decides the wrap. A parameter the
45594
+ // call left out binds to the omitted marker so ISOMITTED can see it
45595
+ // (and any other use of it is #VALUE!).
45271
45596
  const value =
45272
45597
  i < callArgSpans.length
45273
45598
  ? sourceSlice(source, tokens, callArgSpans[i]).trim()
@@ -45392,14 +45717,15 @@ const isLambdaValue = (v) =>
45392
45717
  v != null && typeof v === "object" && v.__isLambda === true;
45393
45718
 
45394
45719
  /**
45395
- * Marker bound to an optional `[param]` the caller left out. Only
45720
+ * Marker bound to a parameter the caller left out (Excel allows a call
45721
+ * with fewer arguments than parameters, `[param]` or not). Only
45396
45722
  * `ISOMITTED` looks at it; anywhere else it is an invalid operand.
45397
45723
  */
45398
45724
  const OMITTED = Object.freeze({ __isOmitted: true });
45399
45725
  const isOmittedValue = (v) =>
45400
45726
  v != null && typeof v === "object" && v.__isOmitted === true;
45401
45727
 
45402
- /** `[name]` → optional parameter, `name` → required. */
45728
+ /** `[name]` → parameter documented as optional, `name` → plain parameter. */
45403
45729
  const parseLambdaParams = (paramsStr) =>
45404
45730
  paramsStr
45405
45731
  .split(",")
@@ -45410,9 +45736,43 @@ const parseLambdaParams = (paramsStr) =>
45410
45736
  return { name: optional ? raw.slice(1, -1).trim() : raw, optional };
45411
45737
  });
45412
45738
 
45739
+ /**
45740
+ * An omitted marker that escapes `ISOMITTED` — a body that returns the
45741
+ * omitted parameter itself, or spreads it into an array — is `#VALUE!`
45742
+ * in the result, per cell inside an array. Arrays are only copied when
45743
+ * one of their cells actually holds the marker.
45744
+ */
45745
+ const rejectOmittedResult = (result) => {
45746
+ if (isOmittedValue(result)) return FormulaError.VALUE;
45747
+ if (!Array.isArray(result)) return result;
45748
+ let out = null;
45749
+ for (let i = 0; i < result.length; i++) {
45750
+ const cell = rejectOmittedResult(result[i]);
45751
+ if (cell !== result[i]) {
45752
+ if (out === null) out = result.slice();
45753
+ out[i] = cell;
45754
+ }
45755
+ }
45756
+ return out ?? result;
45757
+ };
45758
+
45413
45759
  const requiredParamCount = (lambda) =>
45414
45760
  lambda.params.filter((_, i) => !(lambda.optional?.[i] ?? false)).length;
45415
45761
 
45762
+ /**
45763
+ * Excel's "Incorrect Parameters" `#VALUE!` for MAP / REDUCE / SCAN / BYROW /
45764
+ * BYCOL / MAKEARRAY / GROUPBY / PIVOTBY: the helper drives the lambda with
45765
+ * a fixed number of arguments and the lambda must declare a parameter for
45766
+ * each of them. Only a `[name]` parameter may stay unsupplied — the
45767
+ * "fewer arguments than parameters" allowance is for direct calls, not
45768
+ * helpers. Returns the error for the whole helper call, or null when the
45769
+ * counts fit.
45770
+ */
45771
+ const hofArityError = (lambda, supplied) =>
45772
+ supplied < requiredParamCount(lambda) || supplied > lambda.params.length
45773
+ ? FormulaError.VALUE
45774
+ : null;
45775
+
45416
45776
  const formatLiteral = (value) => {
45417
45777
  if (value === null || value === undefined) return "0";
45418
45778
  if (value instanceof FormulaError) {
@@ -45455,12 +45815,11 @@ const formatLiteral = (value) => {
45455
45815
  */
45456
45816
  const applyLambda = async (context, lambda, args) => {
45457
45817
  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
- }
45818
+ // Excel rejects surplus arguments but accepts fewer than declared: the
45819
+ // missing parameters are omitted (ISOMITTED → TRUE, any other use →
45820
+ // #VALUE!). Helpers check their own parameter count with
45821
+ // `hofArityError` before getting here.
45822
+ if (args.length > lambda.params.length) return FormulaError.VALUE;
45464
45823
 
45465
45824
  context.__lambdaDepth = (context.__lambdaDepth ?? 0) + 1;
45466
45825
  if (context.__lambdaDepth > MAX_LAMBDA_DEPTH) {
@@ -45473,14 +45832,14 @@ const applyLambda = async (context, lambda, args) => {
45473
45832
  try {
45474
45833
  const bindings = lambda.params.map((name, i) => ({
45475
45834
  name,
45476
- // An omitted optional parameter binds to the omitted marker so
45835
+ // A parameter the call left out binds to the omitted marker so
45477
45836
  // ISOMITTED inside the body can report it.
45478
45837
  value:
45479
45838
  i < args.length ? `(${formatLiteral(args[i])})` : `(${OMITTED_CALL})`,
45480
45839
  }));
45481
45840
  const substituted = substituteNamesInBody(lambda.body, bindings);
45482
45841
  const result = await context.parseAsync(substituted, context.position, true);
45483
- return result;
45842
+ return rejectOmittedResult(result);
45484
45843
  } catch (e) {
45485
45844
  if (e instanceof FormulaError) return e;
45486
45845
  throw e;
@@ -45541,35 +45900,58 @@ const LambdaFunctions = {
45541
45900
  },
45542
45901
 
45543
45902
  /**
45544
- * Internal: the value spliced in for an optional `[param]` the caller
45545
- * left out (see the LAMBDA preprocessor / applyLambda).
45903
+ * Internal: the value spliced in for a parameter the caller left out
45904
+ * (see the LAMBDA preprocessor / applyLambda).
45546
45905
  */
45547
45906
  __RNC_OMITTED__: () => OMITTED,
45548
45907
 
45549
45908
  /**
45550
45909
  * 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!.
45910
+ * this for an inline `LAMBDA(...)(...)` with more arguments than
45911
+ * parameters, so the arity error surfaces as #VALUE!.
45553
45912
  */
45554
45913
  __RNC_CALL__: async (context, lambdaArg, ...args) =>
45555
45914
  applyLambda(context, unwrap(lambdaArg), args.map(unwrap)),
45556
45915
 
45557
45916
  /**
45558
- * ISOMITTED(argument) — TRUE when `argument` is an optional LAMBDA
45559
- * parameter the caller did not supply, FALSE otherwise.
45917
+ * ISOMITTED(argument) — TRUE when `argument` is a LAMBDA parameter the
45918
+ * caller did not supply (bracketed `[p]` or not), FALSE otherwise.
45560
45919
  */
45561
45920
  ISOMITTED: (arg) => isOmittedValue(unwrap(arg)),
45562
45921
 
45922
+ /**
45923
+ * A well-formed `LET(name, value, ..., body)` never reaches the function
45924
+ * table — the LET preprocessor inlines it. One that does is malformed:
45925
+ * too few arguments is `#N/A` (the too-few-arguments contract shared with
45926
+ * the Rust engine), an even count or a non-name binding is `#VALUE!`.
45927
+ */
45928
+ LET: (...args) => {
45929
+ if (args.length < 3) return FormulaError.NA;
45930
+ return FormulaError.VALUE;
45931
+ },
45932
+
45933
+ /**
45934
+ * Same for `LAMBDA(...)`: the preprocessor rewrites every LAMBDA with a
45935
+ * body, so only `LAMBDA()` (too few arguments, `#N/A`) and a body-less
45936
+ * `LAMBDA(x)` (a lambda value as the cell result, `#CALC!`) get here.
45937
+ */
45938
+ LAMBDA: (...args) => {
45939
+ if (args.length === 0) return FormulaError.NA;
45940
+ return FormulaError.CALC;
45941
+ },
45942
+
45563
45943
  /**
45564
45944
  * MAP(array1, [array2, ...], lambda) — applies lambda to each set
45565
45945
  * of corresponding elements. Returns a same-shape 2D array.
45566
45946
  */
45567
45947
  MAP: async (context, ...args) => {
45568
- if (args.length < 2) return FormulaError.VALUE;
45948
+ if (args.length < 2) return FormulaError.NA;
45569
45949
  const lambda = unwrap(args[args.length - 1]);
45570
45950
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
45571
45951
  const arrays = args.slice(0, -1).map((a) => as2D(unwrap(a)));
45572
45952
  if (arrays.length === 0) return FormulaError.VALUE;
45953
+ const arity = hofArityError(lambda, arrays.length);
45954
+ if (arity) return arity;
45573
45955
  const rows = arrays[0].length;
45574
45956
  const cols = arrays[0][0]?.length ?? 0;
45575
45957
  const out = [];
@@ -45591,6 +45973,8 @@ const LambdaFunctions = {
45591
45973
  REDUCE: async (context, initialArg, arrayArg, lambdaArg) => {
45592
45974
  const lambda = unwrap(lambdaArg);
45593
45975
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
45976
+ const arity = hofArityError(lambda, 2);
45977
+ if (arity) return arity;
45594
45978
  let acc = unwrap(initialArg);
45595
45979
  const values = flatten2D(as2D(unwrap(arrayArg)));
45596
45980
  for (const v of values) {
@@ -45608,6 +45992,8 @@ const LambdaFunctions = {
45608
45992
  SCAN: async (context, initialArg, arrayArg, lambdaArg) => {
45609
45993
  const lambda = unwrap(lambdaArg);
45610
45994
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
45995
+ const arity = hofArityError(lambda, 2);
45996
+ if (arity) return arity;
45611
45997
  let acc = unwrap(initialArg);
45612
45998
  const arr = as2D(unwrap(arrayArg));
45613
45999
  const out = [];
@@ -45629,8 +46015,11 @@ const LambdaFunctions = {
45629
46015
  * array), returns a column vector of results.
45630
46016
  */
45631
46017
  BYROW: async (context, arrayArg, lambdaArg) => {
46018
+ if (lambdaArg === undefined) return FormulaError.NA;
45632
46019
  const lambda = unwrap(lambdaArg);
45633
46020
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
46021
+ const arity = hofArityError(lambda, 1);
46022
+ if (arity) return arity;
45634
46023
  const arr = as2D(unwrap(arrayArg));
45635
46024
  const out = [];
45636
46025
  for (const row of arr) {
@@ -45646,8 +46035,11 @@ const LambdaFunctions = {
45646
46035
  * 1D array), returns a row vector of results.
45647
46036
  */
45648
46037
  BYCOL: async (context, arrayArg, lambdaArg) => {
46038
+ if (lambdaArg === undefined) return FormulaError.NA;
45649
46039
  const lambda = unwrap(lambdaArg);
45650
46040
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
46041
+ const arity = hofArityError(lambda, 1);
46042
+ if (arity) return arity;
45651
46043
  const arr = as2D(unwrap(arrayArg));
45652
46044
  const cols = arr[0]?.length ?? 0;
45653
46045
  const out = [];
@@ -45665,8 +46057,11 @@ const LambdaFunctions = {
45665
46057
  * lambda(r, c) for each cell. r, c are 1-indexed per Excel.
45666
46058
  */
45667
46059
  MAKEARRAY: async (context, rowsArg, colsArg, lambdaArg) => {
46060
+ if (lambdaArg === undefined) return FormulaError.NA;
45668
46061
  const lambda = unwrap(lambdaArg);
45669
46062
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
46063
+ const arity = hofArityError(lambda, 2);
46064
+ if (arity) return arity;
45670
46065
  const rows = Math.floor(
45671
46066
  FormulaHelpers.accept(rowsArg, Types.NUMBER, 0),
45672
46067
  );
@@ -45715,6 +46110,8 @@ const LambdaFunctions = {
45715
46110
  const valueFields = as2D(unwrap(args[1]));
45716
46111
  const lambda = unwrap(args[2]);
45717
46112
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
46113
+ const arity = hofArityError(lambda, 1);
46114
+ if (arity) return arity;
45718
46115
  const fieldHeaders = args[3] != null
45719
46116
  ? Math.floor(FormulaHelpers.accept(args[3], Types.NUMBER, 0))
45720
46117
  : 0;
@@ -45763,6 +46160,8 @@ const LambdaFunctions = {
45763
46160
  const valueFields = as2D(unwrap(args[2]));
45764
46161
  const lambda = unwrap(args[3]);
45765
46162
  if (!isLambdaValue(lambda)) return FormulaError.VALUE;
46163
+ const arity = hofArityError(lambda, 1);
46164
+ if (arity) return arity;
45766
46165
 
45767
46166
  const numOpt = (i, d) =>
45768
46167
  args[i] != null
@@ -46219,6 +46618,42 @@ let Utils$1 = class Utils {
46219
46618
  return Prefix.unaryOp(prefixes, val, isArray);
46220
46619
  }
46221
46620
 
46621
+ /**
46622
+ * Sign a numeric literal (`-1`, `+2` inside an array constant). Nothing
46623
+ * to dereference or await here, so this stays synchronous in both parse
46624
+ * modes.
46625
+ * @param {string} prefix - `+` or `-`
46626
+ * @param {number} number
46627
+ * @return {number}
46628
+ */
46629
+ applyLiteralPrefix(prefix, number) {
46630
+ return this._applyPrefix([prefix], number, false);
46631
+ }
46632
+
46633
+ /**
46634
+ * Under `parseAsync` an array-constant item built from a sub-expression
46635
+ * (`{SUM(1,-2),3}`) is a pending Promise; an array must only ever carry
46636
+ * values, so settle them. A settled item may itself be a reference
46637
+ * (`{INDEX(A1:C5,1,1),2}`), which the sync arm of `constantArray`
46638
+ * dereferences before the Promise ever existed — do the same here so both
46639
+ * parse modes agree. Returns the array itself when nothing is pending.
46640
+ * @param {Array.<Array>} array
46641
+ * @param {{row: number, col: number, sheet: string}} position
46642
+ * @return {Array.<Array>|Promise.<Array.<Array>>}
46643
+ */
46644
+ settleArrayItems(array, position) {
46645
+ const isPending = (item) =>
46646
+ item != null && typeof item.then === "function";
46647
+ if (!array.some((row) => row.some(isPending))) return array;
46648
+ const deref = (item) =>
46649
+ item && item.ref ? this.extractRefValue(item, position).val : item;
46650
+ return Promise.all(
46651
+ array.map((row) =>
46652
+ Promise.all(row).then((items) => items.map(deref))
46653
+ )
46654
+ );
46655
+ }
46656
+
46222
46657
  async applyPrefixAsync(prefixes, value, position) {
46223
46658
  const { val, isArray } = this.extractRefValue(await value, position);
46224
46659
  return this._applyPrefix(prefixes, val, isArray);
@@ -46413,15 +46848,21 @@ let Utils$1 = class Utils {
46413
46848
  }
46414
46849
  }
46415
46850
 
46416
- applyIntersect(refs) {
46851
+ applyIntersect(refs, position) {
46417
46852
  // console.log('applyIntersect', refs);
46418
46853
  if (this.isFormulaError(refs[0])) return refs[0];
46419
46854
  if (!refs[0].ref) throw Error(`Expecting a reference, but got ${refs[0]}.`);
46420
46855
  // a intersection will keep track of references, value won't be retrieved here.
46421
46856
  let maxRow, maxCol, minRow, minCol, sheet, res; // index start from 1
46857
+ // An unqualified operand (`B:B`, sheet undefined) lives on the formula's sheet: it must
46858
+ // intersect an operand that names that sheet explicitly (`Sheet1!A1:C3`, or a defined name
46859
+ // — the host resolves names WITH their sheet). Only a genuinely different sheet is #NULL!.
46860
+ const currentSheet = (position ?? this.context.position)?.sheet;
46861
+ const sheetOf = (ref) => ref.sheet ?? currentSheet;
46422
46862
  // first time setup
46423
46863
  const ref = refs.shift().ref;
46424
46864
  sheet = ref.sheet;
46865
+ const sheetKey = sheetOf(ref);
46425
46866
  if (!ref.from) {
46426
46867
  // check whole row/col reference
46427
46868
  if (ref.row === undefined || ref.col === undefined) {
@@ -46455,10 +46896,11 @@ let Utils$1 = class Utils {
46455
46896
  ref.row < minRow ||
46456
46897
  ref.col > maxCol ||
46457
46898
  ref.col < minCol ||
46458
- sheet !== ref.sheet
46899
+ sheetKey !== sheetOf(ref)
46459
46900
  ) {
46460
46901
  err = FormulaError.NULL;
46461
46902
  }
46903
+ sheet ??= ref.sheet;
46462
46904
  maxRow = minRow = ref.row;
46463
46905
  maxCol = minCol = ref.col;
46464
46906
  } else {
@@ -46472,10 +46914,11 @@ let Utils$1 = class Utils {
46472
46914
  refMaxRow < minRow ||
46473
46915
  refMinCol > maxCol ||
46474
46916
  refMaxCol < minCol ||
46475
- sheet !== ref.sheet
46917
+ sheetKey !== sheetOf(ref)
46476
46918
  ) {
46477
46919
  err = FormulaError.NULL;
46478
46920
  }
46921
+ sheet ??= ref.sheet;
46479
46922
  // update
46480
46923
  maxRow = Math.min(maxRow, refMaxRow);
46481
46924
  minRow = Math.max(minRow, refMinRow);
@@ -47476,6 +47919,9 @@ class FormulaParser {
47476
47919
  if (!this.keepLambdaResult && unwrapLambdaValue(result)) {
47477
47920
  return FormulaError.CALC;
47478
47921
  }
47922
+ // An inlined `LAMBDA(x,y,y)(1)` evaluates straight to the omitted
47923
+ // marker; only ISOMITTED may consume it, so as a result it is #VALUE!.
47924
+ result = rejectOmittedResult(result);
47479
47925
  const type = typeof result;
47480
47926
  // number
47481
47927
  if (type === "number") {
@@ -48681,5 +49127,5 @@ class DepParser {
48681
49127
  // Export a Set of implemented function names for O(1) lookup
48682
49128
  const implementedFunctionNames = FormulaParser.getImplementedFunctionNames();
48683
49129
 
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 };
49130
+ 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
49131
  //# sourceMappingURL=index.js.map