1use 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";
20const MAX_INPUT_CELLS: u64 = 1_000_000;
22const MAX_OUTPUT_CELLS: u64 = 4_000_000;
24const MAX_MATRIX_VARS: usize = 256;
26const MAX_REGRESSORS: usize = 16;
28const MAX_BINS: usize = 100_000;
30const MAX_FOURIER: usize = 4096;
32const MAX_RANDOM: usize = 1_000_000;
34const MAX_OUTCOMES: f64 = 1_000_000.0;
36const MAX_SEED: u64 = (1 << 53) - 1;
38
39pub const OVERWRITE_ERROR: &str = "already holds data";
42
43pub 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
78fn 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
106fn 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
112fn 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
131struct 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
142fn 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
152fn 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
175fn 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
193fn 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
204fn mean(x: &[f64]) -> f64 {
207 if x.is_empty() { f64::NAN } else { x.iter().sum::<f64>() / x.len() as f64 }
208}
209
210fn ss(x: &[f64]) -> f64 {
212 let m = mean(x);
213 x.iter().map(|v| (v - m) * (v - m)).sum()
214}
215
216fn 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
225fn 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
234struct 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
249fn 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#[derive(Default)]
271struct Grid {
272 cells: BTreeMap<(u32, u32), Value>,
273 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 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 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
309fn 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
384fn 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
397fn 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
409fn 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 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
496fn 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
520fn 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
535fn 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 coef: Vec<f64>,
581 se: Vec<f64>,
583 predicted: Vec<f64>,
584 sse: f64,
585 ssr: f64,
586 dfr: f64,
587 dfe: f64,
588 dft: f64,
590}
591
592fn 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 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
779fn 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
790fn 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 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 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 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
861fn 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 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
898fn 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
911fn 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
919fn 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 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 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
1067fn 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
1076fn 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
1091fn 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
1097fn column(data: &[Vec<f64>], j: usize) -> Vec<f64> {
1099 data.iter().filter_map(|r| r.get(j).copied()).collect()
1100}
1101
1102fn 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 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
1165fn 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
1174fn 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 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
1261fn 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
1269fn 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 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
1307fn 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 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
1351fn 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 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
1399struct 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 fn uniform(&mut self) -> f64 {
1429 (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
1430 }
1431
1432 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 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
1453fn 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
1470fn 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
1480fn num_or(p: &Json, key: &str, default: f64) -> Result<f64> {
1482 Ok(opt_num(p, key)?.unwrap_or(default))
1483}
1484
1485fn 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
1494fn 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
1502fn 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
1541struct Table {
1544 first: f64,
1545 cdf: Vec<f64>,
1546}
1547
1548impl Table {
1549 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 fn point(value: f64) -> Table {
1565 Table { first: value, cdf: vec![1.0] }
1566 }
1567
1568 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
1575fn 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
1592fn 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 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 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
1698fn 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
1707fn 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
1745fn 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert!((pmf.iter().sum::<f64>() - 1.0).abs() < 1e-8);
2400 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)] fn binomial_and_poisson_draws_at_the_limits_are_tabulated_quickly() {
2409 let mut s = s();
2410 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 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 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 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 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 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 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}