@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/esm/index.js CHANGED
@@ -3879,6 +3879,80 @@ const IndexedColors = Object.freeze([
3879
3879
 
3880
3880
  const isSingleSectionElapsedTimePattern = (pattern) => !pattern.includes(";") &&
3881
3881
  /\[(?:h+|m+|s+)\]/i.test(pattern.replace(/\[\$[^\]]+\]/g, ""));
3882
+ const intlMonthNames = (locale, options) => {
3883
+ const formatter = new Intl.DateTimeFormat(locale, {
3884
+ ...options,
3885
+ timeZone: "UTC",
3886
+ });
3887
+ return Array.from({ length: 12 }, (_, month) => formatter
3888
+ .formatToParts(new Date(Date.UTC(2020, month, 1)))
3889
+ .find((part) => part.type === "month")?.value ?? "");
3890
+ };
3891
+ const intlRegisteredLocales = new Set();
3892
+ /**
3893
+ * numfmt ships name tables for a fixed set of languages; a tag it does not know
3894
+ * (et, ms, nn, …) silently renders English month/weekday names and en-US
3895
+ * separators. Excel and `Intl.DateTimeFormat` localize every locale, so an
3896
+ * unknown tag gets a numfmt locale built from Intl on first use: separators
3897
+ * from `Intl.NumberFormat`, the in-date month names (numfmt's single table is
3898
+ * the form used beside a day number — see `getStandaloneMonthLocale`), the
3899
+ * in-date weekday names (the form `dateStyle: "full"` shows) and the AM/PM
3900
+ * designators. Tags Intl itself does not know keep numfmt's fallback.
3901
+ */
3902
+ const ensureNumfmtLocale = (locale) => {
3903
+ if (intlRegisteredLocales.has(locale))
3904
+ return;
3905
+ intlRegisteredLocales.add(locale);
3906
+ try {
3907
+ if (getLocale(locale))
3908
+ return;
3909
+ const canonical = Intl.DateTimeFormat.supportedLocalesOf([locale])[0];
3910
+ if (!canonical)
3911
+ return;
3912
+ const numberParts = new Intl.NumberFormat(locale).formatToParts(1234567.5);
3913
+ const part = (type) => numberParts.find((p) => p.type === type)?.value;
3914
+ const weekdayNames = (weekday) => {
3915
+ // 2024-01-07 was a Sunday; numfmt indexes `dddd[0]` as Sunday.
3916
+ const fmt = new Intl.DateTimeFormat(locale, {
3917
+ weekday,
3918
+ day: "numeric",
3919
+ month: "long",
3920
+ year: "numeric",
3921
+ timeZone: "UTC",
3922
+ });
3923
+ return Array.from({ length: 7 }, (_, i) => fmt
3924
+ .formatToParts(new Date(Date.UTC(2024, 0, 7 + i)))
3925
+ .find((p) => p.type === "weekday")?.value ?? "");
3926
+ };
3927
+ const dayPeriod = (hour) => new Intl.DateTimeFormat(locale, {
3928
+ hour: "numeric",
3929
+ hour12: true,
3930
+ timeZone: "UTC",
3931
+ })
3932
+ .formatToParts(new Date(Date.UTC(2024, 0, 7, hour)))
3933
+ .find((p) => p.type === "dayPeriod")?.value;
3934
+ const dateParts = new Intl.DateTimeFormat(locale, {
3935
+ timeZone: "UTC",
3936
+ }).formatToParts(new Date(Date.UTC(2024, 0, 7)));
3937
+ const monthIndex = dateParts.findIndex((p) => p.type === "month");
3938
+ const dayIndex = dateParts.findIndex((p) => p.type === "day");
3939
+ const am = dayPeriod(9);
3940
+ const pm = dayPeriod(21);
3941
+ addLocale({
3942
+ group: part("group") ?? "\u00a0",
3943
+ decimal: part("decimal") ?? ".",
3944
+ mmmm: intlMonthNames(locale, { day: "numeric", month: "long" }),
3945
+ mmm: intlMonthNames(locale, { day: "numeric", month: "short" }),
3946
+ dddd: weekdayNames("long"),
3947
+ ddd: weekdayNames("short"),
3948
+ ...(am && pm && am !== pm ? { ampm: [am, pm] } : {}),
3949
+ preferMDY: monthIndex >= 0 && dayIndex >= 0 && monthIndex < dayIndex,
3950
+ }, locale);
3951
+ }
3952
+ catch {
3953
+ // Malformed tag: numfmt keeps its own fallback.
3954
+ }
3955
+ };
3882
3956
  const localeForPattern = (pattern, locale) => {
3883
3957
  return locale;
3884
3958
  };
@@ -4130,6 +4204,8 @@ const withWorkbookSeparatorsCached = (code, locale) => {
4130
4204
  };
4131
4205
  const ssfFormat = (pattern, value, locale) => {
4132
4206
  try {
4207
+ if (locale)
4208
+ ;
4133
4209
  let shortAmPm = shortAmPmPatternCache.get(pattern);
4134
4210
  if (shortAmPm === undefined && !shortAmPmPatternCache.has(pattern)) {
4135
4211
  shortAmPm = withShortAmPmMarker(pattern);
@@ -12584,7 +12660,10 @@ const forecastEtsPrep = (ys, xs, seasonality) => {
12584
12660
  if (typeof yv !== "number") continue;
12585
12661
  pairs.push([x, yv]);
12586
12662
  }
12587
- if (pairs.length < 4) throw FormulaError.NA;
12663
+ // Excel fits from two pairs (there is no four-pair floor); fewer is
12664
+ // #N/A, and two pairs on the same timeline point have no step (#NUM!
12665
+ // below). rowsncolumns/spreadsheet#642 B-01.
12666
+ if (pairs.length < 2) throw FormulaError.NA;
12588
12667
  pairs.sort((a, b) => a[0] - b[0]);
12589
12668
  const sortedX = pairs.map(p => p[0]);
12590
12669
  const y = pairs.map(p => p[1]);
@@ -12622,7 +12701,7 @@ const detectSeasonality = (y) => {
12622
12701
  const n = y.length;
12623
12702
  if (n < 4) return 1;
12624
12703
  const maxPeriod = Math.min(Math.floor(n / 2), 12);
12625
- const baseline = runHoltWinters(y, 1, 0.5, 0.1, 0, 0).sse;
12704
+ const baseline = runHoltWinters(y, 1, 0.5, 0.1, 0).sse;
12626
12705
  let bestPeriod = 1;
12627
12706
  let bestSse = baseline;
12628
12707
  for (let m = 2; m <= maxPeriod; m++) {
@@ -12633,7 +12712,7 @@ const detectSeasonality = (y) => {
12633
12712
  for (const alpha of [0.3, 0.7]) {
12634
12713
  for (const beta of [0.1, 0.3]) {
12635
12714
  for (const gamma of [0.3, 0.7]) {
12636
- const sse = runHoltWinters(y, m, alpha, beta, gamma, 0).sse;
12715
+ const sse = runHoltWinters(y, m, alpha, beta, gamma).sse;
12637
12716
  if (sse < mSse) mSse = sse;
12638
12717
  }
12639
12718
  }
@@ -12649,11 +12728,10 @@ const detectSeasonality = (y) => {
12649
12728
  };
12650
12729
 
12651
12730
  // Fit Holt-Winters additive model with the supplied (or auto-fit) α,
12652
- // β, γ smoothing parameters. Returns fitted in-sample values + the
12653
- // `horizon`-step-ahead forecast. period=1 collapses to Holt linear
12654
- // (no seasonal component).
12655
- const fitHoltWinters = (y, period, horizon) => {
12656
- y.length;
12731
+ // β, γ smoothing parameters. Returns the fitted in-sample values and the
12732
+ // terminal state (`forecastAt(h)` extrapolates it to any horizon in O(1)).
12733
+ // period=1 collapses to Holt linear (no seasonal component).
12734
+ const fitHoltWinters = (y, period) => {
12657
12735
  // Grid search the smoothing params. The 5×5×5 grid is coarse but
12658
12736
  // close enough to the SSE-minimizing optimum that Excel's results
12659
12737
  // round-match within ~1% on the seasonal demo series.
@@ -12663,7 +12741,7 @@ const fitHoltWinters = (y, period, horizon) => {
12663
12741
  for (const alpha of grid) {
12664
12742
  for (const beta of grid) {
12665
12743
  for (const gamma of candidateGammas) {
12666
- const result = runHoltWinters(y, period, alpha, beta, gamma, horizon);
12744
+ const result = runHoltWinters(y, period, alpha, beta, gamma);
12667
12745
  if (best == null || result.sse < best.sse) {
12668
12746
  best = { alpha, beta, gamma, ...result };
12669
12747
  }
@@ -12673,7 +12751,25 @@ const fitHoltWinters = (y, period, horizon) => {
12673
12751
  return best;
12674
12752
  };
12675
12753
 
12676
- const runHoltWinters = (y, period, alpha, beta, gamma, horizon) => {
12754
+ // The forecast `h` whole steps past the last observation from a run's
12755
+ // terminal state: level + h·trend + seasonal[(n + h - 1) mod m]. O(1) in
12756
+ // `h` — the old per-step forecast array made `FORECAST.ETS(1E15, …)`
12757
+ // hang the tab (and trapped the Rust engine's wasm with a capacity
12758
+ // overflow that poisoned the whole store). rowsncolumns/spreadsheet#642 B-06.
12759
+ const holtWintersForecastAt = ({ n, m, level, trend, seasonal }, h) => {
12760
+ const seasonComp = m > 1 ? seasonal[(n + h - 1) % m] : 0;
12761
+ return level + h * trend + seasonComp;
12762
+ };
12763
+
12764
+ // One additive Holt-Winters pass with fixed smoothing parameters. The
12765
+ // initial level is the level one step *before* the series (first value
12766
+ // or first-cycle mean, minus the trend), so the one-step-ahead prediction
12767
+ // of the first observation is the observation itself and a perfectly
12768
+ // linear history has zero residual — an initial level *at* the first
12769
+ // value predicted y₀ + trend for y₀, and that spurious residual pulled
12770
+ // the grid search off the exact continuation (10,20,…,100 → 109.56
12771
+ // instead of Excel's 110). rowsncolumns/spreadsheet#642 B-01.
12772
+ const runHoltWinters = (y, period, alpha, beta, gamma) => {
12677
12773
  const n = y.length;
12678
12774
  const m = period;
12679
12775
  const fitted = new Array(n).fill(null);
@@ -12681,21 +12777,21 @@ const runHoltWinters = (y, period, alpha, beta, gamma, horizon) => {
12681
12777
  const seasonal = new Array(m).fill(0);
12682
12778
  if (m > 1 && n >= 2 * m) {
12683
12779
  // Initial seasonal indices = first-cycle deviations from the
12684
- // first-cycle mean. Initial level = mean of first cycle.
12685
- // Initial trend = average of per-step changes between first
12686
- // and second cycle, normalized by m.
12780
+ // first-cycle mean. Initial trend = average of per-step changes
12781
+ // between first and second cycle, normalized by m. Initial level
12782
+ // = mean of first cycle, one trend step back.
12687
12783
  let sum1 = 0;
12688
12784
  for (let i = 0; i < m; i++) sum1 += y[i];
12689
12785
  const mean1 = sum1 / m;
12690
- level = mean1;
12691
12786
  let trendSum = 0;
12692
12787
  for (let i = 0; i < m; i++) trendSum += (y[i + m] - y[i]) / m;
12693
12788
  trend = trendSum / m;
12789
+ level = mean1 - trend;
12694
12790
  for (let i = 0; i < m; i++) seasonal[i] = y[i] - mean1;
12695
12791
  } else {
12696
- // Holt linear initialization.
12697
- level = y[0];
12792
+ // Holt linear initialization, one trend step back.
12698
12793
  trend = n > 1 ? y[1] - y[0] : 0;
12794
+ level = y[0] - trend;
12699
12795
  }
12700
12796
  let sse = 0;
12701
12797
  let count = 0;
@@ -12714,12 +12810,12 @@ const runHoltWinters = (y, period, alpha, beta, gamma, horizon) => {
12714
12810
  seasonal[seasonIdx] = gamma * (y[t] - level) + (1 - gamma) * seasonComp;
12715
12811
  }
12716
12812
  }
12717
- const forecast = [];
12718
- for (let h = 1; h <= horizon; h++) {
12719
- const seasonComp = m > 1 ? seasonal[(n + h - 1) % m] : 0;
12720
- forecast.push(level + h * trend + seasonComp);
12721
- }
12722
- return { fitted, forecast, sse: count > 0 ? sse / count : Infinity };
12813
+ const state = { n, m, level, trend, seasonal };
12814
+ return {
12815
+ fitted,
12816
+ sse: count > 0 ? sse / count : Infinity,
12817
+ forecastAt: (h) => holtWintersForecastAt(state, h),
12818
+ };
12723
12819
  };
12724
12820
 
12725
12821
  const percentileInclusive = (values, k) => {
@@ -13504,9 +13600,11 @@ const DistributionFunctions = {
13504
13600
  throw FormulaError.NUM;
13505
13601
  }
13506
13602
  const horizon = Math.round((target - lastX) / step);
13603
+ if (!Number.isFinite(horizon)) throw FormulaError.NUM;
13507
13604
  if (horizon < 1) return y[y.length - 1];
13508
- const { fitted: _f, forecast } = fitHoltWinters(y, period, horizon);
13509
- return forecast[horizon - 1];
13605
+ const forecast = fitHoltWinters(y, period).forecastAt(horizon);
13606
+ if (!Number.isFinite(forecast)) throw FormulaError.NUM;
13607
+ return forecast;
13510
13608
  },
13511
13609
 
13512
13610
  'FORECAST.ETS.CONFINT': (targetDate, values, timeline, confidenceArg, seasonalityArg, dataCompletionArg, aggregationArg) => {
@@ -13522,7 +13620,8 @@ const DistributionFunctions = {
13522
13620
  const { y, lastX, step, period } = forecastEtsPrep(ys, xs, seasonality);
13523
13621
  if (target < lastX) throw FormulaError.NUM;
13524
13622
  const horizon = Math.max(1, Math.round((target - lastX) / step));
13525
- const { fitted, forecast: _fc } = fitHoltWinters(y, period, horizon);
13623
+ if (!Number.isFinite(horizon)) throw FormulaError.NUM;
13624
+ const { fitted } = fitHoltWinters(y, period);
13526
13625
  // RMSE of in-sample residuals — z-scaled to the requested
13527
13626
  // confidence level. Excel uses √(horizon) scaling on the
13528
13627
  // forecast variance.
@@ -13535,7 +13634,9 @@ const DistributionFunctions = {
13535
13634
  }
13536
13635
  const rmse = n > 0 ? Math.sqrt(sse / n) : 0;
13537
13636
  const z = jStat.normal.inv(1 - (1 - confidence) / 2, 0, 1);
13538
- return z * rmse * Math.sqrt(horizon);
13637
+ const halfWidth = z * rmse * Math.sqrt(horizon);
13638
+ if (!Number.isFinite(halfWidth)) throw FormulaError.NUM;
13639
+ return halfWidth;
13539
13640
  },
13540
13641
 
13541
13642
  'FORECAST.ETS.SEASONALITY': (values, timeline, dataCompletionArg, aggregationArg) => {
@@ -13554,7 +13655,7 @@ const DistributionFunctions = {
13554
13655
  const seasonality = forecastEtsSeasonality(seasonalityArg);
13555
13656
  forecastEtsValidateOptions(dataCompletionArg, aggregationArg);
13556
13657
  const { y, period } = forecastEtsPrep(ys, xs, seasonality);
13557
- const { alpha, beta, gamma, fitted } = fitHoltWinters(y, period, 0);
13658
+ const { alpha, beta, gamma, fitted } = fitHoltWinters(y, period);
13558
13659
  // Excel STAT codes:
13559
13660
  // 1: alpha 2: beta 3: gamma 4: MASE
13560
13661
  // 5: SMAPE 6: MAE 7: RMSE 8: detected step size
@@ -18015,6 +18116,11 @@ const applyToArray$1 = (value, checkFn) => {
18015
18116
  * Get the number of values that evaluate to true and false.
18016
18117
  * Cast Number and "TRUE", "FALSE" to boolean.
18017
18118
  * Ignore unrelated values.
18119
+ * An error value — a direct argument (`AND(1/0, FALSE)`) or a cell inside a
18120
+ * reference — is the result, whatever the other arguments decide: Excel never
18121
+ * short-circuits AND/OR/XOR past an error, and the first one in argument order
18122
+ * wins (rowsncolumns/spreadsheet#642 B-08). `flattenParams` only throws error
18123
+ * values itself when a value type is requested.
18018
18124
  * @ignore
18019
18125
  * @param {any[]} params
18020
18126
  * @return {number[]}
@@ -18022,6 +18128,8 @@ const applyToArray$1 = (value, checkFn) => {
18022
18128
  function getNumLogicalValue(params) {
18023
18129
  let numTrue = 0, numFalse = 0;
18024
18130
  H$4.flattenParams(params, null, true, (val, info) => {
18131
+ if (val instanceof FormulaError)
18132
+ throw val;
18025
18133
  const type = typeof val;
18026
18134
  if (type === "string") {
18027
18135
  // Text inside a reference or array is ignored; a direct text