@rowsncolumns/fast-formula-parser 6.2.37 → 6.2.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/index.js CHANGED
@@ -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