Skip to main content

ravenrs_sheet/engine/cmd/
analysis.rs

1//! Analysis ToolPak (Data › Analysis › Data Analysis): statistical tools that compute a report
2//! from input ranges and write it as a static table (values, not formulas) to an output range or
3//! a new worksheet.
4//!
5//! Every tool is one `tool` value of `data.analysisToolPak`. Distribution values (t, F) come
6//! from the worksheet functions (`T.DIST.2T`, `F.DIST.RT`, `T.INV.2T`…), so the reports agree
7//! with the formulas a user would write. Layouts follow Microsoft's public description of the
8//! tools (row labels and section titles); exact spacing is GridCraft's reading of it.
9
10use std::collections::{BTreeMap, HashMap};
11
12use crate::base::{CellError, CellRef, DateSystem, MAX_COLS, MAX_ROWS, RangeRef, Value};
13use crate::model::{Cell, NumFmt, Sheet};
14use serde_json::{Value as Json, json};
15
16use super::whatif::parse_range;
17use super::{CommandSpec, EngineError, Result, Session, bad, cmd, edit, has_doc, str_param};
18
19const CMD: &str = "data.analysisToolPak";
20/// Most input cells a tool reads.
21const MAX_INPUT_CELLS: u64 = 1_000_000;
22/// Most cells a report may write.
23const MAX_OUTPUT_CELLS: u64 = 4_000_000;
24/// Most variables in a correlation or covariance matrix (work grows with its square).
25const MAX_MATRIX_VARS: usize = 256;
26/// Excel's limit on regression X columns.
27const MAX_REGRESSORS: usize = 16;
28/// Most histogram bins.
29const MAX_BINS: usize = 100_000;
30/// Excel's limit on Fourier Analysis input values.
31const MAX_FOURIER: usize = 4096;
32/// Most numbers Random Number Generation or Sampling writes in one run.
33const MAX_RANDOM: usize = 1_000_000;
34/// Most trials of a binomial draw, and the farthest Poisson outcome considered.
35const MAX_OUTCOMES: f64 = 1_000_000.0;
36/// Largest seed: seeds are whole numbers that a JSON number holds exactly.
37const MAX_SEED: u64 = (1 << 53) - 1;
38
39/// Part of the error a programmatic call gets when the output range already holds data; the
40/// dialog recognises it and asks before running again with `overwrite: true`.
41pub const OVERWRITE_ERROR: &str = "already holds data";
42
43/// (id, label) of every tool, in the order of Excel's Analysis Tools list.
44pub const TOOLS: &[(&str, &str)] = &[
45    ("anovaSingle", "Anova: Single Factor"),
46    ("anovaTwoFactorWith", "Anova: Two-Factor With Replication"),
47    ("anovaTwoFactorWithout", "Anova: Two-Factor Without Replication"),
48    ("correlation", "Correlation"),
49    ("covariance", "Covariance"),
50    ("descriptive", "Descriptive Statistics"),
51    ("exponentialSmoothing", "Exponential Smoothing"),
52    ("fTest", "F-Test Two-Sample for Variances"),
53    ("fourierAnalysis", "Fourier Analysis"),
54    ("histogram", "Histogram"),
55    ("movingAverage", "Moving Average"),
56    ("randomNumberGeneration", "Random Number Generation"),
57    ("rankPercentile", "Rank and Percentile"),
58    ("regression", "Regression"),
59    ("sampling", "Sampling"),
60    ("tTestPaired", "t-Test: Paired Two Sample for Means"),
61    ("tTestEqual", "t-Test: Two-Sample Assuming Equal Variances"),
62    ("tTestUnequal", "t-Test: Two-Sample Assuming Unequal Variances"),
63    ("zTest", "z-Test: Two Sample for Means"),
64];
65
66pub fn specs() -> Vec<CommandSpec> {
67    vec![cmd!(
68        "data.analysisToolPak",
69        "Data Analysis…",
70        ["Data", "Analysis"],
71        None,
72        "{tool: anovaSingle|anovaTwoFactorWith|anovaTwoFactorWithout|correlation|covariance|descriptive|exponentialSmoothing|fTest|fourierAnalysis|histogram|movingAverage|randomNumberGeneration|rankPercentile|regression|sampling|tTestPaired|tTestEqual|tTestUnequal|zTest (or the tool's label), output?: \"E1\" (top-left of the report; default a new sheet), newSheet?: name, outputTo?: range|newSheet (picks between the two), overwrite?: bool (else non-empty output cells are refused), labels?: bool (first row/column holds names), groupedBy?: columns|rows, …tool params} — range tools take `range`; descriptive: summary? (default true), confidence?: 95, kthLargest?, kthSmallest?; histogram: bins?: range (default evenly spaced), pareto?, cumulative?; regression: inputY, inputX, constantZero?, confidence?: 95, residuals?, standardizedResiduals?, normalProbability?; movingAverage: interval? (3), standardErrors?; exponentialSmoothing: damping? (0.3), standardErrors?; t-tests and fTest: range1, range2, alpha? (0.05), hypothesizedDifference? (0); anovaSingle: alpha?; anovaTwoFactorWithout: range, labels? (the first row holds column names and the first column row names), alpha?; anovaTwoFactorWith: range (first row and column hold labels), rowsPerSample, alpha?; zTest: range1, range2, variance1, variance2 (known variances), alpha?, hypothesizedDifference?; fourierAnalysis: range (one row or column of 2^k numbers or complex text, at most 4096), inverse?; randomNumberGeneration: variables? (1), count (per variable; not for patterned), distribution: uniform{from, to} | normal{mean, standardDeviation} | bernoulli{p} | binomial{p, trials} | poisson{lambda} | patterned{from, to, step, repeatEach, repeatSequence} | discrete{valueRange: two columns of value and probability}, seed? (whole number, not for patterned; the same seed repeats the numbers; the result reports the seed used), at most 1,000,000 numbers; sampling: range, method?: periodic|random, period (periodic), samples (random, with replacement), seed?. Writes static values. No `tool` → {tools} (the menu opens the dialog).",
73        has_doc,
74        run
75    )]
76}
77
78// ---------------------------------------------------------------- parameters
79
80fn num(p: &Json, key: &str) -> Option<f64> {
81    match p.get(key)? {
82        Json::Number(n) => n.as_f64(),
83        Json::String(s) => s.trim().trim_end_matches('%').trim().parse::<f64>().ok(),
84        _ => None,
85    }
86    .filter(|v| v.is_finite())
87}
88
89fn flag(p: &Json, key: &str) -> bool {
90    match p.get(key) {
91        Some(Json::Bool(b)) => *b,
92        Some(Json::String(s)) => s.eq_ignore_ascii_case("true") || s == "1",
93        Some(Json::Number(n)) => n.as_f64().is_some_and(|v| v != 0.0),
94        _ => false,
95    }
96}
97
98fn text<'a>(p: &'a Json, keys: &[&str]) -> Option<&'a str> {
99    keys.iter().find_map(|k| str_param(p, k)).map(str::trim).filter(|t| !t.is_empty())
100}
101
102fn norm(s: &str) -> String {
103    s.chars().filter(char::is_ascii_alphanumeric).collect::<String>().to_ascii_lowercase()
104}
105
106/// A tool id from its id or its label (case and punctuation ignored).
107fn tool_id(t: &str) -> Option<&'static str> {
108    let n = norm(t);
109    TOOLS.iter().find(|(id, label)| norm(id) == n || norm(label) == n).map(|(id, _)| *id)
110}
111
112/// Confidence level as a percentage in (0, 100): `95`, `"95%"` or `0.95`.
113fn confidence(p: &Json, key: &str) -> Result<Option<f64>> {
114    match num(p, key) {
115        None => Ok(None),
116        Some(c) => {
117            let c = if c > 0.0 && c < 1.0 { c * 100.0 } else { c };
118            if c > 0.0 && c < 100.0 { Ok(Some(c)) } else { Err(bad(CMD, "the confidence level must be between 0 and 100")) }
119        }
120    }
121}
122
123fn alpha(p: &Json) -> Result<f64> {
124    match opt_num(p, "alpha")? {
125        None => Ok(0.05),
126        Some(a) if a > 0.0 && a < 1.0 => Ok(a),
127        Some(_) => Err(bad(CMD, "alpha must be between 0 and 1")),
128    }
129}
130
131// ---------------------------------------------------------------- input
132
133struct Var {
134    name: String,
135    vals: Vec<Value>,
136}
137
138fn non_numeric() -> EngineError {
139    EngineError::Other("Input range contains non-numeric data.".into())
140}
141
142/// The cells of a range parameter, after checking its size.
143fn input_range(s: &Session, p: &Json, keys: &[&str]) -> Result<(usize, RangeRef)> {
144    let t = text(p, keys).ok_or_else(|| bad(CMD, format!("missing `{}` (the input range)", keys.first().copied().unwrap_or("range"))))?;
145    let (si, r) = parse_range(&s.doc()?.wb, t, CMD)?;
146    if r.count() > MAX_INPUT_CELLS {
147        return Err(bad(CMD, format!("the input range is too large ({MAX_INPUT_CELLS} cells at most)")));
148    }
149    Ok((si, r))
150}
151
152/// Splits a range into variables (columns, or rows when `by_rows`); with `labels` the first
153/// cell of each names it, else `default(i)` does.
154fn variables(s: &Session, si: usize, r: RangeRef, by_rows: bool, labels: bool, default: &dyn Fn(usize) -> String) -> Result<Vec<Var>> {
155    let d = s.doc()?;
156    let sh = d.wb.sheet(si).ok_or(EngineError::NoDocument)?;
157    let (outer, inner) = if by_rows { (r.height(), r.width()) } else { (r.width(), r.height()) };
158    let mut out = Vec::with_capacity(outer as usize);
159    for i in 0..outer {
160        let cell = |j: u32| if by_rows { CellRef::new(r.start.row + i, r.start.col + j) } else { CellRef::new(r.start.row + j, r.start.col + i) };
161        let mut vals: Vec<Value> = (0..inner).map(|j| sh.value(cell(j))).collect();
162        let mut name = default(i as usize);
163        if labels && !vals.is_empty() {
164            let first = vals.remove(0);
165            let t = first.display();
166            if !t.trim().is_empty() {
167                name = t;
168            }
169        }
170        out.push(Var { name, vals });
171    }
172    Ok(out)
173}
174
175/// The numbers of a variable; blanks are skipped when `skip_blank`, anything else that isn't a
176/// number is an error.
177fn numbers(v: &[Value], skip_blank: bool) -> Result<Vec<f64>> {
178    let mut out = Vec::with_capacity(v.len());
179    for x in v {
180        match x {
181            Value::Number(n) => out.push(*n),
182            Value::Empty if skip_blank => {}
183            _ => return Err(non_numeric()),
184        }
185    }
186    Ok(out)
187}
188
189fn by_rows(p: &Json) -> bool {
190    str_param(p, "groupedBy").is_some_and(|g| g.to_ascii_lowercase().starts_with("row"))
191}
192
193/// One variable from a single row or column (`range1`/`range2` of the two-sample tests).
194fn single(s: &Session, p: &Json, key: &str, fallback: &str) -> Result<Var> {
195    let (si, r) = input_range(s, p, &[key])?;
196    if r.width() > 1 && r.height() > 1 {
197        return Err(EngineError::Other("Input range must be a single row or column.".into()));
198    }
199    let rows = r.width() > 1;
200    let mut v = variables(s, si, r, rows, flag(p, "labels"), &|_| fallback.to_string())?;
201    v.pop().ok_or_else(|| bad(CMD, "empty input range"))
202}
203
204// ---------------------------------------------------------------- statistics
205
206fn mean(x: &[f64]) -> f64 {
207    if x.is_empty() { f64::NAN } else { x.iter().sum::<f64>() / x.len() as f64 }
208}
209
210/// Sum of squared deviations from the mean.
211fn ss(x: &[f64]) -> f64 {
212    let m = mean(x);
213    x.iter().map(|v| (v - m) * (v - m)).sum()
214}
215
216/// Sample variance (n − 1).
217fn var_s(x: &[f64]) -> f64 {
218    if x.len() < 2 { f64::NAN } else { ss(x) / (x.len() - 1) as f64 }
219}
220
221fn div0() -> Value {
222    Value::Error(CellError::Div0)
223}
224
225/// `a / b`, `#DIV/0!` when `b` is zero or the inputs aren't finite.
226fn ratio(a: f64, b: f64) -> Value {
227    if b == 0.0 || !a.is_finite() || !b.is_finite() { div0() } else { Value::number(a / b) }
228}
229
230fn n_or_div0(x: f64) -> Value {
231    if x.is_finite() { Value::number(x) } else { div0() }
232}
233
234/// Host services for calling worksheet functions directly.
235struct FnHost;
236
237impl crate::functions::Ctx for FnHost {
238    fn date_system(&self) -> DateSystem {
239        DateSystem::D1900
240    }
241    fn now_serial(&self) -> f64 {
242        0.0
243    }
244    fn random(&mut self) -> f64 {
245        0.5
246    }
247}
248
249/// Calls a worksheet function on numeric arguments (and `cumulative` flags as booleans).
250fn call(name: &str, args: &[Value]) -> Value {
251    let Some(spec) = crate::functions::lookup(name) else { return Value::Error(CellError::Name) };
252    if args.iter().any(|a| a.as_f64().is_some_and(|v| !v.is_finite())) {
253        return Value::Error(CellError::Num);
254    }
255    let a: Vec<crate::functions::Arg> = args.iter().cloned().map(crate::functions::Arg::val).collect();
256    crate::functions::call(spec, &a, &mut FnHost)
257}
258
259fn callf(name: &str, args: &[f64]) -> Value {
260    call(name, &args.iter().map(|v| Value::Number(*v)).collect::<Vec<_>>())
261}
262
263fn f64_of(v: &Value) -> f64 {
264    v.as_f64().unwrap_or(f64::NAN)
265}
266
267// ---------------------------------------------------------------- report grid
268
269/// A report: values by (row, col) relative to its top-left corner.
270#[derive(Default)]
271struct Grid {
272    cells: BTreeMap<(u32, u32), Value>,
273    /// Cells shown as percentages.
274    pct: Vec<(u32, u32)>,
275}
276
277impl Grid {
278    fn put(&mut self, r: u32, c: u32, v: Value) {
279        if !v.is_empty() {
280            self.cells.insert((r, c), v);
281        }
282    }
283    fn txt(&mut self, r: u32, c: u32, t: impl AsRef<str>) {
284        self.put(r, c, Value::text(t.as_ref()));
285    }
286    fn num(&mut self, r: u32, c: u32, x: f64) {
287        self.put(r, c, Value::number(x));
288    }
289    /// Writes a row of labels starting at column `c`.
290    fn labels(&mut self, r: u32, c: u32, items: &[&str]) {
291        for (k, t) in items.iter().enumerate() {
292            if !t.is_empty() {
293                self.txt(r, c + k as u32, t);
294            }
295        }
296    }
297    /// (rows, cols) spanned.
298    fn extent(&self) -> (u32, u32) {
299        let rows = self.cells.keys().map(|(r, _)| r + 1).max().unwrap_or(0);
300        let cols = self.cells.keys().map(|(_, c)| c + 1).max().unwrap_or(0);
301        (rows, cols)
302    }
303}
304
305fn idx(i: usize) -> u32 {
306    u32::try_from(i).unwrap_or(u32::MAX)
307}
308
309// ---------------------------------------------------------------- tools
310
311fn descriptive(s: &Session, p: &Json) -> Result<Grid> {
312    let (si, r) = input_range(s, p, &["range", "input"])?;
313    let vars = variables(s, si, r, by_rows(p), flag(p, "labels"), &|i| format!("Column{}", i + 1))?;
314    let summary = p.get("summary").map(|_| flag(p, "summary")).unwrap_or(true);
315    let conf = confidence(p, "confidence")?;
316    let kl = num(p, "kthLargest").filter(|k| *k >= 1.0).map(|k| k as usize);
317    let ks = num(p, "kthSmallest").filter(|k| *k >= 1.0).map(|k| k as usize);
318    if !summary && conf.is_none() && kl.is_none() && ks.is_none() {
319        return Err(bad(CMD, "choose at least one of summary, confidence, kthLargest or kthSmallest"));
320    }
321    let mut g = Grid::default();
322    for (i, v) in vars.iter().enumerate() {
323        let x = numbers(&v.vals, true)?;
324        let c = idx(i).saturating_mul(2);
325        g.txt(0, c, &v.name);
326        let mut row = 2u32;
327        let mut line = |g: &mut Grid, label: &str, val: Value| {
328            g.txt(row, c, label);
329            g.put(row, c + 1, val);
330            row += 1;
331        };
332        let n = x.len() as f64;
333        let mut sorted = x.clone();
334        sorted.sort_by(f64::total_cmp);
335        if summary {
336            let m = mean(&x);
337            let sd = var_s(&x).sqrt();
338            line(&mut g, "Mean", n_or_div0(m));
339            line(&mut g, "Standard Error", if x.len() < 2 { div0() } else { n_or_div0(sd / n.sqrt()) });
340            line(&mut g, "Median", median(&sorted));
341            line(&mut g, "Mode", mode(&x));
342            line(&mut g, "Standard Deviation", n_or_div0(sd));
343            line(&mut g, "Sample Variance", n_or_div0(var_s(&x)));
344            line(&mut g, "Kurtosis", kurtosis(&x));
345            line(&mut g, "Skewness", skewness(&x));
346            let (lo, hi) = (sorted.first().copied(), sorted.last().copied());
347            line(&mut g, "Range", lo.zip(hi).map(|(a, b)| Value::number(b - a)).unwrap_or(Value::Number(0.0)));
348            line(&mut g, "Minimum", lo.map(Value::number).unwrap_or(Value::Number(0.0)));
349            line(&mut g, "Maximum", hi.map(Value::number).unwrap_or(Value::Number(0.0)));
350            line(&mut g, "Sum", Value::number(x.iter().sum()));
351            line(&mut g, "Count", Value::number(n));
352        }
353        if let Some(k) = kl {
354            let v = if k <= sorted.len() { sorted.get(sorted.len() - k).copied().map(Value::number) } else { None };
355            line(&mut g, &format!("Largest({k})"), v.unwrap_or(Value::Error(CellError::Num)));
356        }
357        if let Some(k) = ks {
358            let v = sorted.get(k - 1).copied().map(Value::number);
359            line(&mut g, &format!("Smallest({k})"), v.unwrap_or(Value::Error(CellError::Num)));
360        }
361        if let Some(cl) = conf {
362            let sd = var_s(&x).sqrt();
363            let val = if x.len() < 2 {
364                div0()
365            } else {
366                let t = f64_of(&callf("T.INV.2T", &[1.0 - cl / 100.0, n - 1.0]));
367                n_or_div0(t * sd / n.sqrt())
368            };
369            line(&mut g, &format!("Confidence Level({cl:.1}%)"), val);
370        }
371    }
372    Ok(g)
373}
374
375fn median(sorted: &[f64]) -> Value {
376    let n = sorted.len();
377    if n == 0 {
378        return Value::Error(CellError::Num);
379    }
380    let v = if n % 2 == 1 { sorted.get(n / 2).copied() } else { sorted.get(n / 2 - 1).zip(sorted.get(n / 2)).map(|(a, b)| (a + b) / 2.0) };
381    v.map(Value::number).unwrap_or(Value::Error(CellError::Num))
382}
383
384/// The most frequent value (the first one to reach the top count), `#N/A` when no value repeats.
385fn mode(x: &[f64]) -> Value {
386    let mut counts: HashMap<u64, usize> = HashMap::new();
387    for v in x {
388        *counts.entry(v.to_bits()).or_default() += 1;
389    }
390    let top = counts.values().copied().max().unwrap_or(0);
391    if top < 2 {
392        return Value::Error(CellError::NA);
393    }
394    x.iter().find(|v| counts.get(&v.to_bits()).copied() == Some(top)).map(|v| Value::number(*v)).unwrap_or(Value::Error(CellError::NA))
395}
396
397/// Sample excess kurtosis (Excel's KURT).
398fn kurtosis(x: &[f64]) -> Value {
399    let n = x.len() as f64;
400    let sd = var_s(x).sqrt();
401    if x.len() < 4 || sd.is_nan() || sd <= 0.0 {
402        return div0();
403    }
404    let m = mean(x);
405    let s4: f64 = x.iter().map(|v| ((v - m) / sd).powi(4)).sum();
406    Value::number(n * (n + 1.0) / ((n - 1.0) * (n - 2.0) * (n - 3.0)) * s4 - 3.0 * (n - 1.0) * (n - 1.0) / ((n - 2.0) * (n - 3.0)))
407}
408
409/// Sample skewness (Excel's SKEW).
410fn skewness(x: &[f64]) -> Value {
411    let n = x.len() as f64;
412    let sd = var_s(x).sqrt();
413    if x.len() < 3 || sd.is_nan() || sd <= 0.0 {
414        return div0();
415    }
416    let m = mean(x);
417    let s3: f64 = x.iter().map(|v| ((v - m) / sd).powi(3)).sum();
418    Value::number(n / ((n - 1.0) * (n - 2.0)) * s3)
419}
420
421fn histogram(s: &Session, p: &Json) -> Result<Grid> {
422    let (si, r) = input_range(s, p, &["range", "input"])?;
423    let labels = flag(p, "labels");
424    let d = s.doc()?;
425    let sh = d.wb.sheet(si).ok_or(EngineError::NoDocument)?;
426    let mut cells: Vec<Value> = r.iter().map(|c| sh.value(c)).collect();
427    if labels && !cells.is_empty() {
428        cells.remove(0);
429    }
430    let data = numbers(&cells, true)?;
431    let mut bins: Vec<f64> = match text(p, &["bins", "binRange"]) {
432        Some(_) => {
433            let (bsi, br) = input_range(s, p, &["bins", "binRange"])?;
434            let bsh = d.wb.sheet(bsi).ok_or(EngineError::NoDocument)?;
435            let mut b: Vec<Value> = br.iter().map(|c| bsh.value(c)).collect();
436            if labels && !b.is_empty() {
437                b.remove(0);
438            }
439            numbers(&b, true)?
440        }
441        None => {
442            // Evenly spaced bins from the minimum to the maximum, √n intervals.
443            let lo = data.iter().copied().fold(f64::INFINITY, f64::min);
444            let hi = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
445            if !lo.is_finite() || !hi.is_finite() {
446                return Err(EngineError::Other("The input range holds no numbers.".into()));
447            }
448            let k = ((data.len() as f64).sqrt().floor() as usize).clamp(1, 1000);
449            if hi == lo { vec![lo] } else { (0..=k).map(|i| lo + (hi - lo) * i as f64 / k as f64).collect() }
450        }
451    };
452    if bins.is_empty() {
453        return Err(EngineError::Other("The bin range holds no numbers.".into()));
454    }
455    if bins.len() > MAX_BINS {
456        return Err(bad(CMD, format!("too many bins ({MAX_BINS} at most)")));
457    }
458    bins.sort_by(f64::total_cmp);
459    let mut freq = vec![0u64; bins.len() + 1];
460    for x in &data {
461        let i = bins.partition_point(|b| b < x);
462        if let Some(f) = freq.get_mut(i) {
463            *f += 1;
464        }
465    }
466    let rows: Vec<(Value, u64)> = bins.iter().map(|b| Value::number(*b)).chain(std::iter::once(Value::text("More"))).zip(freq).collect();
467    let total = data.len() as f64;
468    let cumulative = flag(p, "cumulative");
469    let mut g = Grid::default();
470    let write = |g: &mut Grid, c0: u32, rows: &[(Value, u64)]| {
471        g.labels(0, c0, &["Bin", "Frequency"]);
472        if cumulative {
473            g.txt(0, c0 + 2, "Cumulative %");
474        }
475        let mut acc = 0u64;
476        for (i, (b, f)) in rows.iter().enumerate() {
477            let r = idx(i) + 1;
478            g.put(r, c0, b.clone());
479            g.num(r, c0 + 1, *f as f64);
480            if cumulative {
481                acc += f;
482                g.put(r, c0 + 2, ratio(acc as f64, total));
483                g.pct.push((r, c0 + 2));
484            }
485        }
486    };
487    write(&mut g, 0, &rows);
488    if flag(p, "pareto") {
489        let mut sorted = rows.clone();
490        sorted.sort_by_key(|r| std::cmp::Reverse(r.1));
491        write(&mut g, if cumulative { 3 } else { 2 }, &sorted);
492    }
493    Ok(g)
494}
495
496/// Variables of a range whose columns (rows) must all be numbers, for the matrix tools.
497fn matrix_vars(s: &Session, p: &Json) -> Result<Vec<(String, Vec<f64>)>> {
498    let (si, r) = input_range(s, p, &["range", "input"])?;
499    let vars = variables(s, si, r, by_rows(p), flag(p, "labels"), &|i| format!("Column {}", i + 1))?;
500    if vars.len() > MAX_MATRIX_VARS {
501        return Err(bad(CMD, format!("too many variables ({MAX_MATRIX_VARS} at most)")));
502    }
503    vars.into_iter().map(|v| Ok((v.name, numbers(&v.vals, false)?))).collect()
504}
505
506fn matrix(s: &Session, p: &Json, f: impl Fn(&[f64], &[f64]) -> Value, diag: impl Fn(&[f64]) -> Value) -> Result<Grid> {
507    let vars = matrix_vars(s, p)?;
508    let mut g = Grid::default();
509    for (i, (name, x)) in vars.iter().enumerate() {
510        let k = idx(i) + 1;
511        g.txt(0, k, name);
512        g.txt(k, 0, name);
513        for (j, (_, y)) in vars.iter().enumerate().take(i + 1) {
514            g.put(k, idx(j) + 1, if i == j { diag(x) } else { f(x, y) });
515        }
516    }
517    Ok(g)
518}
519
520/// Sum of cross products of deviations.
521fn sxy(x: &[f64], y: &[f64]) -> f64 {
522    let (mx, my) = (mean(x), mean(y));
523    x.iter().zip(y).map(|(a, b)| (a - mx) * (b - my)).sum()
524}
525
526fn correlation(x: &[f64], y: &[f64]) -> Value {
527    let d = (ss(x) * ss(y)).sqrt();
528    if x.is_empty() || d.is_nan() || d <= 0.0 { div0() } else { Value::number(sxy(x, y) / d) }
529}
530
531fn covariance_p(x: &[f64], y: &[f64]) -> Value {
532    if x.is_empty() { div0() } else { Value::number(sxy(x, y) / x.len() as f64) }
533}
534
535// ---------------------------------------------------------------- regression
536
537/// Inverts a small symmetric matrix by Gauss–Jordan elimination with partial pivoting; `None`
538/// when it is singular (collinear inputs).
539fn invert(mut a: Vec<Vec<f64>>) -> Option<Vec<Vec<f64>>> {
540    let n = a.len();
541    let scale = (0..n).filter_map(|i| a.get(i).and_then(|r| r.get(i))).fold(0.0f64, |m, v| m.max(v.abs()));
542    let mut inv: Vec<Vec<f64>> = (0..n).map(|i| (0..n).map(|j| if i == j { 1.0 } else { 0.0 }).collect()).collect();
543    for col in 0..n {
544        let piv = (col..n).max_by(|&x, &y| {
545            let ax = a.get(x).and_then(|r| r.get(col)).map_or(0.0, |v| v.abs());
546            let ay = a.get(y).and_then(|r| r.get(col)).map_or(0.0, |v| v.abs());
547            ax.total_cmp(&ay)
548        })?;
549        let pv = *a.get(piv)?.get(col)?;
550        if pv.is_nan() || pv.abs() <= 1e-12 * scale.max(f64::MIN_POSITIVE) {
551            return None;
552        }
553        a.swap(col, piv);
554        inv.swap(col, piv);
555        for j in 0..n {
556            *a.get_mut(col)?.get_mut(j)? /= pv;
557            *inv.get_mut(col)?.get_mut(j)? /= pv;
558        }
559        let prow = a.get(col)?.clone();
560        let pinv = inv.get(col)?.clone();
561        for i in 0..n {
562            if i == col {
563                continue;
564            }
565            let f = *a.get(i)?.get(col)?;
566            if f == 0.0 {
567                continue;
568            }
569            for j in 0..n {
570                *a.get_mut(i)?.get_mut(j)? -= f * prow.get(j)?;
571                *inv.get_mut(i)?.get_mut(j)? -= f * pinv.get(j)?;
572            }
573        }
574    }
575    Some(inv)
576}
577
578struct Fit {
579    /// Intercept first, then one coefficient per X column.
580    coef: Vec<f64>,
581    /// Standard errors in the same order (NaN for a forced-zero intercept).
582    se: Vec<f64>,
583    predicted: Vec<f64>,
584    sse: f64,
585    ssr: f64,
586    dfr: f64,
587    dfe: f64,
588    /// Total degrees of freedom (n − 1, or n through the origin).
589    dft: f64,
590}
591
592/// Ordinary least squares of `y` on the columns `xs`, with an intercept unless `zero`.
593fn ols(y: &[f64], xs: &[Vec<f64>], zero: bool) -> Option<Fit> {
594    let n = y.len();
595    let k = xs.len();
596    let nf = n as f64;
597    let (ym, xm): (f64, Vec<f64>) = if zero { (0.0, vec![0.0; k]) } else { (mean(y), xs.iter().map(|x| mean(x)).collect()) };
598    let dev = |j: usize, i: usize| xs.get(j).and_then(|x| x.get(i)).copied().unwrap_or(0.0) - xm.get(j).copied().unwrap_or(0.0);
599    let mut sxx = vec![vec![0.0; k]; k];
600    let mut sxy = vec![0.0; k];
601    for i in 0..n {
602        let yi = y.get(i).copied().unwrap_or(0.0) - ym;
603        for a in 0..k {
604            let da = dev(a, i);
605            if let Some(v) = sxy.get_mut(a) {
606                *v += da * yi;
607            }
608            for b in 0..k {
609                if let Some(v) = sxx.get_mut(a).and_then(|r| r.get_mut(b)) {
610                    *v += da * dev(b, i);
611                }
612            }
613        }
614    }
615    let inv = invert(sxx)?;
616    let b: Vec<f64> = (0..k)
617        .map(|a| (0..k).map(|c| inv.get(a).and_then(|r| r.get(c)).copied().unwrap_or(0.0) * sxy.get(c).copied().unwrap_or(0.0)).sum())
618        .collect();
619    let b0 = if zero { 0.0 } else { ym - b.iter().zip(&xm).map(|(bj, mj)| bj * mj).sum::<f64>() };
620    let predicted: Vec<f64> = (0..n)
621        .map(|i| b0 + (0..k).map(|j| b.get(j).copied().unwrap_or(0.0) * xs.get(j).and_then(|x| x.get(i)).copied().unwrap_or(0.0)).sum::<f64>())
622        .collect();
623    let sse: f64 = y.iter().zip(&predicted).map(|(a, p)| (a - p) * (a - p)).sum();
624    let ssr: f64 = predicted.iter().map(|p| (p - ym) * (p - ym)).sum();
625    let dfr = k as f64;
626    let dfe = if zero { nf - dfr } else { nf - dfr - 1.0 };
627    let dft = if zero { nf } else { nf - 1.0 };
628    let s2 = sse / dfe;
629    let mut se = Vec::with_capacity(k + 1);
630    if zero {
631        se.push(f64::NAN);
632    } else {
633        let q: f64 = (0..k)
634            .map(|a| {
635                (0..k)
636                    .map(|c| {
637                        xm.get(a).copied().unwrap_or(0.0)
638                            * inv.get(a).and_then(|r| r.get(c)).copied().unwrap_or(0.0)
639                            * xm.get(c).copied().unwrap_or(0.0)
640                    })
641                    .sum::<f64>()
642            })
643            .sum();
644        se.push((s2 * (1.0 / nf + q)).sqrt());
645    }
646    for a in 0..k {
647        se.push((s2 * inv.get(a).and_then(|r| r.get(a)).copied().unwrap_or(f64::NAN)).sqrt());
648    }
649    let mut coef = vec![b0];
650    coef.extend(b);
651    Some(Fit { coef, se, predicted, sse, ssr, dfr, dfe, dft })
652}
653
654fn regression(s: &Session, p: &Json) -> Result<Grid> {
655    let labels = flag(p, "labels");
656    let (ysi, yr) = input_range(s, p, &["inputY", "y"])?;
657    let (xsi, xr) = input_range(s, p, &["inputX", "x"])?;
658    if yr.width() != 1 {
659        return Err(EngineError::Other("Input Y range must be a single column.".into()));
660    }
661    if xr.height() != yr.height() {
662        return Err(EngineError::Other("Input Y range and input X range must have the same number of rows.".into()));
663    }
664    if xr.width() as usize > MAX_REGRESSORS {
665        return Err(EngineError::Other(format!("Input X range must have {MAX_REGRESSORS} columns or fewer.")));
666    }
667    let yv = variables(s, ysi, yr, false, labels, &|_| "Y".into())?;
668    let xv = variables(s, xsi, xr, false, labels, &|i| format!("X Variable {}", i + 1))?;
669    let yvar = yv.first().ok_or_else(|| bad(CMD, "empty input Y range"))?;
670    let y = numbers(&yvar.vals, false)?;
671    let xs: Vec<Vec<f64>> = xv.iter().map(|v| numbers(&v.vals, false)).collect::<Result<_>>()?;
672    let zero = flag(p, "constantZero");
673    let k = xs.len();
674    let n = y.len();
675    let params = if zero { k } else { k + 1 };
676    if n <= params {
677        return Err(EngineError::Other("Too few observations for this many X variables.".into()));
678    }
679    let fit = ols(&y, &xs, zero).ok_or_else(|| EngineError::Other("The X variables are collinear; the regression can't be computed.".into()))?;
680    let conf = confidence(p, "confidence")?.unwrap_or(95.0);
681    let sst = fit.ssr + fit.sse;
682    let r2 = if sst > 0.0 { fit.ssr / sst } else { f64::NAN };
683    let mut g = Grid::default();
684    g.txt(0, 0, "SUMMARY OUTPUT");
685    g.txt(2, 0, "Regression Statistics");
686    let stats: [(&str, Value); 5] = [
687        ("Multiple R", n_or_div0(r2.sqrt())),
688        ("R Square", n_or_div0(r2)),
689        ("Adjusted R Square", n_or_div0(1.0 - (fit.sse / fit.dfe) / (sst / fit.dft))),
690        ("Standard Error", n_or_div0((fit.sse / fit.dfe).sqrt())),
691        ("Observations", Value::number(n as f64)),
692    ];
693    for (i, (label, v)) in stats.into_iter().enumerate() {
694        g.txt(3 + idx(i), 0, label);
695        g.put(3 + idx(i), 1, v);
696    }
697    g.txt(9, 0, "ANOVA");
698    g.labels(10, 1, &["df", "SS", "MS", "F", "Significance F"]);
699    let (msr, mse) = (fit.ssr / fit.dfr, fit.sse / fit.dfe);
700    let f = msr / mse;
701    g.txt(11, 0, "Regression");
702    g.num(11, 1, fit.dfr);
703    g.num(11, 2, fit.ssr);
704    g.num(11, 3, msr);
705    g.put(11, 4, n_or_div0(f));
706    g.put(11, 5, if f.is_finite() { callf("F.DIST.RT", &[f, fit.dfr, fit.dfe]) } else { div0() });
707    g.txt(12, 0, "Residual");
708    g.num(12, 1, fit.dfe);
709    g.num(12, 2, fit.sse);
710    g.num(12, 3, mse);
711    g.txt(13, 0, "Total");
712    g.num(13, 1, fit.dft);
713    g.num(13, 2, sst);
714    let lo2 = format!("Lower {conf:.1}%");
715    let hi2 = format!("Upper {conf:.1}%");
716    g.labels(15, 1, &["Coefficients", "Standard Error", "t Stat", "P-value", "Lower 95%", "Upper 95%", &lo2, &hi2]);
717    let t95 = f64_of(&callf("T.INV.2T", &[0.05, fit.dfe]));
718    let tc = f64_of(&callf("T.INV.2T", &[1.0 - conf / 100.0, fit.dfe]));
719    for (j, (b, se)) in fit.coef.iter().zip(&fit.se).enumerate() {
720        let row = 16 + idx(j);
721        g.txt(row, 0, if j == 0 { "Intercept" } else { xv.get(j - 1).map_or("", |v| v.name.as_str()) });
722        g.num(row, 1, *b);
723        if j == 0 && zero {
724            for c in 2..=8 {
725                g.put(row, c, Value::Error(CellError::NA));
726            }
727            continue;
728        }
729        let t = b / se;
730        g.put(row, 2, n_or_div0(*se));
731        g.put(row, 3, n_or_div0(t));
732        g.put(row, 4, if t.is_finite() { callf("T.DIST.2T", &[t.abs(), fit.dfe]) } else { div0() });
733        g.num(row, 5, b - t95 * se);
734        g.num(row, 6, b + t95 * se);
735        g.num(row, 7, b - tc * se);
736        g.num(row, 8, b + tc * se);
737    }
738    let mut row = 16 + idx(fit.coef.len()) + 3;
739    let residuals = flag(p, "residuals");
740    let standardized = flag(p, "standardizedResiduals");
741    if residuals || standardized {
742        g.txt(row, 0, "RESIDUAL OUTPUT");
743        let pred = format!("Predicted {}", yvar.name);
744        let mut head = vec!["Observation", pred.as_str(), "Residuals"];
745        if standardized {
746            head.push("Standard Residuals");
747        }
748        g.labels(row + 2, 0, &head);
749        // Residuals scaled by their standard deviation (n − 1).
750        let sd = (fit.sse / (n as f64 - 1.0)).sqrt();
751        for (i, (yi, pi)) in y.iter().zip(&fit.predicted).enumerate() {
752            let r = row + 3 + idx(i);
753            g.num(r, 0, (i + 1) as f64);
754            g.num(r, 1, *pi);
755            g.num(r, 2, yi - pi);
756            if standardized {
757                g.put(r, 3, ratio(yi - pi, sd));
758            }
759        }
760    }
761    if flag(p, "normalProbability") {
762        let c = if residuals || standardized { if standardized { 5 } else { 4 } } else { 0 };
763        if !(residuals || standardized) {
764            row = 16 + idx(fit.coef.len()) + 3;
765        }
766        g.txt(row, c, "PROBABILITY OUTPUT");
767        g.labels(row + 2, c, &["Percentile", &yvar.name]);
768        let mut sorted = y.clone();
769        sorted.sort_by(f64::total_cmp);
770        for (i, v) in sorted.iter().enumerate() {
771            let r = row + 3 + idx(i);
772            g.num(r, c, 100.0 * (i as f64 + 0.5) / n as f64);
773            g.num(r, c + 1, *v);
774        }
775    }
776    Ok(g)
777}
778
779// ---------------------------------------------------------------- smoothing
780
781fn series(s: &Session, p: &Json) -> Result<Vec<f64>> {
782    let (si, r) = input_range(s, p, &["range", "input"])?;
783    if r.width() > 1 && r.height() > 1 {
784        return Err(EngineError::Other("Input range must be a single row or column.".into()));
785    }
786    let v = variables(s, si, r, r.width() > 1, flag(p, "labels"), &|_| String::new())?;
787    numbers(&v.first().map(|v| v.vals.clone()).unwrap_or_default(), false)
788}
789
790/// Writes the forecasts (and with `standardErrors` the standard errors over `se_window`
791/// points) down two columns.
792fn forecast_grid(x: &[f64], f: &[Option<f64>], se_window: usize, p: &Json) -> Grid {
793    let mut g = Grid::default();
794    let na = || Value::Error(CellError::NA);
795    for (i, fi) in f.iter().enumerate() {
796        g.put(idx(i), 0, fi.map(Value::number).unwrap_or_else(na));
797    }
798    if flag(p, "standardErrors") {
799        // A running sum of squared residuals over the last `se_window` points: O(n), not
800        // O(n·window). A point without a forecast restarts it, so every window touching it is #N/A.
801        let sq = |j: usize| Some((x.get(j)? - (*f.get(j)?)?).powi(2));
802        let (mut acc, mut run) = (0.0, 0usize);
803        for i in 0..f.len() {
804            match sq(i) {
805                Some(t) => {
806                    acc += t;
807                    run += 1;
808                }
809                None => (acc, run) = (0.0, 0),
810            }
811            if run > se_window {
812                acc -= i.checked_sub(se_window).and_then(sq).unwrap_or(0.0);
813                run = se_window;
814            }
815            // Rounding in the subtraction can leave a tiny negative sum.
816            let v = (run == se_window).then(|| (acc.max(0.0) / se_window as f64).sqrt());
817            g.put(idx(i), 1, v.map(Value::number).unwrap_or_else(na));
818        }
819    }
820    g
821}
822
823fn moving_average(s: &Session, p: &Json) -> Result<Grid> {
824    let x = series(s, p)?;
825    let interval = num(p, "interval").unwrap_or(3.0);
826    if interval < 1.0 || interval.fract() != 0.0 || interval > x.len() as f64 {
827        return Err(EngineError::Other("The interval must be a whole number from 1 to the number of data points.".into()));
828    }
829    let k = interval as usize;
830    let mut f = Vec::with_capacity(x.len());
831    let mut acc = 0.0;
832    for (i, v) in x.iter().enumerate() {
833        acc += v;
834        if let Some(old) = i.checked_sub(k).and_then(|j| x.get(j)) {
835            acc -= old;
836        }
837        f.push(if i + 1 >= k { Some(acc / k as f64) } else { None });
838    }
839    Ok(forecast_grid(&x, &f, k, p))
840}
841
842fn exponential_smoothing(s: &Session, p: &Json) -> Result<Grid> {
843    let x = series(s, p)?;
844    let damp = num(p, "damping").unwrap_or(0.3);
845    if !(0.0..=1.0).contains(&damp) {
846        return Err(EngineError::Other("The damping factor must be between 0 and 1.".into()));
847    }
848    // F(1) = A(0); F(t) = (1 − damping)·A(t−1) + damping·F(t−1).
849    let mut f: Vec<Option<f64>> = Vec::with_capacity(x.len());
850    for i in 0..x.len() {
851        let v = match i {
852            0 => None,
853            1 => x.first().copied(),
854            _ => x.get(i - 1).zip(f.get(i - 1).copied().flatten()).map(|(a, prev)| (1.0 - damp) * a + damp * prev),
855        };
856        f.push(v);
857    }
858    Ok(forecast_grid(&x, &f, 3, p))
859}
860
861// ---------------------------------------------------------------- rank and percentile
862
863fn rank_percentile(s: &Session, p: &Json) -> Result<Grid> {
864    let (si, r) = input_range(s, p, &["range", "input"])?;
865    let vars = variables(s, si, r, by_rows(p), flag(p, "labels"), &|i| format!("Column{}", i + 1))?;
866    let mut g = Grid::default();
867    for (vi, v) in vars.iter().enumerate() {
868        let c = idx(vi).saturating_mul(4);
869        g.labels(0, c, &["Point", &v.name, "Rank", "Percent"]);
870        let mut pts: Vec<(usize, f64)> = Vec::new();
871        for (i, val) in v.vals.iter().enumerate() {
872            match val {
873                Value::Number(n) => pts.push((i + 1, *n)),
874                Value::Empty => {}
875                _ => return Err(non_numeric()),
876            }
877        }
878        let mut asc: Vec<f64> = pts.iter().map(|p| p.1).collect();
879        asc.sort_by(f64::total_cmp);
880        let n = asc.len();
881        pts.sort_by(|a, b| b.1.total_cmp(&a.1).then(a.0.cmp(&b.0)));
882        for (k, (point, x)) in pts.iter().enumerate() {
883            let row = idx(k) + 1;
884            let below = asc.partition_point(|v| v < x);
885            let above = n - asc.partition_point(|v| v <= x);
886            g.num(row, c, *point as f64);
887            g.num(row, c + 1, *x);
888            g.num(row, c + 2, (above + 1) as f64);
889            // PERCENTRANK.INC: share of values below, truncated to three digits.
890            let pr = if n <= 1 { 1.0 } else { (below as f64 / (n - 1) as f64 * 1000.0 + 1e-9).floor() / 1000.0 };
891            g.num(row, c + 3, pr);
892            g.pct.push((row, c + 3));
893        }
894    }
895    Ok(g)
896}
897
898// ---------------------------------------------------------------- tests of means and variances
899
900fn two_samples(s: &Session, p: &Json, skip_blank: bool) -> Result<(Var, Var, Vec<f64>, Vec<f64>)> {
901    let a = single(s, p, "range1", "Variable 1")?;
902    let b = single(s, p, "range2", "Variable 2")?;
903    let x = numbers(&a.vals, skip_blank)?;
904    let y = numbers(&b.vals, skip_blank)?;
905    if x.len() < 2 || y.len() < 2 {
906        return Err(EngineError::Other("Each input range needs at least two numbers.".into()));
907    }
908    Ok((a, b, x, y))
909}
910
911/// Title, blank row and the "Variable 1 / Variable 2" header; returns the first data row.
912fn test_head(g: &mut Grid, title: &str, a: &str, b: &str) -> u32 {
913    g.txt(0, 0, title);
914    g.txt(2, 1, a);
915    g.txt(2, 2, b);
916    3
917}
918
919/// The t statistic rows shared by the three t-tests.
920fn t_rows(g: &mut Grid, mut row: u32, t: f64, df: f64, alpha: f64) {
921    let ta = t.abs();
922    let lines: [(&str, Value); 5] = [
923        ("t Stat", n_or_div0(t)),
924        ("P(T<=t) one-tail", if t.is_finite() { callf("T.DIST.RT", &[ta, df]) } else { div0() }),
925        ("t Critical one-tail", callf("T.INV", &[1.0 - alpha, df])),
926        ("P(T<=t) two-tail", if t.is_finite() { callf("T.DIST.2T", &[ta, df]) } else { div0() }),
927        ("t Critical two-tail", callf("T.INV.2T", &[alpha, df])),
928    ];
929    for (label, v) in lines {
930        g.txt(row, 0, label);
931        g.put(row, 1, v);
932        row += 1;
933    }
934}
935
936fn pair_row(g: &mut Grid, row: u32, label: &str, a: Value, b: Value) {
937    g.txt(row, 0, label);
938    g.put(row, 1, a);
939    g.put(row, 2, b);
940}
941
942fn t_test(s: &Session, p: &Json, kind: &str) -> Result<Grid> {
943    let alpha = alpha(p)?;
944    let hmd = num(p, "hypothesizedDifference").or_else(|| num(p, "difference")).unwrap_or(0.0);
945    let paired = kind == "tTestPaired";
946    let (a, b, x, y) = two_samples(s, p, !paired)?;
947    let (n1, n2) = (x.len() as f64, y.len() as f64);
948    let (m1, m2, v1, v2) = (mean(&x), mean(&y), var_s(&x), var_s(&y));
949    let mut g = Grid::default();
950    let title = match kind {
951        "tTestPaired" => "t-Test: Paired Two Sample for Means",
952        "tTestEqual" => "t-Test: Two-Sample Assuming Equal Variances",
953        _ => "t-Test: Two-Sample Assuming Unequal Variances",
954    };
955    let mut row = test_head(&mut g, title, &a.name, &b.name);
956    pair_row(&mut g, row, "Mean", Value::number(m1), Value::number(m2));
957    pair_row(&mut g, row + 1, "Variance", Value::number(v1), Value::number(v2));
958    pair_row(&mut g, row + 2, "Observations", Value::number(n1), Value::number(n2));
959    row += 3;
960    let (t, df) = match kind {
961        "tTestPaired" => {
962            if x.len() != y.len() {
963                return Err(EngineError::Other("Input ranges must have the same number of values.".into()));
964            }
965            pair_row(&mut g, row, "Pearson Correlation", correlation(&x, &y), Value::Empty);
966            row += 1;
967            let dif: Vec<f64> = x.iter().zip(&y).map(|(a, b)| a - b).collect();
968            let sd = var_s(&dif).sqrt();
969            ((mean(&dif) - hmd) / (sd / n1.sqrt()), n1 - 1.0)
970        }
971        "tTestEqual" => {
972            let pooled = ((n1 - 1.0) * v1 + (n2 - 1.0) * v2) / (n1 + n2 - 2.0);
973            pair_row(&mut g, row, "Pooled Variance", Value::number(pooled), Value::Empty);
974            row += 1;
975            ((m1 - m2 - hmd) / (pooled * (1.0 / n1 + 1.0 / n2)).sqrt(), n1 + n2 - 2.0)
976        }
977        _ => {
978            let (q1, q2) = (v1 / n1, v2 / n2);
979            // Welch–Satterthwaite degrees of freedom, rounded to a whole number.
980            let df = ((q1 + q2).powi(2) / (q1 * q1 / (n1 - 1.0) + q2 * q2 / (n2 - 1.0))).round();
981            ((m1 - m2 - hmd) / (q1 + q2).sqrt(), df)
982        }
983    };
984    pair_row(&mut g, row, "Hypothesized Mean Difference", Value::number(hmd), Value::Empty);
985    pair_row(&mut g, row + 1, "df", n_or_div0(df), Value::Empty);
986    t_rows(&mut g, row + 2, t, df, alpha);
987    Ok(g)
988}
989
990fn f_test(s: &Session, p: &Json) -> Result<Grid> {
991    let alpha = alpha(p)?;
992    let (a, b, x, y) = two_samples(s, p, true)?;
993    let (v1, v2) = (var_s(&x), var_s(&y));
994    let (d1, d2) = (x.len() as f64 - 1.0, y.len() as f64 - 1.0);
995    let mut g = Grid::default();
996    let row = test_head(&mut g, "F-Test Two-Sample for Variances", &a.name, &b.name);
997    pair_row(&mut g, row, "Mean", Value::number(mean(&x)), Value::number(mean(&y)));
998    pair_row(&mut g, row + 1, "Variance", Value::number(v1), Value::number(v2));
999    pair_row(&mut g, row + 2, "Observations", Value::number(d1 + 1.0), Value::number(d2 + 1.0));
1000    pair_row(&mut g, row + 3, "df", Value::number(d1), Value::number(d2));
1001    let f = v1 / v2;
1002    pair_row(&mut g, row + 4, "F", n_or_div0(f), Value::Empty);
1003    // The tail on the side of F: left when F < 1, right otherwise.
1004    let (pv, crit) = if !f.is_finite() {
1005        (div0(), div0())
1006    } else if f < 1.0 {
1007        (call("F.DIST", &[Value::Number(f), Value::Number(d1), Value::Number(d2), Value::Bool(true)]), callf("F.INV", &[alpha, d1, d2]))
1008    } else {
1009        (callf("F.DIST.RT", &[f, d1, d2]), callf("F.INV.RT", &[alpha, d1, d2]))
1010    };
1011    pair_row(&mut g, row + 5, "P(F<=f) one-tail", pv, Value::Empty);
1012    pair_row(&mut g, row + 6, "F Critical one-tail", crit, Value::Empty);
1013    Ok(g)
1014}
1015
1016fn anova_single(s: &Session, p: &Json) -> Result<Grid> {
1017    let alpha = alpha(p)?;
1018    let (si, r) = input_range(s, p, &["range", "input"])?;
1019    let vars = variables(s, si, r, by_rows(p), flag(p, "labels"), &|i| format!("Column {}", i + 1))?;
1020    let groups: Vec<(String, Vec<f64>)> = vars.into_iter().map(|v| Ok((v.name, numbers(&v.vals, true)?))).collect::<Result<_>>()?;
1021    if groups.len() < 2 || groups.iter().any(|(_, x)| x.is_empty()) {
1022        return Err(EngineError::Other("Anova needs at least two groups, each with at least one number.".into()));
1023    }
1024    let mut g = Grid::default();
1025    g.txt(0, 0, "Anova: Single Factor");
1026    g.txt(2, 0, "SUMMARY");
1027    g.labels(3, 0, &["Groups", "Count", "Sum", "Average", "Variance"]);
1028    let all: Vec<f64> = groups.iter().flat_map(|(_, x)| x.iter().copied()).collect();
1029    let grand = mean(&all);
1030    let (mut ssb, mut ssw) = (0.0, 0.0);
1031    for (i, (name, x)) in groups.iter().enumerate() {
1032        let row = 4 + idx(i);
1033        let m = mean(x);
1034        g.txt(row, 0, name);
1035        g.num(row, 1, x.len() as f64);
1036        g.num(row, 2, x.iter().sum());
1037        g.num(row, 3, m);
1038        g.put(row, 4, n_or_div0(var_s(x)));
1039        ssb += x.len() as f64 * (m - grand) * (m - grand);
1040        ssw += ss(x);
1041    }
1042    let k = groups.len() as f64;
1043    let n = all.len() as f64;
1044    let (dfb, dfw) = (k - 1.0, n - k);
1045    let row = 4 + idx(groups.len()) + 2;
1046    g.txt(row, 0, "ANOVA");
1047    g.labels(row + 1, 0, &["Source of Variation", "SS", "df", "MS", "F", "P-value", "F crit"]);
1048    let (msb, msw) = (ssb / dfb, ratio(ssw, dfw));
1049    let f = msw.as_f64().map_or(f64::NAN, |w| msb / w);
1050    g.txt(row + 2, 0, "Between Groups");
1051    g.num(row + 2, 1, ssb);
1052    g.num(row + 2, 2, dfb);
1053    g.num(row + 2, 3, msb);
1054    g.put(row + 2, 4, n_or_div0(f));
1055    g.put(row + 2, 5, if f.is_finite() && dfw > 0.0 { callf("F.DIST.RT", &[f, dfb, dfw]) } else { div0() });
1056    g.put(row + 2, 6, if dfw > 0.0 { callf("F.INV.RT", &[alpha, dfb, dfw]) } else { div0() });
1057    g.txt(row + 3, 0, "Within Groups");
1058    g.num(row + 3, 1, ssw);
1059    g.num(row + 3, 2, dfw);
1060    g.put(row + 3, 3, msw);
1061    g.txt(row + 5, 0, "Total");
1062    g.num(row + 5, 1, ssb + ssw);
1063    g.num(row + 5, 2, n - 1.0);
1064    Ok(g)
1065}
1066
1067// ---------------------------------------------------------------- two-factor anova and z-test
1068
1069/// Count, Sum, Average and Variance of a group, as the Anova summaries list them.
1070fn group_summary(x: &[f64]) -> [Value; 4] {
1071    [Value::number(x.len() as f64), Value::number(x.iter().sum()), n_or_div0(mean(x)), n_or_div0(var_s(x))]
1072}
1073
1074const ANOVA_HEAD: [&str; 7] = ["Source of Variation", "SS", "df", "MS", "F", "P-value", "F crit"];
1075
1076/// One line of an Anova table. A factor line, tested against the error term `err` (its MS and
1077/// df), adds F, the P-value and the critical F.
1078fn anova_line(g: &mut Grid, row: u32, label: &str, ss: f64, df: f64, err: Option<(f64, f64)>, alpha: f64) {
1079    g.txt(row, 0, label);
1080    g.num(row, 1, ss);
1081    g.num(row, 2, df);
1082    g.put(row, 3, ratio(ss, df));
1083    if let Some((mse, dfe)) = err {
1084        let f = ss / df / mse;
1085        g.put(row, 4, n_or_div0(f));
1086        g.put(row, 5, if f.is_finite() && dfe > 0.0 { callf("F.DIST.RT", &[f, df, dfe]) } else { div0() });
1087        g.put(row, 6, if df > 0.0 && dfe > 0.0 { callf("F.INV.RT", &[alpha, df, dfe]) } else { div0() });
1088    }
1089}
1090
1091/// The cells of a two-factor input range, row by row.
1092fn input_rows(s: &Session, p: &Json) -> Result<Vec<Vec<Value>>> {
1093    let (si, r) = input_range(s, p, &["range", "input"])?;
1094    Ok(variables(s, si, r, true, false, &|_| String::new())?.into_iter().map(|v| v.vals).collect())
1095}
1096
1097/// The values of column `j` of a table of numbers.
1098fn column(data: &[Vec<f64>], j: usize) -> Vec<f64> {
1099    data.iter().filter_map(|r| r.get(j).copied()).collect()
1100}
1101
1102/// Anova: Two-Factor Without Replication: one observation for each pair of levels (rows ×
1103/// columns), the error term being the residual after both main effects.
1104fn anova_two_factor_without(s: &Session, p: &Json) -> Result<Grid> {
1105    let alpha = alpha(p)?;
1106    let labels = flag(p, "labels");
1107    let mut rows = input_rows(s, p)?;
1108    let head = if labels && !rows.is_empty() { Some(rows.remove(0)) } else { None };
1109    let mut row_names = Vec::with_capacity(rows.len());
1110    let mut data: Vec<Vec<f64>> = Vec::with_capacity(rows.len());
1111    for (i, mut row) in rows.into_iter().enumerate() {
1112        let name = if labels && !row.is_empty() { row.remove(0).display() } else { format!("Row {}", i + 1) };
1113        row_names.push(name);
1114        data.push(numbers(&row, false)?);
1115    }
1116    let (a, b) = (data.len(), data.first().map_or(0, Vec::len));
1117    if a < 2 || b < 2 {
1118        return Err(EngineError::Other("Anova needs at least two rows and two columns of numbers.".into()));
1119    }
1120    let col_names: Vec<String> = match head {
1121        Some(h) => h.iter().skip(1).map(Value::display).collect(),
1122        None => (1..=b).map(|j| format!("Column {j}")).collect(),
1123    };
1124    let cols: Vec<Vec<f64>> = (0..b).map(|j| column(&data, j)).collect();
1125    let all: Vec<f64> = data.iter().flatten().copied().collect();
1126    let grand = mean(&all);
1127    let row_means: Vec<f64> = data.iter().map(|r| mean(r)).collect();
1128    let col_means: Vec<f64> = cols.iter().map(|c| mean(c)).collect();
1129    let ss_rows: f64 = row_means.iter().map(|m| b as f64 * (m - grand).powi(2)).sum();
1130    let ss_cols: f64 = col_means.iter().map(|m| a as f64 * (m - grand).powi(2)).sum();
1131    // The residual of each observation after removing both main effects.
1132    let ss_err: f64 =
1133        data.iter().zip(&row_means).flat_map(|(r, rm)| r.iter().zip(&col_means).map(move |(x, cm)| (x - rm - cm + grand).powi(2))).sum();
1134    let (df_rows, df_cols) = ((a - 1) as f64, (b - 1) as f64);
1135    let df_err = df_rows * df_cols;
1136    let mut g = Grid::default();
1137    g.txt(0, 0, "Anova: Two-Factor Without Replication");
1138    g.labels(2, 0, &["SUMMARY", "Count", "Sum", "Average", "Variance"]);
1139    let summary = |g: &mut Grid, row: u32, name: &str, x: &[f64]| {
1140        g.txt(row, 0, name);
1141        for (k, v) in group_summary(x).into_iter().enumerate() {
1142            g.put(row, 1 + idx(k), v);
1143        }
1144    };
1145    for (i, (name, x)) in row_names.iter().zip(&data).enumerate() {
1146        summary(&mut g, 3 + idx(i), name, x);
1147    }
1148    let first_col = 3 + idx(a) + 1;
1149    for (j, (name, x)) in col_names.iter().zip(&cols).enumerate() {
1150        summary(&mut g, first_col + idx(j), name, x);
1151    }
1152    let row = first_col + idx(b) + 2;
1153    g.txt(row, 0, "ANOVA");
1154    g.labels(row + 1, 0, &ANOVA_HEAD);
1155    let err = Some((ss_err / df_err, df_err));
1156    anova_line(&mut g, row + 2, "Rows", ss_rows, df_rows, err, alpha);
1157    anova_line(&mut g, row + 3, "Columns", ss_cols, df_cols, err, alpha);
1158    anova_line(&mut g, row + 4, "Error", ss_err, df_err, None, alpha);
1159    g.txt(row + 6, 0, "Total");
1160    g.num(row + 6, 1, ss(&all));
1161    g.num(row + 6, 2, (all.len() - 1) as f64);
1162    Ok(g)
1163}
1164
1165/// The number of rows in each sample of a two-factor Anova with replication.
1166fn rows_per_sample(p: &Json, data_rows: usize) -> Result<usize> {
1167    let per = num(p, "rowsPerSample").ok_or_else(|| bad(CMD, "missing `rowsPerSample` (the number of rows in each sample)"))?;
1168    if per < 2.0 || per.fract() != 0.0 || per > data_rows as f64 || !data_rows.is_multiple_of(per as usize) {
1169        return Err(EngineError::Other("Rows per sample must be a whole number of at least 2 that divides the number of data rows.".into()));
1170    }
1171    Ok(per as usize)
1172}
1173
1174/// Anova: Two-Factor With Replication. The input range holds column labels in its first row and
1175/// sample labels in its first column; each sample (a level of the row factor) spans
1176/// `rowsPerSample` consecutive rows, so every cell of the design holds that many observations.
1177fn anova_two_factor_with(s: &Session, p: &Json) -> Result<Grid> {
1178    let alpha = alpha(p)?;
1179    let mut rows = input_rows(s, p)?;
1180    if rows.len() < 3 {
1181        return Err(EngineError::Other("The input range must hold a row of labels and at least two samples.".into()));
1182    }
1183    let head = rows.remove(0);
1184    let col_names: Vec<String> = head.iter().skip(1).map(Value::display).collect();
1185    let per = rows_per_sample(p, rows.len())?;
1186    let mut sample_names = Vec::new();
1187    let mut data: Vec<Vec<f64>> = Vec::with_capacity(rows.len());
1188    for (i, mut row) in rows.into_iter().enumerate() {
1189        let name = if row.is_empty() { Value::Empty } else { row.remove(0) };
1190        if i.is_multiple_of(per) {
1191            sample_names.push(name.display());
1192        }
1193        data.push(numbers(&row, false)?);
1194    }
1195    let (a, b) = (sample_names.len(), col_names.len());
1196    if a < 2 || b < 2 {
1197        return Err(EngineError::Other("Anova needs at least two samples and two columns of numbers.".into()));
1198    }
1199    // cells[i][j]: the observations of sample i in column j.
1200    let cells: Vec<Vec<Vec<f64>>> = data.chunks(per).map(|block| (0..b).map(|j| column(block, j)).collect()).collect();
1201    let all: Vec<f64> = data.iter().flatten().copied().collect();
1202    let grand = mean(&all);
1203    let sample_all: Vec<Vec<f64>> = data.chunks(per).map(|block| block.iter().flatten().copied().collect()).collect();
1204    let cols: Vec<Vec<f64>> = (0..b).map(|j| column(&data, j)).collect();
1205    let (sm, cm): (Vec<f64>, Vec<f64>) = (sample_all.iter().map(|x| mean(x)).collect(), cols.iter().map(|x| mean(x)).collect());
1206    let n = per as f64;
1207    let ss_sample: f64 = sm.iter().map(|m| n * b as f64 * (m - grand).powi(2)).sum();
1208    let ss_cols: f64 = cm.iter().map(|m| n * a as f64 * (m - grand).powi(2)).sum();
1209    let mut ss_inter = 0.0;
1210    let mut ss_within = 0.0;
1211    for (i, row) in cells.iter().enumerate() {
1212        for (j, cell) in row.iter().enumerate() {
1213            let (si, cj) = (sm.get(i).copied().unwrap_or(f64::NAN), cm.get(j).copied().unwrap_or(f64::NAN));
1214            ss_inter += n * (mean(cell) - si - cj + grand).powi(2);
1215            ss_within += ss(cell);
1216        }
1217    }
1218    let (df_sample, df_cols) = ((a - 1) as f64, (b - 1) as f64);
1219    let df_within = (a * b * (per - 1)) as f64;
1220    let mut g = Grid::default();
1221    g.txt(0, 0, "Anova: Two-Factor With Replication");
1222    g.txt(2, 0, "SUMMARY");
1223    for (j, name) in col_names.iter().enumerate() {
1224        g.txt(2, 1 + idx(j), name);
1225    }
1226    g.txt(2, 1 + idx(b), "Total");
1227    let stats = ["Count", "Sum", "Average", "Variance"];
1228    let mut row = 3u32;
1229    let block = |g: &mut Grid, row: &mut u32, name: &str, groups: Vec<&[f64]>| {
1230        g.txt(*row, 0, name);
1231        let sums: Vec<[Value; 4]> = groups.iter().map(|x| group_summary(x)).collect();
1232        for (k, label) in stats.iter().enumerate() {
1233            let r = *row + 1 + idx(k);
1234            g.txt(r, 0, label);
1235            for (j, sm) in sums.iter().enumerate() {
1236                g.put(r, 1 + idx(j), sm.get(k).cloned().unwrap_or(Value::Empty));
1237            }
1238        }
1239        *row += 6;
1240    };
1241    for ((name, row_cells), whole) in sample_names.iter().zip(&cells).zip(&sample_all) {
1242        let mut groups: Vec<&[f64]> = row_cells.iter().map(Vec::as_slice).collect();
1243        groups.push(whole);
1244        block(&mut g, &mut row, name, groups);
1245    }
1246    block(&mut g, &mut row, "Total", cols.iter().map(Vec::as_slice).collect());
1247    row += 1;
1248    g.txt(row, 0, "ANOVA");
1249    g.labels(row + 1, 0, &ANOVA_HEAD);
1250    let err = Some((ss_within / df_within, df_within));
1251    anova_line(&mut g, row + 2, "Sample", ss_sample, df_sample, err, alpha);
1252    anova_line(&mut g, row + 3, "Columns", ss_cols, df_cols, err, alpha);
1253    anova_line(&mut g, row + 4, "Interaction", ss_inter, df_sample * df_cols, err, alpha);
1254    anova_line(&mut g, row + 5, "Within", ss_within, df_within, None, alpha);
1255    g.txt(row + 7, 0, "Total");
1256    g.num(row + 7, 1, ss(&all));
1257    g.num(row + 7, 2, (all.len() - 1) as f64);
1258    Ok(g)
1259}
1260
1261/// A known variance parameter: a positive number.
1262fn known_variance(p: &Json, key: &str) -> Result<f64> {
1263    match num(p, key) {
1264        Some(v) if v > 0.0 => Ok(v),
1265        _ => Err(EngineError::Other(format!("Enter the known variance `{key}` (a positive number)."))),
1266    }
1267}
1268
1269/// z-Test: Two Sample for Means, for two samples whose population variances are known.
1270fn z_test(s: &Session, p: &Json) -> Result<Grid> {
1271    let alpha = alpha(p)?;
1272    let hmd = match opt_num(p, "hypothesizedDifference")? {
1273        Some(d) => d,
1274        None => num_or(p, "difference", 0.0)?,
1275    };
1276    let (v1, v2) = (known_variance(p, "variance1")?, known_variance(p, "variance2")?);
1277    let a = single(s, p, "range1", "Variable 1")?;
1278    let b = single(s, p, "range2", "Variable 2")?;
1279    let (x, y) = (numbers(&a.vals, true)?, numbers(&b.vals, true)?);
1280    if x.is_empty() || y.is_empty() {
1281        return Err(EngineError::Other("Each input range needs at least one number.".into()));
1282    }
1283    let (n1, n2) = (x.len() as f64, y.len() as f64);
1284    let (m1, m2) = (mean(&x), mean(&y));
1285    let z = (m1 - m2 - hmd) / (v1 / n1 + v2 / n2).sqrt();
1286    let mut g = Grid::default();
1287    let row = test_head(&mut g, "z-Test: Two Sample for Means", &a.name, &b.name);
1288    pair_row(&mut g, row, "Mean", Value::number(m1), Value::number(m2));
1289    pair_row(&mut g, row + 1, "Known Variance", Value::number(v1), Value::number(v2));
1290    pair_row(&mut g, row + 2, "Observations", Value::number(n1), Value::number(n2));
1291    pair_row(&mut g, row + 3, "Hypothesized Mean Difference", Value::number(hmd), Value::Empty);
1292    pair_row(&mut g, row + 4, "z", n_or_div0(z), Value::Empty);
1293    // The tail beyond |z|: 1 − Φ(|z|).
1294    let tail = call("NORM.S.DIST", &[Value::Number(z.abs()), Value::Bool(true)]).as_f64().map(|c| 1.0 - c);
1295    let lines: [(&str, Value); 4] = [
1296        ("P(Z<=z) one-tail", tail.map_or_else(div0, Value::number)),
1297        ("z Critical one-tail", callf("NORM.S.INV", &[1.0 - alpha])),
1298        ("P(Z<=z) two-tail", tail.map_or_else(div0, |t| Value::number(2.0 * t))),
1299        ("z Critical two-tail", callf("NORM.S.INV", &[1.0 - alpha / 2.0])),
1300    ];
1301    for (k, (label, v)) in lines.into_iter().enumerate() {
1302        pair_row(&mut g, row + 5 + idx(k), label, v, Value::Empty);
1303    }
1304    Ok(g)
1305}
1306
1307// ---------------------------------------------------------------- fourier analysis
1308
1309/// Radix-2 fast Fourier transform (iterative Cooley–Tukey) in place, for a power-of-two number of
1310/// complex values (re, im). The forward transform is unscaled, X[k] = Σ x[n]·e^(−2πikn/N); the
1311/// inverse uses e^(+2πikn/N) and divides by N, so it returns the values the forward one took.
1312fn fft(a: &mut [(f64, f64)], inverse: bool) {
1313    let n = a.len();
1314    if n < 2 || !n.is_power_of_two() {
1315        return;
1316    }
1317    let bits = n.trailing_zeros();
1318    for i in 0..n {
1319        let j = i.reverse_bits() >> (usize::BITS - bits);
1320        if i < j {
1321            a.swap(i, j);
1322        }
1323    }
1324    let sign = if inverse { 1.0 } else { -1.0 };
1325    let mut len = 2;
1326    while len <= n {
1327        let ang = sign * 2.0 * std::f64::consts::PI / len as f64;
1328        for start in (0..n).step_by(len) {
1329            for k in 0..len / 2 {
1330                // Each twiddle factor is computed directly, not by repeated multiplication.
1331                let (wr, wi) = ((ang * k as f64).cos(), (ang * k as f64).sin());
1332                let (i, j) = (start + k, start + k + len / 2);
1333                let (Some(&(ur, ui)), Some(&(xr, xi))) = (a.get(i), a.get(j)) else { continue };
1334                let (vr, vi) = (xr * wr - xi * wi, xr * wi + xi * wr);
1335                if let Some(u) = a.get_mut(i) {
1336                    *u = (ur + vr, ui + vi);
1337                }
1338                if let Some(x) = a.get_mut(j) {
1339                    *x = (ur - vr, ui - vi);
1340                }
1341            }
1342        }
1343        len <<= 1;
1344    }
1345    if inverse {
1346        let nf = n as f64;
1347        a.iter_mut().for_each(|v| *v = (v.0 / nf, v.1 / nf));
1348    }
1349}
1350
1351/// Fourier Analysis: the discrete Fourier transform (or with `inverse` its inverse) of one row or
1352/// column of 2^k values, read and written as complex numbers the way IMREAL, IMAGINARY and
1353/// COMPLEX read and write them.
1354fn fourier_analysis(s: &Session, p: &Json) -> Result<Grid> {
1355    let v = single(s, p, "range", "")?;
1356    let n = v.vals.len();
1357    if n == 0 || n > MAX_FOURIER || !n.is_power_of_two() {
1358        return Err(EngineError::Other(format!("Fourier Analysis needs 1, 2, 4, 8 … {MAX_FOURIER} input values (a power of two).")));
1359    }
1360    let mut a = Vec::with_capacity(n);
1361    for x in &v.vals {
1362        let part = |f: &str| call(f, std::slice::from_ref(x)).as_f64();
1363        match (x, part("IMREAL"), part("IMAGINARY")) {
1364            (Value::Empty, ..) => return Err(non_numeric()),
1365            (_, Some(re), Some(im)) => a.push((re, im)),
1366            _ => return Err(EngineError::Other("Input range must hold numbers or complex numbers such as 3+4i.".into())),
1367        }
1368    }
1369    let total: f64 = a.iter().map(|(re, im)| re.abs() + im.abs()).sum();
1370    let inverse = flag(p, "inverse");
1371    fft(&mut a, inverse);
1372    // Parts that can only be rounding noise of the transform are written as 0. Each of the
1373    // log2(N) butterfly stages adds at most a few units of f64::EPSILON times the magnitude of
1374    // the values it combines, so the computed transform differs from the exact one by at most
1375    // about log2(N) · ε · Σ|x_n| times a small constant (Higham, Accuracy and Stability of
1376    // Numerical Algorithms, 2nd ed., chapter 24, The Fast Fourier Transform). The bound comes
1377    // from the input and N, never from the largest output, so a small part beside a large one
1378    // (an imaginary part of 1 beside a real part of 1e14, a harmonic of 4e-7 beside a DC term of
1379    // 8e6) is kept. With N = 1 the transform is the identity and log2(N) = 0: nothing is zeroed.
1380    // The inverse divides its outputs by N, so its noise is divided by N as well. An input sum
1381    // that overflows gives no finite bound; then nothing is cleaned up.
1382    let mut noise = 8.0 * f64::EPSILON * f64::from(n.trailing_zeros()) * total;
1383    if inverse {
1384        noise /= n as f64;
1385    }
1386    let noise = if noise.is_finite() { noise } else { 0.0 };
1387    let clean = |x: f64| if x.abs() <= noise { 0.0 } else { x };
1388    let mut g = Grid::default();
1389    let first = u32::from(flag(p, "labels"));
1390    if first == 1 {
1391        g.txt(0, 0, &v.name);
1392    }
1393    for (i, (re, im)) in a.into_iter().enumerate() {
1394        g.put(first + idx(i), 0, callf("COMPLEX", &[clean(re), clean(im)]));
1395    }
1396    Ok(g)
1397}
1398
1399// ---------------------------------------------------------------- random numbers and sampling
1400
1401/// A seeded generator: xoshiro256** (Blackman and Vigna, a public-domain algorithm) with its
1402/// state filled by SplitMix64. The same seed gives the same numbers in GridCraft; they are not
1403/// Excel's numbers, whose generator is not documented.
1404struct Rng([u64; 4]);
1405
1406impl Rng {
1407    fn new(seed: u64) -> Rng {
1408        let mut x = seed;
1409        let mut next = || {
1410            x = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
1411            let mut z = x;
1412            z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1413            z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1414            z ^ (z >> 31)
1415        };
1416        Rng([next(), next(), next(), next()])
1417    }
1418
1419    fn next_u64(&mut self) -> u64 {
1420        let [s0, s1, s2, s3] = self.0;
1421        let out = s1.wrapping_mul(5).rotate_left(7).wrapping_mul(9);
1422        let (s2, s3) = (s2 ^ s0, s3 ^ s1);
1423        self.0 = [s0 ^ s3, s1 ^ s2, s2 ^ (s1 << 17), s3.rotate_left(45)];
1424        out
1425    }
1426
1427    /// Uniform in [0, 1), with 53 random bits.
1428    fn uniform(&mut self) -> f64 {
1429        (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
1430    }
1431
1432    /// Uniform whole number in [0, n), without modulo bias.
1433    fn below(&mut self, n: u64) -> u64 {
1434        if n <= 1 {
1435            return 0;
1436        }
1437        let zone = u64::MAX - u64::MAX % n;
1438        loop {
1439            let v = self.next_u64();
1440            if v < zone {
1441                return v % n;
1442            }
1443        }
1444    }
1445
1446    /// Standard normal by the Box–Muller transform.
1447    fn normal(&mut self) -> f64 {
1448        let (u1, u2) = (1.0 - self.uniform(), self.uniform());
1449        (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
1450    }
1451}
1452
1453/// The `seed` parameter, or a seed taken from the clock when there is none.
1454fn seed_of(p: &Json) -> Result<u64> {
1455    let given = p.get("seed").is_some_and(|v| match v {
1456        Json::Null => false,
1457        Json::String(t) => !t.trim().is_empty(),
1458        _ => true,
1459    });
1460    if !given {
1461        let ms = crate::base::date::unix_now_ms().unwrap_or(0.0);
1462        return Ok((ms * 1000.0) as u64 & MAX_SEED);
1463    }
1464    match num(p, "seed") {
1465        Some(x) if x >= 0.0 && x.fract() == 0.0 && x <= MAX_SEED as f64 => Ok(x as u64),
1466        _ => Err(EngineError::Other("The random seed must be a whole number from 0.".into())),
1467    }
1468}
1469
1470/// A number parameter: `None` when absent, null or blank; an error when present but not a
1471/// finite number (a typed "0,3" or "abc" must not quietly become the default).
1472fn opt_num(p: &Json, key: &str) -> Result<Option<f64>> {
1473    match p.get(key) {
1474        None | Some(Json::Null) => Ok(None),
1475        Some(Json::String(t)) if t.trim().is_empty() => Ok(None),
1476        Some(_) => num(p, key).map(Some).ok_or_else(|| EngineError::Other(format!("`{key}` must be a number."))),
1477    }
1478}
1479
1480/// A number parameter, `default` when absent or blank.
1481fn num_or(p: &Json, key: &str, default: f64) -> Result<f64> {
1482    Ok(opt_num(p, key)?.unwrap_or(default))
1483}
1484
1485/// A whole-number parameter of at least `min`, `default` when absent.
1486fn whole(p: &Json, key: &str, default: Option<f64>, min: f64) -> Result<f64> {
1487    match opt_num(p, key)?.or(default) {
1488        Some(x) if x >= min && x.fract() == 0.0 => Ok(x),
1489        Some(_) => Err(EngineError::Other(format!("`{key}` must be a whole number of at least {min}."))),
1490        None => Err(bad(CMD, format!("missing `{key}`"))),
1491    }
1492}
1493
1494/// A probability parameter in [0, 1].
1495fn probability(p: &Json) -> Result<f64> {
1496    match num_or(p, "p", 0.5)? {
1497        x if (0.0..=1.0).contains(&x) => Ok(x),
1498        _ => Err(EngineError::Other("The p value must be between 0 and 1.".into())),
1499    }
1500}
1501
1502/// The probabilities of a distribution on 0, 1, 2 … `last`, as (first outcome, probabilities of
1503/// first, first + 1, …), from its probability `at_mode` at `mode`, its most likely outcome, and
1504/// `ratio`(k) = pmf(k + 1) / pmf(k). Each term is one multiplication of its neighbour towards the
1505/// mode, so no worksheet function runs per outcome, and the terms only shrink away from the mode,
1506/// so nothing underflows before the terms that matter. The left tail ends where its terms vanish,
1507/// the right tail once the cumulative probability reaches 1 within f64 resolution (every later
1508/// draw would map to the last outcome). The table is as wide as the distribution's effective
1509/// support, not as `last`: Binomial(1,000,000, 0.5) takes about 23,000 entries, not 1,000,001.
1510fn pmf_table(last: f64, mode: f64, at_mode: f64, ratio: impl Fn(f64) -> f64) -> Result<(f64, Vec<f64>)> {
1511    if !at_mode.is_finite() || at_mode <= 0.0 || !(0.0..=last).contains(&mode) || mode.fract() != 0.0 {
1512        return Err(EngineError::Other("The distribution could not be tabulated.".into()));
1513    }
1514    let mut left = Vec::new();
1515    let (mut k, mut v) = (mode, at_mode);
1516    while k > 0.0 {
1517        v /= ratio(k - 1.0);
1518        if v.is_nan() || v < f64::MIN_POSITIVE {
1519            break;
1520        }
1521        left.push(v);
1522        k -= 1.0;
1523    }
1524    let first = k;
1525    let mut pmf: Vec<f64> = left.into_iter().rev().collect();
1526    let mut acc = pmf.iter().sum::<f64>() + at_mode;
1527    pmf.push(at_mode);
1528    let (mut k, mut v) = (mode, at_mode);
1529    while k < last && acc < 1.0 - 1e-15 {
1530        v *= ratio(k);
1531        if v.is_nan() || v < f64::MIN_POSITIVE {
1532            break;
1533        }
1534        pmf.push(v);
1535        acc += v;
1536        k += 1.0;
1537    }
1538    Ok((first, pmf))
1539}
1540
1541/// A distribution on whole numbers tabulated for inversion sampling: the cumulative
1542/// probabilities of `first`, `first + 1`, … up to the last outcome that matters.
1543struct Table {
1544    first: f64,
1545    cdf: Vec<f64>,
1546}
1547
1548impl Table {
1549    /// The distribution on 0, 1, 2 … `last` that `pmf_table` describes.
1550    fn from_mode(last: f64, mode: f64, at_mode: f64, ratio: impl Fn(f64) -> f64) -> Result<Table> {
1551        let (first, pmf) = pmf_table(last, mode, at_mode, ratio)?;
1552        let mut acc = 0.0;
1553        let cdf = pmf
1554            .into_iter()
1555            .map(|p| {
1556                acc += p;
1557                acc
1558            })
1559            .collect();
1560        Ok(Table { first, cdf })
1561    }
1562
1563    /// All the probability on one outcome.
1564    fn point(value: f64) -> Table {
1565        Table { first: value, cdf: vec![1.0] }
1566    }
1567
1568    /// A draw: the first outcome whose cumulative probability exceeds a uniform number (inversion).
1569    fn draw(&self, r: &mut Rng) -> f64 {
1570        let u = r.uniform() * self.cdf.last().copied().unwrap_or(1.0);
1571        self.first + self.cdf.partition_point(|c| *c <= u).min(self.cdf.len().saturating_sub(1)) as f64
1572    }
1573}
1574
1575/// The numbers of a patterned sequence: `from` to `to` in steps, each number repeated, the whole
1576/// sequence repeated.
1577fn pattern(p: &Json) -> Result<Vec<f64>> {
1578    let (from, to, step) = (num_or(p, "from", 1.0)?, num_or(p, "to", 1.0)?, num_or(p, "step", 1.0)?);
1579    let (each, times) = (whole(p, "repeatEach", Some(1.0), 1.0)?, whole(p, "repeatSequence", Some(1.0), 1.0)?);
1580    if step <= 0.0 || to < from {
1581        return Err(EngineError::Other("The pattern must run from a smaller number to a larger one in positive steps.".into()));
1582    }
1583    let steps = ((to - from) / step + 1e-9).floor() + 1.0;
1584    if steps * each * times > MAX_RANDOM as f64 {
1585        return Err(EngineError::Other(format!("Random Number Generation writes at most {MAX_RANDOM} numbers.")));
1586    }
1587    let base: Vec<f64> = (0..steps as usize).map(|k| from + step * k as f64).collect();
1588    let one: Vec<f64> = base.iter().flat_map(|v| std::iter::repeat_n(*v, each as usize)).collect();
1589    Ok(one.repeat(times as usize))
1590}
1591
1592/// Random Number Generation: `count` numbers from a distribution in each of `variables`
1593/// columns, drawn row by row.
1594fn random_numbers(s: &Session, p: &Json, seed: u64) -> Result<Grid> {
1595    let vars = whole(p, "variables", Some(1.0), 1.0)?;
1596    let dist = text(p, &["distribution"]).map(norm).unwrap_or_else(|| "uniform".into());
1597    let cells = |rows: f64| {
1598        if rows * vars > MAX_RANDOM as f64 {
1599            Err(EngineError::Other(format!("Random Number Generation writes at most {MAX_RANDOM} numbers.")))
1600        } else {
1601            Ok(rows as usize)
1602        }
1603    };
1604    let mut g = Grid::default();
1605    let cols = vars as usize;
1606    if dist == "patterned" {
1607        let seq = pattern(p)?;
1608        cells(seq.len() as f64)?;
1609        for (i, x) in seq.iter().enumerate() {
1610            for c in 0..cols {
1611                g.num(idx(i), idx(c), *x);
1612            }
1613        }
1614        return Ok(g);
1615    }
1616    let count = cells(whole(p, "count", None, 1.0)?)?;
1617    let mut draw: Box<dyn FnMut(&mut Rng) -> f64> = match dist.as_str() {
1618        "uniform" => {
1619            let (a, b) = (num_or(p, "from", 0.0)?, num_or(p, "to", 1.0)?);
1620            Box::new(move |r| a + (b - a) * r.uniform())
1621        }
1622        "normal" => {
1623            let (m, sd) = (num_or(p, "mean", 0.0)?, num_or(p, "standardDeviation", 1.0)?);
1624            if sd < 0.0 {
1625                return Err(EngineError::Other("The standard deviation must not be negative.".into()));
1626            }
1627            Box::new(move |r| m + sd * r.normal())
1628        }
1629        "bernoulli" => {
1630            let pr = probability(p)?;
1631            Box::new(move |r| if r.uniform() < pr { 1.0 } else { 0.0 })
1632        }
1633        "binomial" => {
1634            let (pr, n) = (probability(p)?, whole(p, "trials", Some(1.0), 1.0)?);
1635            if n > MAX_OUTCOMES {
1636                return Err(EngineError::Other(format!("The number of trials must be at most {MAX_OUTCOMES}.")));
1637            }
1638            // p = 0 and p = 1 are exact: every draw is 0 or n. Otherwise one BINOM.DIST value at
1639            // the mode ⌊(n + 1)p⌋ anchors pmf(k + 1) = pmf(k) · (n − k)/(k + 1) · p/(1 − p).
1640            let table = if pr == 0.0 {
1641                Table::point(0.0)
1642            } else if pr == 1.0 {
1643                Table::point(n)
1644            } else {
1645                let mode = ((n + 1.0) * pr).floor().min(n);
1646                let at_mode = f64_of(&call("BINOM.DIST", &[Value::Number(mode), Value::Number(n), Value::Number(pr), Value::Bool(false)]));
1647                let odds = pr / (1.0 - pr);
1648                Table::from_mode(n, mode, at_mode, |k| (n - k) / (k + 1.0) * odds)?
1649            };
1650            Box::new(move |r| table.draw(r))
1651        }
1652        "poisson" => {
1653            let lambda = num_or(p, "lambda", 1.0)?;
1654            if lambda <= 0.0 || lambda > 100_000.0 {
1655                return Err(EngineError::Other("Lambda must be greater than 0 and at most 100000.".into()));
1656            }
1657            // One POISSON.DIST value at the mode ⌊λ⌋ anchors pmf(k + 1) = pmf(k) · λ/(k + 1).
1658            let last = (lambda + 40.0 * lambda.sqrt() + 100.0).min(MAX_OUTCOMES);
1659            let mode = lambda.floor();
1660            let at_mode = f64_of(&call("POISSON.DIST", &[Value::Number(mode), Value::Number(lambda), Value::Bool(false)]));
1661            let table = Table::from_mode(last, mode, at_mode, |k| lambda / (k + 1.0))?;
1662            Box::new(move |r| table.draw(r))
1663        }
1664        "discrete" => {
1665            let (si, r) = input_range(s, p, &["valueRange", "values"])?;
1666            if r.width() != 2 {
1667                return Err(EngineError::Other("The value and probability input range must have two columns.".into()));
1668            }
1669            let v = variables(s, si, r, false, false, &|_| String::new())?;
1670            let (values, probs) = match v.as_slice() {
1671                [a, b] => (numbers(&a.vals, false)?, numbers(&b.vals, false)?),
1672                _ => return Err(non_numeric()),
1673            };
1674            if probs.iter().any(|x| !(0.0..=1.0).contains(x)) || (probs.iter().sum::<f64>() - 1.0).abs() > 1e-9 {
1675                return Err(EngineError::Other("The probabilities must be between 0 and 1 and add up to 1.".into()));
1676            }
1677            let cdf: Vec<f64> = probs
1678                .iter()
1679                .scan(0.0, |acc, x| {
1680                    *acc += x;
1681                    Some(*acc)
1682                })
1683                .collect();
1684            let table = Table { first: 0.0, cdf };
1685            Box::new(move |r| values.get(table.draw(r) as usize).copied().unwrap_or(f64::NAN))
1686        }
1687        other => return Err(bad(CMD, format!("unknown distribution `{other}`"))),
1688    };
1689    let mut rng = Rng::new(seed);
1690    for i in 0..count {
1691        for c in 0..cols {
1692            g.num(idx(i), idx(c), draw(&mut rng));
1693        }
1694    }
1695    Ok(g)
1696}
1697
1698/// Whether Random Number Generation writes a patterned sequence (which uses no seed).
1699fn patterned(p: &Json) -> bool {
1700    text(p, &["distribution"]).map(norm).is_some_and(|d| d == "patterned")
1701}
1702
1703fn random_sampling(p: &Json) -> bool {
1704    text(p, &["method"]).is_some_and(|m| m.eq_ignore_ascii_case("random"))
1705}
1706
1707/// Sampling: every `period`-th value of the input, or `samples` values drawn at random with
1708/// replacement, down one column. The input is read column by column (row by row for a single
1709/// row); blanks are skipped.
1710fn sampling(s: &Session, p: &Json, seed: Option<u64>) -> Result<Grid> {
1711    let (si, r) = input_range(s, p, &["range", "input"])?;
1712    let by_rows = r.height() == 1 && r.width() > 1;
1713    let vars = variables(s, si, r, by_rows, flag(p, "labels"), &|_| String::new())?;
1714    let mut x = Vec::new();
1715    for v in &vars {
1716        x.extend(numbers(&v.vals, true)?);
1717    }
1718    if x.is_empty() {
1719        return Err(EngineError::Other("The input range holds no numbers.".into()));
1720    }
1721    let picked: Vec<f64> = match seed {
1722        Some(seed) => {
1723            let k = whole(p, "samples", None, 1.0)?;
1724            if k > MAX_RANDOM as f64 {
1725                return Err(EngineError::Other(format!("Sampling takes at most {MAX_RANDOM} samples.")));
1726            }
1727            let mut rng = Rng::new(seed);
1728            (0..k as usize).filter_map(|_| x.get(rng.below(x.len() as u64) as usize).copied()).collect()
1729        }
1730        None => {
1731            let period = whole(p, "period", None, 1.0)?;
1732            if period > x.len() as f64 {
1733                return Err(EngineError::Other("The period must be at most the number of input values.".into()));
1734            }
1735            x.iter().skip(period as usize - 1).step_by(period as usize).copied().collect()
1736        }
1737    };
1738    let mut g = Grid::default();
1739    for (i, v) in picked.iter().enumerate() {
1740        g.num(idx(i), 0, *v);
1741    }
1742    Ok(g)
1743}
1744
1745// ---------------------------------------------------------------- output
1746
1747/// Writes the report at `output` (refusing to overwrite data unless `overwrite`) or on a new
1748/// sheet.
1749fn write_report(s: &mut Session, p: &Json, tool: &str, g: Grid) -> Result<Json> {
1750    let (rows, cols) = g.extent();
1751    let (rows, cols) = (rows.max(1), cols.max(1));
1752    if u64::from(rows) * u64::from(cols) > MAX_OUTPUT_CELLS {
1753        return Err(bad(CMD, "the report is too large"));
1754    }
1755    let d = s.doc()?;
1756    let target = match str_param(p, "outputTo").map(str::to_ascii_lowercase).as_deref() {
1757        Some("newsheet") => None,
1758        Some("range") => Some(text(p, &["output", "outputRange"]).ok_or_else(|| bad(CMD, "missing `output` (the output range)"))?),
1759        _ => text(p, &["output", "outputRange"]),
1760    };
1761    let (si, at, new_sheet) = match target {
1762        Some(t) => {
1763            let (si, r) = parse_range(&d.wb, t, CMD)?;
1764            (si, r.start, None)
1765        }
1766        None => {
1767            if d.wb.protected_structure {
1768                return Err(EngineError::Other("The workbook is protected and cannot be changed.".into()));
1769            }
1770            let name = match p.get("newSheet") {
1771                Some(Json::String(n)) if !n.trim().is_empty() => n.trim().to_string(),
1772                _ => d.wb.next_sheet_name(),
1773            };
1774            d.wb.check_sheet_name(&name, None).map_err(EngineError::Other)?;
1775            (d.wb.sheets.len(), CellRef::new(0, 0), Some(name))
1776        }
1777    };
1778    let end_row = at.row.checked_add(rows - 1).filter(|r| *r < MAX_ROWS);
1779    let end_col = at.col.checked_add(cols - 1).filter(|c| *c < MAX_COLS);
1780    let (Some(end_row), Some(end_col)) = (end_row, end_col) else {
1781        return Err(EngineError::Other("The report doesn't fit on the sheet below and right of the output cell.".into()));
1782    };
1783    let rect = RangeRef::new(at, CellRef::new(end_row, end_col));
1784    // Existing content inside the report's rectangle.
1785    let mut occupied: Vec<CellRef> = Vec::new();
1786    if new_sheet.is_none()
1787        && let Some(sh) = d.wb.sheet(si)
1788    {
1789        for row in rect.start.row..=rect.end.row {
1790            for (c, cell) in sh.cells.row(row, rect.start.col, rect.end.col) {
1791                if cell.has_content() {
1792                    occupied.push(CellRef::new(row, c));
1793                }
1794            }
1795        }
1796        if sh.spill.keys().any(|c| rect.contains(*c)) && occupied.is_empty() {
1797            occupied.push(rect.start);
1798        }
1799    }
1800    if !occupied.is_empty() && !flag(p, "overwrite") {
1801        return Err(EngineError::Other(format!("The output range {} {OVERWRITE_ERROR}; pass overwrite: true to replace it.", rect.a1())));
1802    }
1803    let sheet_name = new_sheet.clone().or_else(|| d.wb.sheet(si).map(|sh| sh.name.clone())).unwrap_or_default();
1804    let new_index = edit(s, |cx| {
1805        let pct_fmt = NumFmt::new("0.00%");
1806        if let Some(name) = &new_sheet {
1807            cx.wb.sheets.push(std::sync::Arc::new(Sheet::new(name.clone())));
1808            cx.structural = true;
1809        }
1810        let pct_styles: Vec<(CellRef, crate::model::StyleId)> = {
1811            let sh = cx.wb.sheet(si).ok_or(EngineError::NoDocument)?;
1812            g.pct.iter().map(|(r, c)| CellRef::new(at.row + r, at.col + c)).map(|c| (c, sh.style_id(c))).collect()
1813        };
1814        let pct_styles: HashMap<CellRef, crate::model::StyleId> =
1815            pct_styles.into_iter().map(|(c, st)| (c, cx.wb.styles.derive(st, |s| s.num_fmt = pct_fmt.clone()))).collect();
1816        let sh = cx.sheet_mut(si)?;
1817        for c in &occupied {
1818            let style = sh.style_id(*c);
1819            sh.set_cell(*c, Cell { value: Value::Empty, formula: None, style, py: None });
1820        }
1821        let mut touched = occupied.clone();
1822        for ((r, c), v) in &g.cells {
1823            let cell = CellRef::new(at.row + r, at.col + c);
1824            let style = pct_styles.get(&cell).copied().unwrap_or_else(|| sh.style_id(cell));
1825            sh.set_cell(cell, Cell { value: v.clone(), formula: None, style, py: None });
1826            touched.push(cell);
1827        }
1828        for c in touched {
1829            cx.touch(si, c);
1830        }
1831        Ok(new_sheet.as_ref().map(|_| si))
1832    })?;
1833    if let Some(i) = new_index {
1834        s.doc_mut()?.wb_switch_sheet(i);
1835    }
1836    Ok(json!({"tool": tool, "sheet": sheet_name, "range": rect.a1(), "rows": rows, "cols": cols}))
1837}
1838
1839fn run(s: &mut Session, p: &Json) -> Result<Json> {
1840    let Some(t) = text(p, &["tool"]) else {
1841        s.ui_requests.push(crate::engine::UiRequest::Dialog("analysisToolPak".into(), json!({})));
1842        return Ok(json!({"tools": TOOLS.iter().map(|(id, label)| json!({"id": id, "label": label})).collect::<Vec<_>>()}));
1843    };
1844    let tool = tool_id(t).ok_or_else(|| bad(CMD, format!("unknown tool `{t}`")))?;
1845    let mut seed = None;
1846    let g = match tool {
1847        "descriptive" => descriptive(s, p)?,
1848        "histogram" => histogram(s, p)?,
1849        "correlation" => matrix(s, p, correlation, |_| Value::Number(1.0))?,
1850        "covariance" => matrix(s, p, covariance_p, |x| covariance_p(x, x))?,
1851        "regression" => regression(s, p)?,
1852        "movingAverage" => moving_average(s, p)?,
1853        "exponentialSmoothing" => exponential_smoothing(s, p)?,
1854        "rankPercentile" => rank_percentile(s, p)?,
1855        "tTestPaired" | "tTestEqual" | "tTestUnequal" => t_test(s, p, tool)?,
1856        "fTest" => f_test(s, p)?,
1857        "anovaSingle" => anova_single(s, p)?,
1858        "anovaTwoFactorWithout" => anova_two_factor_without(s, p)?,
1859        "anovaTwoFactorWith" => anova_two_factor_with(s, p)?,
1860        "zTest" => z_test(s, p)?,
1861        "fourierAnalysis" => fourier_analysis(s, p)?,
1862        "randomNumberGeneration" => {
1863            // A patterned sequence draws nothing: its seed is neither read nor reported.
1864            if !patterned(p) {
1865                seed = Some(seed_of(p)?);
1866            }
1867            random_numbers(s, p, seed.unwrap_or(0))?
1868        }
1869        "sampling" => {
1870            if random_sampling(p) {
1871                seed = Some(seed_of(p)?);
1872            }
1873            sampling(s, p, seed)?
1874        }
1875        _ => return Err(bad(CMD, format!("unknown tool `{t}`"))),
1876    };
1877    let mut out = write_report(s, p, tool, g)?;
1878    if let Some(k) = seed {
1879        out["seed"] = json!(k);
1880    }
1881    Ok(out)
1882}
1883
1884#[cfg(test)]
1885mod tests {
1886    use crate::base::{CellRef, Value};
1887    use serde_json::{Value as Json, json};
1888
1889    use super::{MAX_OUTCOMES, call, f64_of, pmf_table};
1890    use crate::engine::Session;
1891
1892    fn s() -> Session {
1893        let mut s = Session::new();
1894        s.new_workbook();
1895        s
1896    }
1897    fn v(s: &Session, a: &str) -> Value {
1898        let d = s.doc().unwrap();
1899        let (sh, body) = crate::engine::cmd::split_sheet(a);
1900        let si = sh.map(|n| d.wb.sheet_index(&n).unwrap()).unwrap_or(d.wb.active_sheet());
1901        d.wb.sheet(si).unwrap().value(CellRef::parse(body).unwrap())
1902    }
1903    fn n(s: &Session, a: &str) -> f64 {
1904        v(s, a).as_f64().unwrap_or(f64::NAN)
1905    }
1906    fn close(a: f64, b: f64, tol: f64) -> bool {
1907        (a - b).abs() <= tol
1908    }
1909    /// Evaluates a formula in a scratch cell.
1910    fn eval(s: &mut Session, f: &str) -> f64 {
1911        s.execute("cell.set", json!({"cell": "Z1000", "input": f})).unwrap();
1912        let x = n(s, "Z1000");
1913        s.execute("cell.set", json!({"cell": "Z1000", "input": ""})).unwrap();
1914        x
1915    }
1916
1917    #[test]
1918    fn forecast_standard_errors_match_the_full_window_rescan() {
1919        // #1043: the running sum must give what re-adding every window gave, gaps included.
1920        let x: [f64; 11] = [2.0, 4.0, 5.0, 4.0, 5.0, 7.0, 1.0, 3.0, 8.0, 6.0, 2.0];
1921        let f = [None, Some(3.0), Some(4.5), None, Some(4.0), Some(6.5), Some(2.0), Some(2.5), Some(7.0), Some(5.0), Some(4.0)];
1922        let rescan = |i: usize, w: usize| -> Value {
1923            let v = (i + 1).checked_sub(w).and_then(|s0| {
1924                let mut acc = 0.0;
1925                for j in s0..=i {
1926                    acc += (x.get(j)? - (*f.get(j)?)?).powi(2);
1927                }
1928                Some((acc / w as f64).sqrt())
1929            });
1930            v.map(Value::number).unwrap_or(Value::Error(crate::base::CellError::NA))
1931        };
1932        for w in 1..=5 {
1933            let g = super::forecast_grid(&x, &f, w, &json!({"standardErrors": true}));
1934            for i in 0..f.len() {
1935                let (got, want) = (g.cells.get(&(i as u32, 1)).cloned().unwrap_or(Value::Empty), rescan(i, w));
1936                match (got.as_f64(), want.as_f64()) {
1937                    (Some(a), Some(b)) => assert!(close(a, b, 1e-12), "window {w}, row {i}: {a} vs {b}"),
1938                    _ => assert_eq!(got, want, "window {w}, row {i}"),
1939                }
1940            }
1941        }
1942        // Row 3 has no forecast: windows through it are #N/A, the next full window after it is not.
1943        let g = super::forecast_grid(&x, &f, 3, &json!({"standardErrors": true}));
1944        assert_eq!(g.cells.get(&(5, 1)), Some(&Value::Error(crate::base::CellError::NA)));
1945        assert!(g.cells.get(&(6, 1)).and_then(Value::as_f64).is_some());
1946    }
1947
1948    #[test]
1949    fn descriptive_statistics_of_one_to_five() {
1950        // 1..5: mean 3, s = √2.5, kurtosis −1.2, skewness 0 (textbook values).
1951        let mut s = s();
1952        s.execute("range.setValues", json!({"range": "A1", "values": [["Score"], [1], [2], [3], [4], [5]]})).unwrap();
1953        let r = s
1954            .execute("data.analysisToolPak", json!({"tool": "Descriptive Statistics", "range": "A1:A6", "labels": true, "output": "C1", "confidence": 95, "kthLargest": 2, "kthSmallest": 1}))
1955            .unwrap();
1956        assert_eq!(r["range"], "C1:D18");
1957        assert_eq!(v(&s, "C1"), Value::text("Score"));
1958        assert_eq!(v(&s, "C3"), Value::text("Mean"));
1959        assert_eq!(n(&s, "D3"), 3.0);
1960        assert!(close(n(&s, "D4"), (2.5f64 / 5.0).sqrt(), 1e-12));
1961        assert_eq!(n(&s, "D5"), 3.0);
1962        assert_eq!(v(&s, "D6"), Value::Error(crate::base::CellError::NA));
1963        assert!(close(n(&s, "D7"), 2.5f64.sqrt(), 1e-12));
1964        assert_eq!(n(&s, "D8"), 2.5);
1965        assert!(close(n(&s, "D9"), -1.2, 1e-12));
1966        assert!(close(n(&s, "D10"), 0.0, 1e-12));
1967        assert_eq!((n(&s, "D11"), n(&s, "D12"), n(&s, "D13"), n(&s, "D14"), n(&s, "D15")), (4.0, 1.0, 5.0, 15.0, 5.0));
1968        assert_eq!(v(&s, "C16"), Value::text("Largest(2)"));
1969        assert_eq!(n(&s, "D16"), 4.0);
1970        assert_eq!(n(&s, "D17"), 1.0);
1971        assert_eq!(v(&s, "C18"), Value::text("Confidence Level(95.0%)"));
1972        let want = eval(&mut s, "=CONFIDENCE.T(0.05,STDEV.S(A2:A6),5)");
1973        assert!(close(n(&s, "D18"), want, 1e-9), "{} vs {want}", n(&s, "D18"));
1974        // The output range now holds data: a second run is refused unless it may overwrite.
1975        let again = json!({"tool": "descriptive", "range": "A2:A6", "output": "C1"});
1976        let e = s.execute("data.analysisToolPak", again.clone()).unwrap_err().to_string();
1977        assert!(e.contains(super::OVERWRITE_ERROR), "{e}");
1978        let mut again = again;
1979        again["overwrite"] = json!(true);
1980        s.execute("data.analysisToolPak", again).unwrap();
1981        assert_eq!(v(&s, "C1"), Value::text("Column1"));
1982    }
1983
1984    #[test]
1985    fn regression_matches_textbook_and_linest() {
1986        // x = 1..5, y = 2,4,5,4,5: b = 0.6, a = 2.2, R² = 0.6, F = 4.5 (textbook example).
1987        let mut s = s();
1988        s.execute("range.setValues", json!({"range": "A1", "values": [["y", "x"], [2, 1], [4, 2], [5, 3], [4, 4], [5, 5]]})).unwrap();
1989        s.execute(
1990            "data.analysisToolPak",
1991            json!({"tool": "regression", "inputY": "A1:A6", "inputX": "B1:B6", "labels": true, "newSheet": "Fit", "residuals": true, "standardizedResiduals": true}),
1992        )
1993        .unwrap();
1994        assert!(close(n(&s, "Fit!B5"), 0.6, 1e-12), "R²");
1995        assert!(close(n(&s, "Fit!B6"), 1.0 - 0.4 * 4.0 / 3.0, 1e-12), "adjusted R²");
1996        assert!(close(n(&s, "Fit!B7"), 0.8f64.sqrt(), 1e-12), "standard error");
1997        assert!(close(n(&s, "Fit!E12"), 4.5, 1e-12), "F");
1998        assert!(close(n(&s, "Fit!B17"), 2.2, 1e-12), "intercept");
1999        assert!(close(n(&s, "Fit!B18"), 0.6, 1e-12), "slope");
2000        assert_eq!(v(&s, "Fit!A18"), Value::text("x"));
2001        assert!(close(n(&s, "Fit!C18"), (0.8f64 / 10.0).sqrt(), 1e-12), "SE of slope");
2002        // Simple regression: Significance F equals the slope's p-value.
2003        assert!(close(n(&s, "Fit!F12"), n(&s, "Fit!E18"), 1e-9));
2004        assert!(n(&s, "Fit!E18") > 0.12 && n(&s, "Fit!E18") < 0.13);
2005        assert_eq!(v(&s, "Fit!A22"), Value::text("RESIDUAL OUTPUT"));
2006        assert!(close(n(&s, "Fit!B25"), 2.8, 1e-12), "first prediction");
2007        assert!(close(n(&s, "Fit!C25"), -0.8, 1e-12), "first residual");
2008        // Multiple regression agrees with LINEST.
2009        s.execute(
2010            "range.setValues",
2011            json!({"range": "Sheet1!D1", "values": [[1, 2, 3], [3, 1, 5], [4, 5, 2], [7, 3, 4], [6, 6, 1], [9, 4, 6], [8, 7, 3]]}),
2012        )
2013        .unwrap();
2014        s.execute("data.analysisToolPak", json!({"tool": "regression", "inputY": "Sheet1!D1:D7", "inputX": "Sheet1!E1:F7", "newSheet": "Multi"}))
2015            .unwrap();
2016        let slope2 = eval(&mut s, "=INDEX(LINEST(Sheet1!D1:D7,Sheet1!E1:F7,TRUE,TRUE),1,1)");
2017        let slope1 = eval(&mut s, "=INDEX(LINEST(Sheet1!D1:D7,Sheet1!E1:F7,TRUE,TRUE),1,2)");
2018        let se1 = eval(&mut s, "=INDEX(LINEST(Sheet1!D1:D7,Sheet1!E1:F7,TRUE,TRUE),2,2)");
2019        let icpt = eval(&mut s, "=INDEX(LINEST(Sheet1!D1:D7,Sheet1!E1:F7,TRUE,TRUE),1,3)");
2020        assert!(close(n(&s, "Multi!B17"), icpt, 1e-9));
2021        assert!(close(n(&s, "Multi!B18"), slope1, 1e-9));
2022        assert!(close(n(&s, "Multi!B19"), slope2, 1e-9));
2023        assert!(close(n(&s, "Multi!C18"), se1, 1e-9));
2024        assert_eq!(v(&s, "Multi!A19"), Value::text("X Variable 2"));
2025    }
2026
2027    #[test]
2028    fn tests_of_means_and_variances_match_worksheet_functions() {
2029        let mut s = s();
2030        s.execute("range.setValues", json!({"range": "A1", "values": [[12, 10], [15, 14], [11, 9], [18, 15], [14, 13], [16, 12]]})).unwrap();
2031        s.execute("data.analysisToolPak", json!({"tool": "tTestEqual", "range1": "A1:A6", "range2": "B1:B6", "output": "D1"})).unwrap();
2032        assert_eq!(v(&s, "D1"), Value::text("t-Test: Two-Sample Assuming Equal Variances"));
2033        assert_eq!(n(&s, "E9"), 10.0, "df");
2034        let p2 = eval(&mut s, "=T.TEST(A1:A6,B1:B6,2,2)");
2035        assert!(close(n(&s, "E13"), p2, 1e-9), "two-tail p");
2036        s.execute("data.analysisToolPak", json!({"tool": "tTestPaired", "range1": "A1:A6", "range2": "B1:B6", "output": "H1"})).unwrap();
2037        let pp = eval(&mut s, "=T.TEST(A1:A6,B1:B6,2,1)");
2038        assert_eq!(v(&s, "H13"), Value::text("P(T<=t) two-tail"));
2039        assert!(close(n(&s, "I13"), pp, 1e-9), "paired two-tail p");
2040        s.execute("data.analysisToolPak", json!({"tool": "fTest", "range1": "A1:A6", "range2": "B1:B6", "output": "L1"})).unwrap();
2041        let fp = eval(&mut s, "=F.TEST(A1:A6,B1:B6)");
2042        assert!(close(n(&s, "M9") * 2.0, fp, 1e-9), "one-tail p is half of F.TEST");
2043        // Anova on {1,2,3}, {4,5,6}, {7,8,9}: SSB 54, SSW 6, F 27.
2044        s.execute("range.setValues", json!({"range": "A20", "values": [[1, 4, 7], [2, 5, 8], [3, 6, 9]]})).unwrap();
2045        s.execute("data.analysisToolPak", json!({"tool": "anovaSingle", "range": "A20:C22", "output": "E20"})).unwrap();
2046        assert_eq!(v(&s, "E31"), Value::text("Between Groups"));
2047        assert_eq!((n(&s, "F31"), n(&s, "F32"), n(&s, "I31")), (54.0, 6.0, 27.0));
2048        let p = eval(&mut s, "=F.DIST.RT(27,2,6)");
2049        assert!(close(n(&s, "J31"), p, 1e-12));
2050    }
2051
2052    #[test]
2053    fn histogram_matrices_smoothing_and_ranks() {
2054        let mut s = s();
2055        s.execute("range.setValues", json!({"range": "A1", "values": [[1], [2], [2], [3], [5], [7]]})).unwrap();
2056        s.execute("range.setValues", json!({"range": "B1", "values": [[2], [4], [6]]})).unwrap();
2057        s.execute(
2058            "data.analysisToolPak",
2059            json!({"tool": "histogram", "range": "A1:A6", "bins": "B1:B3", "output": "D1", "cumulative": true, "pareto": true}),
2060        )
2061        .unwrap();
2062        assert_eq!((n(&s, "E2"), n(&s, "E3"), n(&s, "E4"), n(&s, "E5")), (3.0, 1.0, 1.0, 1.0));
2063        assert_eq!(v(&s, "D5"), Value::text("More"));
2064        assert_eq!(n(&s, "F5"), 1.0);
2065        assert_eq!((n(&s, "G2"), n(&s, "H2")), (2.0, 3.0), "Pareto puts the largest bin first");
2066        // Correlation and covariance against CORREL and COVARIANCE.P.
2067        s.execute("range.setValues", json!({"range": "J1", "values": [[1, 2], [2, 4], [3, 5], [4, 4], [5, 5]]})).unwrap();
2068        s.execute("data.analysisToolPak", json!({"tool": "correlation", "range": "J1:K5", "output": "M1"})).unwrap();
2069        assert_eq!(n(&s, "N2"), 1.0);
2070        assert!(close(n(&s, "N3"), eval(&mut s, "=CORREL(J1:J5,K1:K5)"), 1e-12));
2071        assert!(v(&s, "O2").is_empty(), "upper triangle is blank");
2072        s.execute("data.analysisToolPak", json!({"tool": "covariance", "range": "J1:K5", "output": "M10"})).unwrap();
2073        assert!(close(n(&s, "N12"), eval(&mut s, "=COVARIANCE.P(J1:J5,K1:K5)"), 1e-12));
2074        assert!(close(n(&s, "N11"), eval(&mut s, "=VAR.P(J1:J5)"), 1e-12));
2075        // Moving average of 3 and exponential smoothing with damping 0.3.
2076        s.execute("data.analysisToolPak", json!({"tool": "movingAverage", "range": "K1:K5", "output": "Q1", "standardErrors": true})).unwrap();
2077        assert_eq!(v(&s, "Q2"), Value::Error(crate::base::CellError::NA));
2078        assert!(close(n(&s, "Q3"), 11.0 / 3.0, 1e-12));
2079        assert!(close(n(&s, "Q5"), 14.0 / 3.0, 1e-12));
2080        assert_eq!(v(&s, "R4"), Value::Error(crate::base::CellError::NA));
2081        assert!(n(&s, "R5").is_finite());
2082        s.execute("data.analysisToolPak", json!({"tool": "exponentialSmoothing", "range": "K1:K5", "output": "S1"})).unwrap();
2083        assert_eq!(n(&s, "S2"), 2.0);
2084        assert!(close(n(&s, "S3"), 0.7 * 4.0 + 0.3 * 2.0, 1e-12));
2085        // Rank and Percentile of 2,4,5,4,5: the two 5s share rank 1 (3 of 4 others below them).
2086        s.execute("data.analysisToolPak", json!({"tool": "rankPercentile", "range": "K1:K5", "output": "U1"})).unwrap();
2087        assert_eq!((n(&s, "U2"), n(&s, "V2"), n(&s, "W2"), n(&s, "X2")), (3.0, 5.0, 1.0, 0.75));
2088        assert_eq!((n(&s, "U3"), n(&s, "W3")), (5.0, 1.0));
2089        assert_eq!((n(&s, "V6"), n(&s, "X6")), (2.0, 0.0));
2090        assert_eq!(n(&s, "X4"), 0.25, "4 has one value below it among five");
2091    }
2092
2093    #[test]
2094    fn two_factor_anova_without_replication_matches_a_textbook_example() {
2095        // Hardness testing, a randomized complete block design (Montgomery, Design and Analysis
2096        // of Experiments, Example 4.1): four tips (rows) by four coupons (columns). Published:
2097        // SS tips 0.385, SS coupons 0.825, SS error 0.080, SS total 1.290, F tips 14.44.
2098        let mut s = s();
2099        s.execute(
2100            "range.setValues",
2101            json!({"range": "A1", "values": [
2102                ["", "Coupon 1", "Coupon 2", "Coupon 3", "Coupon 4"],
2103                ["Tip 1", 9.3, 9.4, 9.6, 10.0],
2104                ["Tip 2", 9.4, 9.3, 9.8, 9.9],
2105                ["Tip 3", 9.2, 9.4, 9.5, 9.7],
2106                ["Tip 4", 9.7, 9.6, 10.0, 10.2],
2107            ]}),
2108        )
2109        .unwrap();
2110        s.execute("data.analysisToolPak", json!({"tool": "Anova: Two-Factor Without Replication", "range": "A1:E5", "labels": true, "output": "H1"}))
2111            .unwrap();
2112        assert_eq!(v(&s, "H1"), Value::text("Anova: Two-Factor Without Replication"));
2113        assert_eq!((v(&s, "H4"), n(&s, "I4")), (Value::text("Tip 1"), 4.0));
2114        assert!(close(n(&s, "J4"), 38.3, 1e-12), "sum of tip 1");
2115        assert_eq!(v(&s, "H9"), Value::text("Coupon 1"));
2116        assert!(close(n(&s, "K12"), 9.95, 1e-12), "average of coupon 4");
2117        assert_eq!(v(&s, "H17"), Value::text("Rows"));
2118        assert!(close(n(&s, "I17"), 0.385, 1e-12));
2119        assert!(close(n(&s, "I18"), 0.825, 1e-12));
2120        assert!(close(n(&s, "I19"), 0.080, 1e-12));
2121        assert!(close(n(&s, "I21"), 1.290, 1e-12));
2122        assert_eq!((n(&s, "J17"), n(&s, "J19"), n(&s, "J21")), (3.0, 9.0, 15.0));
2123        assert!(close(n(&s, "L17"), 14.4375, 1e-9), "F for tips");
2124        assert!(close(n(&s, "M17"), eval(&mut s, "=F.DIST.RT(14.4375,3,9)"), 1e-9));
2125        assert!(close(n(&s, "N17"), eval(&mut s, "=F.INV.RT(0.05,3,9)"), 1e-9));
2126        assert!(v(&s, "L19").is_empty(), "the error line has no F");
2127    }
2128
2129    #[test]
2130    fn two_factor_anova_with_replication_matches_a_textbook_example() {
2131        let mut s = s();
2132        // Battery life (Montgomery, Example 5.1): three materials (samples of four rows) by three
2133        // temperatures (columns). Published: SS material 10683.72, SS temperature 39118.72,
2134        // SS interaction 9613.78, SS error 18230.75 on 27 df, SS total 77646.97.
2135        s.execute(
2136            "range.setValues",
2137            json!({"range": "A20", "values": [
2138                ["Material", "15 F", "70 F", "125 F"],
2139                ["Type 1", 130, 34, 20], ["", 155, 40, 70], ["", 74, 80, 82], ["", 180, 75, 58],
2140                ["Type 2", 150, 136, 25], ["", 188, 122, 70], ["", 159, 106, 58], ["", 126, 115, 45],
2141                ["Type 3", 138, 174, 96], ["", 110, 120, 104], ["", 168, 150, 82], ["", 160, 139, 60],
2142            ]}),
2143        )
2144        .unwrap();
2145        s.execute("data.analysisToolPak", json!({"tool": "anovaTwoFactorWith", "range": "A20:D32", "rowsPerSample": 4, "newSheet": "Battery"}))
2146            .unwrap();
2147        assert_eq!(v(&s, "Battery!A3"), Value::text("SUMMARY"));
2148        assert_eq!((v(&s, "Battery!B3"), v(&s, "Battery!E3")), (Value::text("15 F"), Value::text("Total")));
2149        assert_eq!(v(&s, "Battery!A4"), Value::text("Type 1"));
2150        assert_eq!((n(&s, "Battery!B5"), n(&s, "Battery!B6"), n(&s, "Battery!E5")), (4.0, 539.0, 12.0));
2151        assert_eq!(v(&s, "Battery!A22"), Value::text("Total"));
2152        assert_eq!(n(&s, "Battery!B24"), 1738.0, "sum of the 15 F column");
2153        assert_eq!(v(&s, "Battery!A29"), Value::text("ANOVA"));
2154        assert_eq!(
2155            (v(&s, "Battery!A31"), v(&s, "Battery!A34"), v(&s, "Battery!A36")),
2156            (Value::text("Sample"), Value::text("Within"), Value::text("Total"))
2157        );
2158        assert!(close(n(&s, "Battery!B31"), 10683.72, 0.01));
2159        assert!(close(n(&s, "Battery!B32"), 39118.72, 0.01));
2160        assert!(close(n(&s, "Battery!B33"), 9613.78, 0.01));
2161        assert!(close(n(&s, "Battery!B34"), 18230.75, 0.01));
2162        assert!(close(n(&s, "Battery!B36"), 77646.97, 0.01));
2163        assert_eq!((n(&s, "Battery!C33"), n(&s, "Battery!C34"), n(&s, "Battery!C36")), (4.0, 27.0, 35.0));
2164        assert!(close(n(&s, "Battery!E31"), 7.91, 0.005) && close(n(&s, "Battery!E32"), 28.97, 0.005) && close(n(&s, "Battery!E33"), 3.56, 0.005));
2165        assert!(close(n(&s, "Battery!F33"), 0.0186, 0.00005), "interaction P-value");
2166        // Rows per sample must divide the data rows.
2167        let bad = json!({"tool": "anovaTwoFactorWith", "range": "Sheet1!A20:D32", "rowsPerSample": 5, "output": "F60"});
2168        assert!(s.execute("data.analysisToolPak", bad).is_err());
2169    }
2170
2171    #[test]
2172    fn z_test_with_known_variances() {
2173        // x̄ = 3 (n = 5, σ² = 2), ȳ = 4 (n = 3, σ² = 6): z = −1/√2.4. Critical values from the
2174        // standard normal table: 1.644854 (one tail) and 1.959964 (two tails) at α = 0.05.
2175        let mut s = s();
2176        s.execute("range.setValues", json!({"range": "A1", "values": [["x", "y"], [1, 2], [2, 4], [3, 6], [4, null], [5, null]]})).unwrap();
2177        s.execute(
2178            "data.analysisToolPak",
2179            json!({"tool": "zTest", "range1": "A1:A6", "range2": "B1:B6", "labels": true, "variance1": 2, "variance2": 6, "output": "D1"}),
2180        )
2181        .unwrap();
2182        assert_eq!(v(&s, "D1"), Value::text("z-Test: Two Sample for Means"));
2183        assert_eq!((v(&s, "E3"), v(&s, "F3")), (Value::text("x"), Value::text("y")));
2184        assert_eq!((n(&s, "E4"), n(&s, "F4"), n(&s, "E5"), n(&s, "F6")), (3.0, 4.0, 2.0, 3.0));
2185        let z = -1.0 / 2.4f64.sqrt();
2186        assert!(close(n(&s, "E8"), z, 1e-12));
2187        let tail = 1.0 - eval(&mut s, &format!("=NORM.S.DIST({},TRUE)", -z));
2188        assert!(close(n(&s, "E9"), tail, 1e-12) && close(n(&s, "E9"), 0.2593, 1e-4));
2189        assert!(close(n(&s, "E10"), 1.644_853_6, 1e-6));
2190        assert!(close(n(&s, "E11"), 2.0 * tail, 1e-12));
2191        assert!(close(n(&s, "E12"), 1.959_964_0, 1e-6));
2192        // Unknown or non-positive variances are refused.
2193        assert!(s.execute("data.analysisToolPak", json!({"tool": "zTest", "range1": "A2:A6", "range2": "B2:B4", "output": "H1"})).is_err());
2194        let zero = json!({"tool": "zTest", "range1": "A2:A6", "range2": "B2:B4", "variance1": 0, "variance2": 1, "output": "H1"});
2195        assert!(s.execute("data.analysisToolPak", zero).is_err());
2196    }
2197
2198    #[test]
2199    fn fourier_analysis_matches_known_transforms() {
2200        let mut s = s();
2201        // DFT of 1, 2, 3, 4: 10, −2+2i, −2, −2−2i.
2202        s.execute("range.setValues", json!({"range": "A1", "values": [["Signal"], [1], [2], [3], [4]]})).unwrap();
2203        s.execute("data.analysisToolPak", json!({"tool": "Fourier Analysis", "range": "A1:A5", "labels": true, "output": "C1"})).unwrap();
2204        let col = |s: &Session, c: &str, rows: std::ops::RangeInclusive<u32>| rows.map(|r| v(s, &format!("{c}{r}"))).collect::<Vec<_>>();
2205        assert_eq!(col(&s, "C", 1..=5), ["Signal", "10", "-2+2i", "-2", "-2-2i"].map(Value::text).to_vec());
2206        // The inverse transform of that output returns the signal.
2207        s.execute("data.analysisToolPak", json!({"tool": "fourierAnalysis", "range": "C2:C5", "inverse": true, "output": "E1"})).unwrap();
2208        assert_eq!(col(&s, "E", 1..=4), ["1", "2", "3", "4"].map(Value::text).to_vec());
2209        // cos(2πn/8) over eight points: N/2 = 4 at the first and last frequencies, 0 elsewhere.
2210        let wave: Vec<Json> = (0..8).map(|k| json!([(std::f64::consts::PI * k as f64 / 4.0).cos()])).collect();
2211        s.execute("range.setValues", json!({"range": "G1", "values": wave})).unwrap();
2212        s.execute("data.analysisToolPak", json!({"tool": "fourierAnalysis", "range": "G1:G8", "output": "H1"})).unwrap();
2213        assert_eq!(col(&s, "H", 1..=8), ["0", "4", "0", "0", "0", "0", "0", "4"].map(Value::text).to_vec());
2214        // Complex text input; a single value is its own transform.
2215        s.execute("range.setValues", json!({"range": "J1", "values": [["3+4i"], ["1-i"]]})).unwrap();
2216        s.execute("data.analysisToolPak", json!({"tool": "fourierAnalysis", "range": "J1:J2", "output": "K1"})).unwrap();
2217        assert_eq!(col(&s, "K", 1..=2), ["4+3i", "2+5i"].map(Value::text).to_vec());
2218        s.execute("data.analysisToolPak", json!({"tool": "fourierAnalysis", "range": "J1", "output": "L1"})).unwrap();
2219        assert_eq!(v(&s, "L1"), Value::text("3+4i"));
2220        // Three values are not a power of two; a word is not a number.
2221        assert!(s.execute("data.analysisToolPak", json!({"tool": "fourierAnalysis", "range": "A2:A4", "output": "N1"})).is_err());
2222        assert!(s.execute("data.analysisToolPak", json!({"tool": "fourierAnalysis", "range": "A1:A4", "output": "N1"})).is_err());
2223        assert!(s.execute("data.analysisToolPak", json!({"tool": "fourierAnalysis", "range": "A1:A8192", "output": "N1"})).is_err());
2224    }
2225
2226    #[test]
2227    fn fourier_analysis_keeps_a_small_imaginary_part_beside_a_large_real_one() {
2228        let mut s = s();
2229        // With one value the transform is the identity and performs no arithmetic, so no part of
2230        // the output is rounding noise: 1e14 + 1i comes back as 1e14 + 1i, forward and inverse.
2231        s.execute("range.setValues", json!({"range": "A1", "values": [["100000000000000+1i"]]})).unwrap();
2232        s.execute("data.analysisToolPak", json!({"tool": "fourierAnalysis", "range": "A1", "output": "C1"})).unwrap();
2233        assert_eq!(eval(&mut s, "=IMREAL(C1)"), 1e14);
2234        assert_eq!(eval(&mut s, "=IMAGINARY(C1)"), 1.0);
2235        s.execute("data.analysisToolPak", json!({"tool": "fourierAnalysis", "range": "C1", "inverse": true, "output": "E1"})).unwrap();
2236        assert_eq!(eval(&mut s, "=IMREAL(E1)"), 1e14);
2237        assert_eq!(eval(&mut s, "=IMAGINARY(E1)"), 1.0);
2238    }
2239
2240    #[test]
2241    fn fourier_analysis_round_trip_recovers_small_components_beside_a_large_dc() {
2242        let mut s = s();
2243        // x_n = 1e6 + 1e-7·cos(2πn/8) for n = 0 … 7. The transform of a constant c over N points is
2244        // N·c at k = 0 and 0 elsewhere; the transform of a·cos(2πn/N) is a·N/2 at k = 1 and
2245        // k = N − 1. So X_0 = 8·1e6 = 8e6, X_1 = X_7 = 1e-7·8/2 = 4e-7 and X_2 … X_6 = 0. The inputs
2246        // are stored to within ulp(1e6)/2 ≈ 6e-11 and the first butterfly stage subtracts nearby
2247        // numbers exactly, so the harmonics carry at most about 5e-10 of noise. The cleanup bound
2248        // 8·ε·log2(8)·Σ|x_n| ≈ 8 · 2.2e-16 · 3 · 8e6 ≈ 4.3e-8 zeroes noise of that size and keeps
2249        // 4e-7; a cutoff relative to the largest output (8e6 · 1e-13 = 8e-7) would zero the
2250        // harmonics.
2251        let x = |k: usize| 1e6 + 1e-7 * (std::f64::consts::PI * k as f64 / 4.0).cos();
2252        let wave: Vec<Json> = (0..8).map(|k| json!([x(k)])).collect();
2253        s.execute("range.setValues", json!({"range": "A1", "values": wave})).unwrap();
2254        s.execute("data.analysisToolPak", json!({"tool": "fourierAnalysis", "range": "A1:A8", "output": "C1"})).unwrap();
2255        assert!(close(eval(&mut s, "=IMREAL(C1)"), 8e6, 1e-6));
2256        assert_eq!(eval(&mut s, "=IMAGINARY(C1)"), 0.0);
2257        for cell in ["C2", "C8"] {
2258            assert!(close(eval(&mut s, &format!("=IMREAL({cell})")), 4e-7, 5e-9), "{cell}");
2259            assert_eq!(eval(&mut s, &format!("=IMAGINARY({cell})")), 0.0, "{cell}");
2260        }
2261        for r in 3..=7 {
2262            assert_eq!(v(&s, &format!("C{r}")), Value::text("0"), "C{r}");
2263        }
2264        // The inverse of that output returns each x_n to within 2e-9 (2e-15 relative); without the
2265        // harmonics every value would come back as 1e6, off by up to 1e-7.
2266        s.execute("data.analysisToolPak", json!({"tool": "fourierAnalysis", "range": "C1:C8", "inverse": true, "output": "E1"})).unwrap();
2267        for k in 0..8 {
2268            let cell = format!("E{}", k + 1);
2269            assert!(close(eval(&mut s, &format!("=IMREAL({cell})")), x(k), 2e-9), "{cell}");
2270            assert_eq!(eval(&mut s, &format!("=IMAGINARY({cell})")), 0.0, "{cell}");
2271        }
2272    }
2273
2274    /// Column `col` of sheet `sheet`, rows 1 to `rows`, as numbers.
2275    fn numbers_of(s: &Session, sheet: &str, col: &str, rows: u32) -> Vec<f64> {
2276        (1..=rows).map(|r| n(s, &format!("{sheet}!{col}{r}"))).collect()
2277    }
2278    fn mean_var(x: &[f64]) -> (f64, f64) {
2279        let m = x.iter().sum::<f64>() / x.len() as f64;
2280        (m, x.iter().map(|v| (v - m).powi(2)).sum::<f64>() / (x.len() - 1) as f64)
2281    }
2282
2283    #[test]
2284    fn random_numbers_are_seeded_and_follow_their_distributions() {
2285        let mut s = s();
2286        let rng = |s: &mut Session, sheet: &str, p: Json| {
2287            let mut p = p;
2288            p["tool"] = json!("randomNumberGeneration");
2289            p["newSheet"] = json!(sheet);
2290            s.execute("data.analysisToolPak", p).unwrap()
2291        };
2292        // The same seed repeats the numbers; the result reports the seed.
2293        let r = rng(&mut s, "U1", json!({"variables": 2, "count": 2000, "distribution": "uniform", "from": 2, "to": 4, "seed": 42}));
2294        assert_eq!((r["seed"].as_u64(), r["range"].as_str()), (Some(42), Some("A1:B2000")));
2295        rng(&mut s, "U2", json!({"variables": 2, "count": 2000, "distribution": "uniform", "from": 2, "to": 4, "seed": 42}));
2296        let (a, b) = (numbers_of(&s, "U1", "B", 2000), numbers_of(&s, "U2", "B", 2000));
2297        assert_eq!(a, b);
2298        assert!(a.iter().all(|x| (2.0..4.0).contains(x)));
2299        // Uniform on [2, 4]: mean 3, variance 1/3.
2300        let (m, var) = mean_var(&a);
2301        assert!((m - 3.0).abs() < 0.05 && (var - 1.0 / 3.0).abs() < 0.03, "{m} {var}");
2302        let unseeded = rng(&mut s, "U3", json!({"count": 1}));
2303        assert!(unseeded["seed"].as_u64().is_some(), "a seed is chosen and reported");
2304        // Normal(10, 2): mean 10, variance 4.
2305        rng(&mut s, "N", json!({"count": 20000, "distribution": "normal", "mean": 10, "standardDeviation": 2, "seed": 7}));
2306        let (m, var) = mean_var(&numbers_of(&s, "N", "A", 20000));
2307        assert!((m - 10.0).abs() < 0.05 && (var - 4.0).abs() < 0.15, "{m} {var}");
2308        // Binomial(20, 0.3): mean np = 6, variance np(1 − p) = 4.2.
2309        rng(&mut s, "B", json!({"count": 20000, "distribution": "binomial", "p": 0.3, "trials": 20, "seed": 8}));
2310        let x = numbers_of(&s, "B", "A", 20000);
2311        assert!(x.iter().all(|v| v.fract() == 0.0 && (0.0..=20.0).contains(v)));
2312        let (m, var) = mean_var(&x);
2313        assert!((m - 6.0).abs() < 0.08 && (var - 4.2).abs() < 0.2, "{m} {var}");
2314        // Poisson(3.5): mean and variance 3.5.
2315        rng(&mut s, "P", json!({"count": 20000, "distribution": "poisson", "lambda": 3.5, "seed": 9}));
2316        let (m, var) = mean_var(&numbers_of(&s, "P", "A", 20000));
2317        assert!((m - 3.5).abs() < 0.08 && (var - 3.5).abs() < 0.2, "{m} {var}");
2318        // Bernoulli(0.25) and a discrete distribution on {1, 2} with P(2) = 0.75.
2319        rng(&mut s, "Ber", json!({"count": 4000, "distribution": "bernoulli", "p": 0.25, "seed": 10}));
2320        let ones = numbers_of(&s, "Ber", "A", 4000).iter().filter(|v| **v == 1.0).count();
2321        assert!((900..1100).contains(&ones), "{ones}");
2322        s.execute("range.setValues", json!({"range": "Sheet1!A1", "values": [[1, 0.25], [2, 0.75]]})).unwrap();
2323        rng(&mut s, "D", json!({"count": 4000, "distribution": "discrete", "valueRange": "Sheet1!A1:B2", "seed": 11}));
2324        let x = numbers_of(&s, "D", "A", 4000);
2325        assert!(x.iter().all(|v| *v == 1.0 || *v == 2.0));
2326        let twos = x.iter().filter(|v| **v == 2.0).count();
2327        assert!((2900..3100).contains(&twos), "{twos}");
2328        // Patterned: 1 to 3, each number twice, the sequence twice.
2329        rng(&mut s, "Pat", json!({"distribution": "patterned", "from": 1, "to": 3, "step": 1, "repeatEach": 2, "repeatSequence": 2}));
2330        assert_eq!(numbers_of(&s, "Pat", "A", 12), [1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0]);
2331        assert!(v(&s, "Pat!A13").is_empty());
2332        // Output is bounded; parameters are checked.
2333        for p in [
2334            json!({"count": 2_000_000}),
2335            json!({"count": 1000, "variables": 2000}),
2336            json!({"count": 0}),
2337            json!({"count": 1.5}),
2338            json!({"distribution": "uniform"}),
2339            json!({"count": 1, "distribution": "bernoulli", "p": 2}),
2340            json!({"count": 1, "distribution": "binomial", "trials": 1e9}),
2341            json!({"count": 1, "distribution": "poisson", "lambda": -1}),
2342            json!({"count": 1, "distribution": "normal", "standardDeviation": -1}),
2343            json!({"count": 1, "distribution": "discrete", "valueRange": "Sheet1!A1:A2"}),
2344            json!({"count": 1, "distribution": "patterned", "from": 1, "to": 1e9, "step": 1}),
2345            json!({"count": 1, "distribution": "patterned", "from": 3, "to": 1}),
2346            json!({"count": 1, "distribution": "weird"}),
2347            json!({"count": 1, "seed": -1}),
2348            json!({"count": 1, "seed": "abc"}),
2349        ] {
2350            let mut q = p.clone();
2351            q["tool"] = json!("randomNumberGeneration");
2352            q["output"] = json!("Sheet1!H1");
2353            assert!(s.execute("data.analysisToolPak", q).is_err(), "{p}");
2354        }
2355        s.execute("range.setValues", json!({"range": "Sheet1!A1", "values": [[1, 0.5], [2, 0.6]]})).unwrap();
2356        let p =
2357            json!({"tool": "randomNumberGeneration", "count": 1, "distribution": "discrete", "valueRange": "Sheet1!A1:B2", "output": "Sheet1!H1"});
2358        assert!(s.execute("data.analysisToolPak", p).is_err(), "probabilities must add up to 1");
2359    }
2360
2361    #[test]
2362    fn binomial_and_poisson_tables_agree_with_the_worksheet_functions() {
2363        // The recurrence outward from the mode reproduces BINOM.DIST and POISSON.DIST at every
2364        // tabulated outcome, the table carries the whole distribution, and it is only as wide as
2365        // the outcomes that matter. The worksheet functions themselves are accurate to about
2366        // n · ε for n trials (their logarithms are of that size), hence the moderate n and λ.
2367        let binom = |k: f64, n: f64, p: f64| f64_of(&call("BINOM.DIST", &[Value::Number(k), Value::Number(n), Value::Number(p), Value::Bool(false)]));
2368        for (n, p) in [(20.0f64, 0.3f64), (100.0, 0.5), (50.0, 0.9), (30.0, 0.01), (7.0, 0.999), (1.0, 0.5)] {
2369            let mode = ((n + 1.0) * p).floor().min(n);
2370            let odds = p / (1.0 - p);
2371            let (first, pmf) = pmf_table(n, mode, binom(mode, n, p), |k| (n - k) / (k + 1.0) * odds).unwrap();
2372            assert!(first >= 0.0 && first + pmf.len() as f64 - 1.0 <= n, "n={n} p={p}: {first} + {}", pmf.len());
2373            for (i, got) in pmf.iter().enumerate() {
2374                let k = first + i as f64;
2375                let want = binom(k, n, p);
2376                assert!(((got - want) / want).abs() < 1e-12, "n={n} p={p} k={k}: {got} vs {want}");
2377            }
2378            assert!((pmf.iter().sum::<f64>() - 1.0).abs() < 1e-12, "n={n} p={p}");
2379        }
2380        let poisson = |k: f64, lambda: f64| f64_of(&call("POISSON.DIST", &[Value::Number(k), Value::Number(lambda), Value::Bool(false)]));
2381        for lambda in [0.2f64, 3.5, 50.0] {
2382            let mode = lambda.floor();
2383            let (first, pmf) = pmf_table(MAX_OUTCOMES, mode, poisson(mode, lambda), |k| lambda / (k + 1.0)).unwrap();
2384            assert_eq!(first, 0.0);
2385            for (i, got) in pmf.iter().enumerate() {
2386                let want = poisson(i as f64, lambda);
2387                assert!(((got - want) / want).abs() < 1e-12, "lambda={lambda} k={i}: {got} vs {want}");
2388            }
2389            assert!((pmf.iter().sum::<f64>() - 1.0).abs() < 1e-12, "lambda={lambda}");
2390        }
2391        // Binomial(1e6, 0.5): standard deviation 500, so the terms vanish within about 38
2392        // standard deviations below the mode and the sum reaches 1 within about 8 above it:
2393        // roughly 23,000 entries starting near 481,000, not 1,000,001 starting at 0.
2394        let (n, p) = (1_000_000.0, 0.5);
2395        let (first, pmf) = pmf_table(n, 500_000.0, binom(500_000.0, n, p), |k| (n - k) / (k + 1.0)).unwrap();
2396        assert!((470_000.0..=490_000.0).contains(&first) && pmf.len() < 40_000, "{first} + {}", pmf.len());
2397        // The anchor BINOM.DIST(500000, 1e6, 0.5) carries that function's error of about n · ε
2398        // (its logarithms are near 7e5), which scales the whole table; draws divide by the total.
2399        assert!((pmf.iter().sum::<f64>() - 1.0).abs() < 1e-8);
2400        // A bad anchor or mode is refused rather than tabulated.
2401        assert!(pmf_table(10.0, 5.0, f64::NAN, |_| 1.0).is_err());
2402        assert!(pmf_table(10.0, 5.0, 0.0, |_| 1.0).is_err());
2403        assert!(pmf_table(10.0, 11.0, 0.5, |_| 1.0).is_err());
2404    }
2405
2406    #[test]
2407    #[allow(clippy::disallowed_methods)] // a native-only timing test: the clock is read off wasm
2408    fn binomial_and_poisson_draws_at_the_limits_are_tabulated_quickly() {
2409        let mut s = s();
2410        // The tables cover the outcomes around the mode, not every one of the 1,000,001 for a
2411        // million trials, so the largest inputs cost milliseconds. Binomial(1e6, 0.5) has mean
2412        // 500000 and standard deviation 500: 1000 draws put the sample mean within 500/√1000 ≈ 16
2413        // of it, so 100 is six standard errors. Poisson(1e5) has mean 1e5 and standard deviation
2414        // 316: 1000 draws put the mean within 10, so 60 is six standard errors. With p = 1 every
2415        // draw is exactly 1e6, with p = 0 exactly 0.
2416        let mut draw = |sheet: &str, dist: Json| {
2417            let mut dist = dist;
2418            dist["tool"] = json!("randomNumberGeneration");
2419            dist["count"] = json!(1000);
2420            dist["seed"] = json!(3);
2421            dist["newSheet"] = json!(sheet);
2422            s.execute("data.analysisToolPak", dist).unwrap();
2423        };
2424        let start = std::time::Instant::now();
2425        draw("Half", json!({"distribution": "binomial", "p": 0.5, "trials": 1_000_000}));
2426        draw("Sure", json!({"distribution": "binomial", "p": 1, "trials": 1_000_000}));
2427        draw("Never", json!({"distribution": "binomial", "p": 0, "trials": 1_000_000}));
2428        draw("Pois", json!({"distribution": "poisson", "lambda": 100_000}));
2429        let elapsed = start.elapsed();
2430        let (m, _) = mean_var(&numbers_of(&s, "Half", "A", 1000));
2431        assert!((m - 500_000.0).abs() < 100.0, "{m}");
2432        assert!(numbers_of(&s, "Sure", "A", 1000).iter().all(|v| *v == 1_000_000.0));
2433        assert!(numbers_of(&s, "Never", "A", 1000).iter().all(|v| *v == 0.0));
2434        let (m, _) = mean_var(&numbers_of(&s, "Pois", "A", 1000));
2435        assert!((m - 100_000.0).abs() < 60.0, "{m}");
2436        assert!(elapsed < std::time::Duration::from_secs(2), "{elapsed:?}");
2437    }
2438
2439    #[test]
2440    fn sampling_periodic_and_random() {
2441        let mut s = s();
2442        let values: Vec<Json> = (1..=10).map(|k| json!([k])).collect();
2443        s.execute("range.setValues", json!({"range": "A1", "values": values})).unwrap();
2444        s.execute("data.analysisToolPak", json!({"tool": "Sampling", "range": "A1:A10", "period": 3, "output": "C1"})).unwrap();
2445        assert_eq!(numbers_of(&s, "Sheet1", "C", 3), [3.0, 6.0, 9.0]);
2446        assert!(v(&s, "C4").is_empty());
2447        let r = s
2448            .execute(
2449                "data.analysisToolPak",
2450                json!({"tool": "sampling", "range": "A1:A10", "method": "random", "samples": 20, "seed": 5, "output": "D1"}),
2451            )
2452            .unwrap();
2453        assert_eq!(r["seed"], 5);
2454        s.execute(
2455            "data.analysisToolPak",
2456            json!({"tool": "sampling", "range": "A1:A10", "method": "random", "samples": 20, "seed": 5, "output": "E1"}),
2457        )
2458        .unwrap();
2459        let (a, b) = (numbers_of(&s, "Sheet1", "D", 20), numbers_of(&s, "Sheet1", "E", 20));
2460        assert_eq!(a, b, "the same seed repeats the sample");
2461        assert!(a.iter().all(|x| x.fract() == 0.0 && (1.0..=10.0).contains(x)));
2462        for p in [
2463            json!({"range": "A1:A10", "period": 11}),
2464            json!({"range": "A1:A10", "period": 0}),
2465            json!({"range": "A1:A10"}),
2466            json!({"range": "A1:A10", "method": "random", "samples": 2_000_000}),
2467            json!({"range": "B1:B10", "period": 1}),
2468        ] {
2469            let mut q = p.clone();
2470            q["tool"] = json!("sampling");
2471            q["output"] = json!("G1");
2472            assert!(s.execute("data.analysisToolPak", q).is_err(), "{p}");
2473        }
2474    }
2475
2476    #[test]
2477    fn options_typed_as_text_that_is_not_a_number_are_refused() {
2478        // A form sends its options as text; one that doesn't read as a number (a decimal comma,
2479        // a typo) is an error, not the default, while a blank option takes the default.
2480        let mut s = s();
2481        s.execute("range.setValues", json!({"range": "A1", "values": [[1, 2], [2, 4], [3, 6]]})).unwrap();
2482        let rng = |dist: &str| json!({"tool": "randomNumberGeneration", "count": 3, "distribution": dist, "seed": 1, "outputTo": "newSheet"});
2483        let z = json!({"tool": "zTest", "range1": "Sheet1!A1:A3", "range2": "Sheet1!B1:B3", "variance1": 1, "variance2": 4, "outputTo": "newSheet"});
2484        let with = |base: &Json, key: &str, val: &str| {
2485            let mut p = base.clone();
2486            p[key] = json!(val);
2487            p
2488        };
2489        let cases = [
2490            (rng("uniform"), "from"),
2491            (rng("uniform"), "to"),
2492            (rng("normal"), "mean"),
2493            (rng("normal"), "standardDeviation"),
2494            (rng("bernoulli"), "p"),
2495            (rng("binomial"), "p"),
2496            (rng("poisson"), "lambda"),
2497            (rng("patterned"), "step"),
2498            (rng("uniform"), "variables"),
2499            (z.clone(), "hypothesizedDifference"),
2500        ];
2501        for (base, key) in cases {
2502            let e = s.execute("data.analysisToolPak", with(&base, key, "0,3")).unwrap_err().to_string();
2503            assert!(e.contains(key), "{key}: {e}");
2504            s.execute("data.analysisToolPak", with(&base, key, " ")).unwrap_or_else(|e| panic!("{key} blank: {e}"));
2505        }
2506    }
2507
2508    #[test]
2509    fn an_alpha_that_is_not_a_number_is_refused() {
2510        // A decimal comma or a typo in Alpha must not run the test at 0.05 without a word.
2511        let mut s = s();
2512        s.execute("range.setValues", json!({"range": "A1", "values": [["", "c1", "c2"], ["r1", 1, 2], ["r1", 2, 4], ["r2", 3, 6], ["r2", 5, 9]]}))
2513            .unwrap();
2514        for base in [
2515            json!({"tool": "zTest", "range1": "Sheet1!B2:B5", "range2": "Sheet1!C2:C5", "variance1": 1, "variance2": 4}),
2516            json!({"tool": "anovaTwoFactorWithout", "range": "Sheet1!B2:C5"}),
2517            json!({"tool": "anovaTwoFactorWith", "range": "Sheet1!A1:C5", "rowsPerSample": 2}),
2518            json!({"tool": "tTestEqual", "range1": "Sheet1!B2:B5", "range2": "Sheet1!C2:C5"}),
2519        ] {
2520            let mut p = base.clone();
2521            p["alpha"] = json!("0,01");
2522            let e = s.execute("data.analysisToolPak", p).unwrap_err().to_string();
2523            assert!(e.contains("alpha"), "{base}: {e}");
2524            let mut p = base.clone();
2525            p["alpha"] = json!("");
2526            s.execute("data.analysisToolPak", p).unwrap_or_else(|e| panic!("{base} blank alpha: {e}"));
2527        }
2528    }
2529
2530    #[test]
2531    fn a_patterned_sequence_ignores_the_seed() {
2532        // Patterned generation draws nothing, so a seed left in the form (even one that isn't a
2533        // number) neither blocks the run nor shows up in the result.
2534        let mut s = s();
2535        for seed in [json!("abc"), json!(7)] {
2536            let r = s
2537                .execute(
2538                    "data.analysisToolPak",
2539                    json!({"tool": "randomNumberGeneration", "distribution": "patterned", "from": 1, "to": 3, "seed": seed, "outputTo": "newSheet"}),
2540                )
2541                .unwrap_or_else(|e| panic!("seed {seed}: {e}"));
2542            assert!(r.get("seed").is_none(), "seed {seed}: {r}");
2543        }
2544        // A random distribution still refuses the bad seed.
2545        let bad = json!({"tool": "randomNumberGeneration", "distribution": "uniform", "count": 2, "seed": "abc", "outputTo": "newSheet"});
2546        assert!(s.execute("data.analysisToolPak", bad).is_err());
2547    }
2548
2549    #[test]
2550    fn hostile_inputs_are_refused_without_panicking() {
2551        let mut s = s();
2552        let r = s.execute("data.analysisToolPak", json!({})).unwrap();
2553        assert!(r["tools"].as_array().is_some_and(|t| t.len() == super::TOOLS.len()));
2554        assert!(s.take_ui_requests().iter().any(|q| matches!(q, crate::engine::UiRequest::Dialog(n, _) if n == "analysisToolPak")));
2555        s.execute("range.setValues", json!({"range": "A1", "values": [[1, "x"], [2, 3]]})).unwrap();
2556        for p in [
2557            json!({"tool": "nope"}),
2558            json!({"tool": "descriptive"}),
2559            json!({"tool": "descriptive", "range": "A1:XFD1048576"}),
2560            json!({"tool": "descriptive", "range": "A1:B2"}),
2561            json!({"tool": "descriptive", "range": "A1:A2", "output": "XFD1048576"}),
2562            json!({"tool": "descriptive", "range": "A1:A2", "newSheet": "bad[name]"}),
2563            json!({"tool": "regression", "inputY": "A1:A2", "inputX": "A1:A2"}),
2564            json!({"tool": "regression", "inputY": "A1:A2", "inputX": "A1:Q2"}),
2565            json!({"tool": "movingAverage", "range": "A1:A2", "interval": 1e300}),
2566            json!({"tool": "movingAverage", "range": "A1:A2", "interval": -3}),
2567            json!({"tool": "exponentialSmoothing", "range": "A1:A2", "damping": 7}),
2568            json!({"tool": "histogram", "range": "C1:C3"}),
2569            json!({"tool": "tTestPaired", "range1": "A1", "range2": "A2"}),
2570            json!({"tool": "anovaSingle", "range": "A1:A2"}),
2571            json!({"tool": "correlation", "range": "A1:B2", "alpha": "x", "groupedBy": 5}),
2572            json!({"tool": "descriptive", "range": "A1:A2", "confidence": 250}),
2573            json!({"tool": "anovaTwoFactorWithout", "range": "A1:B2"}),
2574            json!({"tool": "anovaTwoFactorWithout", "range": "A1:A2"}),
2575            json!({"tool": "anovaTwoFactorWith", "range": "A1:B2", "rowsPerSample": 2}),
2576            json!({"tool": "anovaTwoFactorWith", "range": "A1:B2", "rowsPerSample": 1e300}),
2577            json!({"tool": "zTest", "range1": "A1", "range2": "A2", "variance1": "x", "variance2": -1}),
2578        ] {
2579            assert!(s.execute("data.analysisToolPak", p.clone()).is_err(), "{p}");
2580        }
2581        // Collinear X columns are an error, not a NaN report.
2582        s.execute("range.setValues", json!({"range": "D1", "values": [[1, 1, 2], [2, 2, 4], [3, 3, 6], [5, 4, 8]]})).unwrap();
2583        assert!(s.execute("data.analysisToolPak", json!({"tool": "regression", "inputY": "D1:D4", "inputX": "E1:F4"})).is_err());
2584        // Constant data gives #DIV/0! cells, not a crash.
2585        s.execute("range.setValues", json!({"range": "H1", "values": [[4], [4], [4]]})).unwrap();
2586        s.execute("data.analysisToolPak", json!({"tool": "descriptive", "range": "H1:H3", "output": "J1"})).unwrap();
2587        assert_eq!(v(&s, "K9"), Value::Error(crate::base::CellError::Div0));
2588    }
2589}