@rowsncolumns/fast-formula-parser 6.2.37 → 6.2.39

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
@@ -3883,6 +3883,80 @@ const IndexedColors = Object.freeze([
3883
3883
 
3884
3884
  const isSingleSectionElapsedTimePattern = (pattern) => !pattern.includes(";") &&
3885
3885
  /\[(?:h+|m+|s+)\]/i.test(pattern.replace(/\[\$[^\]]+\]/g, ""));
3886
+ const intlMonthNames = (locale, options) => {
3887
+ const formatter = new Intl.DateTimeFormat(locale, {
3888
+ ...options,
3889
+ timeZone: "UTC",
3890
+ });
3891
+ return Array.from({ length: 12 }, (_, month) => formatter
3892
+ .formatToParts(new Date(Date.UTC(2020, month, 1)))
3893
+ .find((part) => part.type === "month")?.value ?? "");
3894
+ };
3895
+ const intlRegisteredLocales = new Set();
3896
+ /**
3897
+ * numfmt ships name tables for a fixed set of languages; a tag it does not know
3898
+ * (et, ms, nn, …) silently renders English month/weekday names and en-US
3899
+ * separators. Excel and `Intl.DateTimeFormat` localize every locale, so an
3900
+ * unknown tag gets a numfmt locale built from Intl on first use: separators
3901
+ * from `Intl.NumberFormat`, the in-date month names (numfmt's single table is
3902
+ * the form used beside a day number — see `getStandaloneMonthLocale`), the
3903
+ * in-date weekday names (the form `dateStyle: "full"` shows) and the AM/PM
3904
+ * designators. Tags Intl itself does not know keep numfmt's fallback.
3905
+ */
3906
+ const ensureNumfmtLocale = (locale) => {
3907
+ if (intlRegisteredLocales.has(locale))
3908
+ return;
3909
+ intlRegisteredLocales.add(locale);
3910
+ try {
3911
+ if (getLocale(locale))
3912
+ return;
3913
+ const canonical = Intl.DateTimeFormat.supportedLocalesOf([locale])[0];
3914
+ if (!canonical)
3915
+ return;
3916
+ const numberParts = new Intl.NumberFormat(locale).formatToParts(1234567.5);
3917
+ const part = (type) => numberParts.find((p) => p.type === type)?.value;
3918
+ const weekdayNames = (weekday) => {
3919
+ // 2024-01-07 was a Sunday; numfmt indexes `dddd[0]` as Sunday.
3920
+ const fmt = new Intl.DateTimeFormat(locale, {
3921
+ weekday,
3922
+ day: "numeric",
3923
+ month: "long",
3924
+ year: "numeric",
3925
+ timeZone: "UTC",
3926
+ });
3927
+ return Array.from({ length: 7 }, (_, i) => fmt
3928
+ .formatToParts(new Date(Date.UTC(2024, 0, 7 + i)))
3929
+ .find((p) => p.type === "weekday")?.value ?? "");
3930
+ };
3931
+ const dayPeriod = (hour) => new Intl.DateTimeFormat(locale, {
3932
+ hour: "numeric",
3933
+ hour12: true,
3934
+ timeZone: "UTC",
3935
+ })
3936
+ .formatToParts(new Date(Date.UTC(2024, 0, 7, hour)))
3937
+ .find((p) => p.type === "dayPeriod")?.value;
3938
+ const dateParts = new Intl.DateTimeFormat(locale, {
3939
+ timeZone: "UTC",
3940
+ }).formatToParts(new Date(Date.UTC(2024, 0, 7)));
3941
+ const monthIndex = dateParts.findIndex((p) => p.type === "month");
3942
+ const dayIndex = dateParts.findIndex((p) => p.type === "day");
3943
+ const am = dayPeriod(9);
3944
+ const pm = dayPeriod(21);
3945
+ addLocale({
3946
+ group: part("group") ?? "\u00a0",
3947
+ decimal: part("decimal") ?? ".",
3948
+ mmmm: intlMonthNames(locale, { day: "numeric", month: "long" }),
3949
+ mmm: intlMonthNames(locale, { day: "numeric", month: "short" }),
3950
+ dddd: weekdayNames("long"),
3951
+ ddd: weekdayNames("short"),
3952
+ ...(am && pm && am !== pm ? { ampm: [am, pm] } : {}),
3953
+ preferMDY: monthIndex >= 0 && dayIndex >= 0 && monthIndex < dayIndex,
3954
+ }, locale);
3955
+ }
3956
+ catch {
3957
+ // Malformed tag: numfmt keeps its own fallback.
3958
+ }
3959
+ };
3886
3960
  const localeForPattern = (pattern, locale) => {
3887
3961
  return locale;
3888
3962
  };
@@ -4134,6 +4208,8 @@ const withWorkbookSeparatorsCached = (code, locale) => {
4134
4208
  };
4135
4209
  const ssfFormat = (pattern, value, locale) => {
4136
4210
  try {
4211
+ if (locale)
4212
+ ;
4137
4213
  let shortAmPm = shortAmPmPatternCache.get(pattern);
4138
4214
  if (shortAmPm === undefined && !shortAmPmPatternCache.has(pattern)) {
4139
4215
  shortAmPm = withShortAmPmMarker(pattern);
@@ -12588,7 +12664,10 @@ const forecastEtsPrep = (ys, xs, seasonality) => {
12588
12664
  if (typeof yv !== "number") continue;
12589
12665
  pairs.push([x, yv]);
12590
12666
  }
12591
- if (pairs.length < 4) throw FormulaError.NA;
12667
+ // Excel fits from two pairs (there is no four-pair floor); fewer is
12668
+ // #N/A, and two pairs on the same timeline point have no step (#NUM!
12669
+ // below). rowsncolumns/spreadsheet#642 B-01.
12670
+ if (pairs.length < 2) throw FormulaError.NA;
12592
12671
  pairs.sort((a, b) => a[0] - b[0]);
12593
12672
  const sortedX = pairs.map(p => p[0]);
12594
12673
  const y = pairs.map(p => p[1]);
@@ -12626,7 +12705,7 @@ const detectSeasonality = (y) => {
12626
12705
  const n = y.length;
12627
12706
  if (n < 4) return 1;
12628
12707
  const maxPeriod = Math.min(Math.floor(n / 2), 12);
12629
- const baseline = runHoltWinters(y, 1, 0.5, 0.1, 0, 0).sse;
12708
+ const baseline = runHoltWinters(y, 1, 0.5, 0.1, 0).sse;
12630
12709
  let bestPeriod = 1;
12631
12710
  let bestSse = baseline;
12632
12711
  for (let m = 2; m <= maxPeriod; m++) {
@@ -12637,7 +12716,7 @@ const detectSeasonality = (y) => {
12637
12716
  for (const alpha of [0.3, 0.7]) {
12638
12717
  for (const beta of [0.1, 0.3]) {
12639
12718
  for (const gamma of [0.3, 0.7]) {
12640
- const sse = runHoltWinters(y, m, alpha, beta, gamma, 0).sse;
12719
+ const sse = runHoltWinters(y, m, alpha, beta, gamma).sse;
12641
12720
  if (sse < mSse) mSse = sse;
12642
12721
  }
12643
12722
  }
@@ -12653,11 +12732,10 @@ const detectSeasonality = (y) => {
12653
12732
  };
12654
12733
 
12655
12734
  // Fit Holt-Winters additive model with the supplied (or auto-fit) α,
12656
- // β, γ smoothing parameters. Returns fitted in-sample values + the
12657
- // `horizon`-step-ahead forecast. period=1 collapses to Holt linear
12658
- // (no seasonal component).
12659
- const fitHoltWinters = (y, period, horizon) => {
12660
- y.length;
12735
+ // β, γ smoothing parameters. Returns the fitted in-sample values and the
12736
+ // terminal state (`forecastAt(h)` extrapolates it to any horizon in O(1)).
12737
+ // period=1 collapses to Holt linear (no seasonal component).
12738
+ const fitHoltWinters = (y, period) => {
12661
12739
  // Grid search the smoothing params. The 5×5×5 grid is coarse but
12662
12740
  // close enough to the SSE-minimizing optimum that Excel's results
12663
12741
  // round-match within ~1% on the seasonal demo series.
@@ -12667,7 +12745,7 @@ const fitHoltWinters = (y, period, horizon) => {
12667
12745
  for (const alpha of grid) {
12668
12746
  for (const beta of grid) {
12669
12747
  for (const gamma of candidateGammas) {
12670
- const result = runHoltWinters(y, period, alpha, beta, gamma, horizon);
12748
+ const result = runHoltWinters(y, period, alpha, beta, gamma);
12671
12749
  if (best == null || result.sse < best.sse) {
12672
12750
  best = { alpha, beta, gamma, ...result };
12673
12751
  }
@@ -12677,7 +12755,25 @@ const fitHoltWinters = (y, period, horizon) => {
12677
12755
  return best;
12678
12756
  };
12679
12757
 
12680
- const runHoltWinters = (y, period, alpha, beta, gamma, horizon) => {
12758
+ // The forecast `h` whole steps past the last observation from a run's
12759
+ // terminal state: level + h·trend + seasonal[(n + h - 1) mod m]. O(1) in
12760
+ // `h` — the old per-step forecast array made `FORECAST.ETS(1E15, …)`
12761
+ // hang the tab (and trapped the Rust engine's wasm with a capacity
12762
+ // overflow that poisoned the whole store). rowsncolumns/spreadsheet#642 B-06.
12763
+ const holtWintersForecastAt = ({ n, m, level, trend, seasonal }, h) => {
12764
+ const seasonComp = m > 1 ? seasonal[(n + h - 1) % m] : 0;
12765
+ return level + h * trend + seasonComp;
12766
+ };
12767
+
12768
+ // One additive Holt-Winters pass with fixed smoothing parameters. The
12769
+ // initial level is the level one step *before* the series (first value
12770
+ // or first-cycle mean, minus the trend), so the one-step-ahead prediction
12771
+ // of the first observation is the observation itself and a perfectly
12772
+ // linear history has zero residual — an initial level *at* the first
12773
+ // value predicted y₀ + trend for y₀, and that spurious residual pulled
12774
+ // the grid search off the exact continuation (10,20,…,100 → 109.56
12775
+ // instead of Excel's 110). rowsncolumns/spreadsheet#642 B-01.
12776
+ const runHoltWinters = (y, period, alpha, beta, gamma) => {
12681
12777
  const n = y.length;
12682
12778
  const m = period;
12683
12779
  const fitted = new Array(n).fill(null);
@@ -12685,21 +12781,21 @@ const runHoltWinters = (y, period, alpha, beta, gamma, horizon) => {
12685
12781
  const seasonal = new Array(m).fill(0);
12686
12782
  if (m > 1 && n >= 2 * m) {
12687
12783
  // Initial seasonal indices = first-cycle deviations from the
12688
- // first-cycle mean. Initial level = mean of first cycle.
12689
- // Initial trend = average of per-step changes between first
12690
- // and second cycle, normalized by m.
12784
+ // first-cycle mean. Initial trend = average of per-step changes
12785
+ // between first and second cycle, normalized by m. Initial level
12786
+ // = mean of first cycle, one trend step back.
12691
12787
  let sum1 = 0;
12692
12788
  for (let i = 0; i < m; i++) sum1 += y[i];
12693
12789
  const mean1 = sum1 / m;
12694
- level = mean1;
12695
12790
  let trendSum = 0;
12696
12791
  for (let i = 0; i < m; i++) trendSum += (y[i + m] - y[i]) / m;
12697
12792
  trend = trendSum / m;
12793
+ level = mean1 - trend;
12698
12794
  for (let i = 0; i < m; i++) seasonal[i] = y[i] - mean1;
12699
12795
  } else {
12700
- // Holt linear initialization.
12701
- level = y[0];
12796
+ // Holt linear initialization, one trend step back.
12702
12797
  trend = n > 1 ? y[1] - y[0] : 0;
12798
+ level = y[0] - trend;
12703
12799
  }
12704
12800
  let sse = 0;
12705
12801
  let count = 0;
@@ -12718,12 +12814,12 @@ const runHoltWinters = (y, period, alpha, beta, gamma, horizon) => {
12718
12814
  seasonal[seasonIdx] = gamma * (y[t] - level) + (1 - gamma) * seasonComp;
12719
12815
  }
12720
12816
  }
12721
- const forecast = [];
12722
- for (let h = 1; h <= horizon; h++) {
12723
- const seasonComp = m > 1 ? seasonal[(n + h - 1) % m] : 0;
12724
- forecast.push(level + h * trend + seasonComp);
12725
- }
12726
- return { fitted, forecast, sse: count > 0 ? sse / count : Infinity };
12817
+ const state = { n, m, level, trend, seasonal };
12818
+ return {
12819
+ fitted,
12820
+ sse: count > 0 ? sse / count : Infinity,
12821
+ forecastAt: (h) => holtWintersForecastAt(state, h),
12822
+ };
12727
12823
  };
12728
12824
 
12729
12825
  const percentileInclusive = (values, k) => {
@@ -13508,9 +13604,11 @@ const DistributionFunctions = {
13508
13604
  throw FormulaError.NUM;
13509
13605
  }
13510
13606
  const horizon = Math.round((target - lastX) / step);
13607
+ if (!Number.isFinite(horizon)) throw FormulaError.NUM;
13511
13608
  if (horizon < 1) return y[y.length - 1];
13512
- const { fitted: _f, forecast } = fitHoltWinters(y, period, horizon);
13513
- return forecast[horizon - 1];
13609
+ const forecast = fitHoltWinters(y, period).forecastAt(horizon);
13610
+ if (!Number.isFinite(forecast)) throw FormulaError.NUM;
13611
+ return forecast;
13514
13612
  },
13515
13613
 
13516
13614
  'FORECAST.ETS.CONFINT': (targetDate, values, timeline, confidenceArg, seasonalityArg, dataCompletionArg, aggregationArg) => {
@@ -13526,7 +13624,8 @@ const DistributionFunctions = {
13526
13624
  const { y, lastX, step, period } = forecastEtsPrep(ys, xs, seasonality);
13527
13625
  if (target < lastX) throw FormulaError.NUM;
13528
13626
  const horizon = Math.max(1, Math.round((target - lastX) / step));
13529
- const { fitted, forecast: _fc } = fitHoltWinters(y, period, horizon);
13627
+ if (!Number.isFinite(horizon)) throw FormulaError.NUM;
13628
+ const { fitted } = fitHoltWinters(y, period);
13530
13629
  // RMSE of in-sample residuals — z-scaled to the requested
13531
13630
  // confidence level. Excel uses √(horizon) scaling on the
13532
13631
  // forecast variance.
@@ -13539,7 +13638,9 @@ const DistributionFunctions = {
13539
13638
  }
13540
13639
  const rmse = n > 0 ? Math.sqrt(sse / n) : 0;
13541
13640
  const z = jStat.normal.inv(1 - (1 - confidence) / 2, 0, 1);
13542
- return z * rmse * Math.sqrt(horizon);
13641
+ const halfWidth = z * rmse * Math.sqrt(horizon);
13642
+ if (!Number.isFinite(halfWidth)) throw FormulaError.NUM;
13643
+ return halfWidth;
13543
13644
  },
13544
13645
 
13545
13646
  'FORECAST.ETS.SEASONALITY': (values, timeline, dataCompletionArg, aggregationArg) => {
@@ -13558,7 +13659,7 @@ const DistributionFunctions = {
13558
13659
  const seasonality = forecastEtsSeasonality(seasonalityArg);
13559
13660
  forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
13560
13661
  const { y, period } = forecastEtsPrep(ys, xs, seasonality);
13561
- const { alpha, beta, gamma, fitted } = fitHoltWinters(y, period, 0);
13662
+ const { alpha, beta, gamma, fitted } = fitHoltWinters(y, period);
13562
13663
  // Excel STAT codes:
13563
13664
  // 1: alpha 2: beta 3: gamma 4: MASE
13564
13665
  // 5: SMAPE 6: MAE 7: RMSE 8: detected step size
@@ -18019,6 +18120,11 @@ const applyToArray$1 = (value, checkFn) => {
18019
18120
  * Get the number of values that evaluate to true and false.
18020
18121
  * Cast Number and "TRUE", "FALSE" to boolean.
18021
18122
  * Ignore unrelated values.
18123
+ * An error value — a direct argument (`AND(1/0, FALSE)`) or a cell inside a
18124
+ * reference — is the result, whatever the other arguments decide: Excel never
18125
+ * short-circuits AND/OR/XOR past an error, and the first one in argument order
18126
+ * wins (rowsncolumns/spreadsheet#642 B-08). `flattenParams` only throws error
18127
+ * values itself when a value type is requested.
18022
18128
  * @ignore
18023
18129
  * @param {any[]} params
18024
18130
  * @return {number[]}
@@ -18026,6 +18132,8 @@ const applyToArray$1 = (value, checkFn) => {
18026
18132
  function getNumLogicalValue(params) {
18027
18133
  let numTrue = 0, numFalse = 0;
18028
18134
  H$4.flattenParams(params, null, true, (val, info) => {
18135
+ if (val instanceof FormulaError)
18136
+ throw val;
18029
18137
  const type = typeof val;
18030
18138
  if (type === "string") {
18031
18139
  // Text inside a reference or array is ignored; a direct text
@@ -44190,9 +44298,16 @@ const FormulaErrorT$1 = createToken({
44190
44298
  /#NULL!|#DIV\/0!|#VALUE!|#NAME\?|#NUM!|#N\/A|#GETTING_DATA|#SPILL!|#CONNECT!|#BLOCKED!|#UNKNOWN!|#FIELD!|#CALC!|#EXTERNAL!/,
44191
44299
  });
44192
44300
 
44301
+ // `Table1[#REF!]` / `Table1[@#REF!]` — the text Excel writes once the referenced table column is
44302
+ // deleted — carries `#REF!` as a COLUMN specifier: `longer_alt` lets `TableColumnName` (defined
44303
+ // below) claim `#REF!]` when the error token is directly closed by `]`. A bare `#REF!` elsewhere
44304
+ // (`=SUM(#REF!)`) stays the error literal.
44305
+ // `#REF!#` is what a structural delete leaves of a spilled-range reference whose anchor row/column
44306
+ // went away (`=C1#` → `=#REF!#`); the postfix stays on the error token so the text still parses
44307
+ // (to the `#REF!` error) instead of an `#ERROR!` syntax failure.
44193
44308
  const RefError$1 = createToken({
44194
44309
  name: "RefError",
44195
- pattern: /#REF!/,
44310
+ pattern: /#REF!#?/,
44196
44311
  });
44197
44312
 
44198
44313
  const Name$1 = createToken({
@@ -44219,12 +44334,17 @@ const Sheet$1 = createToken({
44219
44334
  // `Sales[Q1 '[est']]` for the column named `Q1 [est]`. Defined ahead of `Cell`
44220
44335
  // so it can be its `longer_alt`: a column named like a cell address (`Q1`,
44221
44336
  // `FY24`) must lex as the column specifier, not as a cell reference.
44337
+ // `#REF!` (Excel's specifier for a deleted column) is the one `#`-form admitted as a column name;
44338
+ // every other `#…` inside brackets is a `SpecialItem`.
44222
44339
  const TableColumnName$1 = createToken({
44223
44340
  name: "TableColumnName",
44224
44341
  pattern:
44225
- /(?!,)[\[]?[@]?[\[+]?(?:[@?%A-Za-z_.\d\s(),!@$%^&*+=\-\/$\u007F-\uFFFF]|'[\[\]#'])+\]+(?:(?:\.[a-zA-Z_][a-zA-Z0-9_~?]*(?:\[[0-9]+\])*)+(?:\.(?![^,\s)]))?|\.(?![^,\s)]))?/,
44342
+ /(?!,)[\[]?[@]?[\[+]?(?:#REF!|(?:[@?%A-Za-z_.\d\s(),!@$%^&*+=\-\/$\u007F-\uFFFF]|'[\[\]#'])+)\]+(?:(?:\.[a-zA-Z_][a-zA-Z0-9_~?]*(?:\[[0-9]+\])*)+(?:\.(?![^,\s)]))?|\.(?![^,\s)]))?/,
44226
44343
  });
44227
44344
 
44345
+ // Declared after `TableColumnName` exists (see the `RefError` note above).
44346
+ RefError$1.LONGER_ALT = TableColumnName$1;
44347
+
44228
44348
  const Cell$1 = createToken({
44229
44349
  name: "Cell",
44230
44350
  // Trailing `#` is Excel's "spilled-range operator": e.g. `A1#` resolves
@@ -44890,7 +45010,8 @@ class Parser extends EmbeddedActionsParser {
44890
45010
  },
44891
45011
  {
44892
45012
  ALT: () => {
44893
- const err = $.CONSUME(RefError).image;
45013
+ // `#REF!#` (a broken spilled-range reference) is the `#REF!` error.
45014
+ const err = $.CONSUME(RefError).image.replace(/#$/, "");
44894
45015
  return $.ACTION(() => this.utils.toError(err));
44895
45016
  },
44896
45017
  },
@@ -45077,7 +45198,8 @@ class Parser extends EmbeddedActionsParser {
45077
45198
  // parse the number to row number when needed.
45078
45199
  {
45079
45200
  ALT: () => {
45080
- const err = $.CONSUME(RefError).image;
45201
+ // `#REF!#` (a broken spilled-range reference) is the `#REF!` error.
45202
+ const err = $.CONSUME(RefError).image.replace(/#$/, "");
45081
45203
  return $.ACTION(() => this.utils.toError(err));
45082
45204
  },
45083
45205
  },