Skip to main content

ravenrs_sheet/functions/stat/
descriptive.rs

1use super::{Arg, C, CellError, R, Value, arg, array_numbers, array_val, as_array, has, num, num_val, opt_num};
2
3// ---------------------------------------------------------------------------------------------
4// Basic helpers over number lists
5// ---------------------------------------------------------------------------------------------
6
7pub(super) fn mean(x: &[f64]) -> R<f64> {
8    if x.is_empty() {
9        return Err(CellError::Div0);
10    }
11    Ok(x.iter().sum::<f64>() / x.len() as f64)
12}
13
14pub(super) fn devsq(x: &[f64]) -> f64 {
15    if x.is_empty() {
16        return 0.0;
17    }
18    let m = x.iter().sum::<f64>() / x.len() as f64;
19    x.iter().map(|v| (v - m) * (v - m)).sum()
20}
21
22pub(super) fn var_s(x: &[f64]) -> R<f64> {
23    if x.len() < 2 {
24        return Err(CellError::Div0);
25    }
26    Ok(devsq(x) / (x.len() - 1) as f64)
27}
28
29pub(super) fn var_p(x: &[f64]) -> R<f64> {
30    if x.is_empty() {
31        return Err(CellError::Div0);
32    }
33    Ok(devsq(x) / x.len() as f64)
34}
35
36pub(super) fn sorted(mut v: Vec<f64>) -> Vec<f64> {
37    v.sort_by(f64::total_cmp);
38    v
39}
40
41/// Sorts `v` if it is already monotone in `total_cmp` order (a descending slice is reversed) and
42/// says whether it did. At most two passes, each stopping at the first out-of-order pair, so
43/// unordered data costs next to nothing. Needed because the std sort recognises ascending and
44/// descending runs in O(n) with a small constant; selection is O(n) too but measured 1.2-1.8x
45/// slower on such input.
46fn sorted_if_monotone(v: &mut [f64]) -> bool {
47    if v.is_sorted_by(|a, b| a.total_cmp(b).is_le()) {
48        return true;
49    }
50    if v.is_sorted_by(|a, b| a.total_cmp(b).is_ge()) {
51        v.reverse();
52        return true;
53    }
54    false
55}
56
57pub(super) fn median(v: Vec<f64>) -> R<f64> {
58    if v.is_empty() {
59        return Err(CellError::Num);
60    }
61    let mut v = v;
62    let n = v.len();
63    if sorted_if_monotone(&mut v) {
64        let hi = v.get(n / 2).copied().unwrap_or(0.0);
65        return if n % 2 == 1 { Ok(hi) } else { Ok(v.get(n / 2 - 1).copied().unwrap_or(0.0).midpoint(hi)) };
66    }
67    let (lower, hi, _) = v.select_nth_unstable_by(n / 2, f64::total_cmp);
68    let hi = *hi;
69    if n % 2 == 1 { Ok(hi) } else { Ok(lower.iter().copied().max_by(f64::total_cmp).unwrap_or(0.0).midpoint(hi)) }
70}
71
72/// Values occurring at least twice, ordered by first occurrence, with their (equal, maximal) count.
73fn modes(v: &[f64]) -> Vec<f64> {
74    // Index of each distinct number in `counts`, so counting is linear rather than a search of
75    // every distinct number so far. Keyed by bits with -0 folded into 0 (they compare equal); NaN
76    // never equals anything, so it is never merged.
77    let mut index: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
78    let mut counts: Vec<(f64, usize)> = Vec::new();
79    for &x in v {
80        if x.is_nan() {
81            counts.push((x, 1));
82            continue;
83        }
84        let key = if x == 0.0 { 0 } else { x.to_bits() };
85        match index.entry(key) {
86            std::collections::hash_map::Entry::Occupied(e) => {
87                if let Some(c) = counts.get_mut(*e.get()) {
88                    c.1 += 1;
89                }
90            }
91            std::collections::hash_map::Entry::Vacant(e) => {
92                e.insert(counts.len());
93                counts.push((x, 1));
94            }
95        }
96    }
97    let best = counts.iter().map(|e| e.1).max().unwrap_or(0);
98    if best < 2 {
99        return Vec::new();
100    }
101    counts.into_iter().filter(|e| e.1 == best).map(|e| e.0).collect()
102}
103
104pub(super) fn mode_sngl(v: &[f64]) -> R<f64> {
105    modes(v).first().copied().ok_or(CellError::NA)
106}
107
108/// Numbers for MODE / MODE.SNGL / MODE.MULT. Inside multi-cell ranges and arrays only numbers
109/// count, like [`numbers`]. Scalar arguments differ from SUM: in Excel 365, text and logical
110/// values are skipped whether typed or referenced (even numeric text such as `"1"`, so
111/// `MODE(1,"1",2,3)` is `#N/A`, and `MODE(1,TRUE,TRUE,2)` is `#N/A`), but a single-cell reference
112/// to a blank cell is `#VALUE!` (`MODE(B9,2,D7)` with D7 blank). Arguments are processed left to
113/// right, so the first error wins. The results cached in POI's FormulaEvalTestData come from an
114/// older Excel that answered `#VALUE!` for typed text; they are not the reference here.
115pub(super) fn mode_numbers(args: &[Arg]) -> R<Vec<f64>> {
116    let mut out = Vec::new();
117    for a in args {
118        match &a.value {
119            Value::Array(arr) => {
120                for v in arr.iter() {
121                    match v {
122                        Value::Number(n) => out.push(*n),
123                        Value::Error(e) => return Err(*e),
124                        _ => {}
125                    }
126                }
127            }
128            Value::Number(n) => out.push(*n),
129            Value::Error(e) => return Err(*e),
130            Value::Text(_) | Value::Bool(_) => {}
131            Value::Empty => return Err(CellError::Value),
132        }
133    }
134    Ok(out)
135}
136
137fn product(v: &[f64]) -> f64 {
138    if v.is_empty() { 0.0 } else { v.iter().product() }
139}
140
141pub(super) fn min_of(v: &[f64]) -> f64 {
142    if v.is_empty() { 0.0 } else { v.iter().copied().fold(f64::INFINITY, f64::min) }
143}
144
145pub(super) fn max_of(v: &[f64]) -> f64 {
146    if v.is_empty() { 0.0 } else { v.iter().copied().fold(f64::NEG_INFINITY, f64::max) }
147}
148
149/// k-th largest (k is rounded up like Excel).
150pub(super) fn kth(v: Vec<f64>, k: f64, largest: bool) -> R<f64> {
151    let k = k.ceil();
152    if k.is_nan() || k < 1.0 || k > v.len() as f64 {
153        return Err(CellError::Num);
154    }
155    let mut v = v;
156    let k = k as usize;
157    let idx = if largest { v.len() - k } else { k - 1 };
158    // Selection instead of a full sort (monotone input is read directly); same total order, so the
159    // same bit pattern as `sorted(v)[idx]`.
160    if sorted_if_monotone(&mut v) {
161        return v.get(idx).copied().ok_or(CellError::Num);
162    }
163    Ok(*v.select_nth_unstable_by(idx, f64::total_cmp).1)
164}
165
166/// Value at fractional rank `pos` of the sorted data, linearly interpolated, found by selection.
167fn interp(mut v: Vec<f64>, pos: f64) -> R<f64> {
168    let i = pos.floor();
169    let frac = pos - i;
170    let i = i as usize;
171    if i >= v.len() {
172        return Err(CellError::Num);
173    }
174    if sorted_if_monotone(&mut v) {
175        let lo = v.get(i).copied().ok_or(CellError::Num)?;
176        return match v.get(i + 1) {
177            Some(hi) if frac > 0.0 => Ok(lo + frac * (hi - lo)),
178            _ => Ok(lo),
179        };
180    }
181    let (_, lo, upper) = v.select_nth_unstable_by(i, f64::total_cmp);
182    let lo = *lo;
183    // The next order statistic is the minimum of the upper partition.
184    if frac > 0.0
185        && let Some(hi) = upper.iter().copied().min_by(f64::total_cmp)
186    {
187        return Ok(lo + frac * (hi - lo));
188    }
189    Ok(lo)
190}
191
192pub(super) fn percentile_inc(v: Vec<f64>, k: f64) -> R<f64> {
193    if v.is_empty() || !(0.0..=1.0).contains(&k) {
194        return Err(CellError::Num);
195    }
196    let pos = k * (v.len() - 1) as f64;
197    interp(v, pos)
198}
199
200pub(super) fn percentile_exc(v: Vec<f64>, k: f64) -> R<f64> {
201    let n = v.len() as f64;
202    if v.is_empty() || !(k > 0.0 && k < 1.0) {
203        return Err(CellError::Num);
204    }
205    let pos = k * (n + 1.0);
206    if pos < 1.0 || pos > n {
207        return Err(CellError::Num);
208    }
209    interp(v, pos - 1.0)
210}
211
212pub(super) fn quartile_inc(v: Vec<f64>, q: f64) -> R<f64> {
213    let q = q.trunc();
214    if !(0.0..=4.0).contains(&q) {
215        return Err(CellError::Num);
216    }
217    percentile_inc(v, q / 4.0)
218}
219
220pub(super) fn quartile_exc(v: Vec<f64>, q: f64) -> R<f64> {
221    let q = q.trunc();
222    if !(1.0..=3.0).contains(&q) {
223        return Err(CellError::Num);
224    }
225    percentile_exc(v, q / 4.0)
226}
227
228pub(super) fn arg_nums(a: &[Arg], i: usize) -> R<Vec<f64>> {
229    array_numbers(&arg(a, i)?.value)
230}
231
232/// Pairs of numbers at matching positions of two arrays (pairs with a non-number skipped).
233/// Different sizes give `#N/A`; errors propagate.
234fn pairs(x: &Value, y: &Value) -> R<(Vec<f64>, Vec<f64>)> {
235    let xa = as_array(x);
236    let ya = as_array(y);
237    if xa.data.len() != ya.data.len() {
238        return Err(CellError::NA);
239    }
240    let mut xs = Vec::new();
241    let mut ys = Vec::new();
242    for (a, b) in xa.data.iter().zip(ya.data.iter()) {
243        if let Value::Error(e) = a {
244            return Err(*e);
245        }
246        if let Value::Error(e) = b {
247            return Err(*e);
248        }
249        if let (Value::Number(p), Value::Number(q)) = (a, b) {
250            xs.push(*p);
251            ys.push(*q);
252        }
253    }
254    Ok((xs, ys))
255}
256
257pub(super) fn arg_pairs(a: &[Arg], i: usize, j: usize) -> R<(Vec<f64>, Vec<f64>)> {
258    pairs(&arg(a, i)?.value, &arg(a, j)?.value)
259}
260
261/// (sum of cross deviations, devsq x, devsq y, mean x, mean y).
262pub(super) fn moments(x: &[f64], y: &[f64]) -> R<(f64, f64, f64, f64, f64)> {
263    let mx = mean(x)?;
264    let my = mean(y)?;
265    let mut sxy = 0.0;
266    let mut sxx = 0.0;
267    let mut syy = 0.0;
268    for (a, b) in x.iter().zip(y) {
269        sxy += (a - mx) * (b - my);
270        sxx += (a - mx) * (a - mx);
271        syy += (b - my) * (b - my);
272    }
273    Ok((sxy, sxx, syy, mx, my))
274}
275
276pub(super) fn correl(x: &[f64], y: &[f64]) -> R<f64> {
277    if x.len() < 2 {
278        return Err(CellError::Div0);
279    }
280    let (sxy, sxx, syy, _, _) = moments(x, y)?;
281    if sxx == 0.0 || syy == 0.0 {
282        return Err(CellError::Div0);
283    }
284    Ok(sxy / (sxx * syy).sqrt())
285}
286
287pub(super) fn slope(y: &[f64], x: &[f64]) -> R<f64> {
288    let (sxy, sxx, _, _, _) = moments(x, y)?;
289    if sxx == 0.0 {
290        return Err(CellError::Div0);
291    }
292    Ok(sxy / sxx)
293}
294
295pub(super) fn intercept(y: &[f64], x: &[f64]) -> R<f64> {
296    let (sxy, sxx, _, mx, my) = moments(x, y)?;
297    if sxx == 0.0 {
298        return Err(CellError::Div0);
299    }
300    Ok(my - sxy / sxx * mx)
301}
302
303fn trunc_digits(r: f64, sig: f64) -> f64 {
304    let f = 10f64.powi(sig as i32);
305    if !f.is_finite() {
306        return r;
307    }
308    ((r * f) + 1e-9).floor() / f
309}
310
311fn col_value(v: Vec<f64>) -> R<Value> {
312    let n = v.len();
313    array_val(n, 1, v.into_iter().map(Value::Number).collect())
314}
315
316// ---------------------------------------------------------------------------------------------
317// Aggregates
318// ---------------------------------------------------------------------------------------------
319
320pub(super) fn count_impl(a: &[Arg]) -> f64 {
321    let mut c = 0usize;
322    for x in a {
323        match &x.value {
324            Value::Array(arr) => c += arr.iter().filter(|v| v.is_number()).count(),
325            Value::Number(_) => c += 1,
326            _ if x.from_ref => {}
327            Value::Bool(_) => c += 1,
328            Value::Text(t) if crate::base::parse::parse_number_text(t).is_some() => c += 1,
329            _ => {}
330        }
331    }
332    c as f64
333}
334
335pub(super) fn counta_impl(a: &[Arg]) -> f64 {
336    let mut c = 0usize;
337    for x in a {
338        match &x.value {
339            Value::Array(arr) => c += arr.iter().filter(|v| !v.is_empty()).count(),
340            Value::Empty if x.from_ref => {}
341            _ => c += 1,
342        }
343    }
344    c as f64
345}
346
347pub(super) fn countblank(a: &[Arg], _: C) -> R<Value> {
348    let v = &arg(a, 0)?.value;
349    let blank = |x: &Value| matches!(x, Value::Empty) || matches!(x, Value::Text(t) if t.is_empty());
350    let c = match v {
351        Value::Array(arr) => arr.iter().filter(|x| blank(x)).count(),
352        x => usize::from(blank(x)),
353    };
354    num_val(c as f64)
355}
356
357pub(super) fn mode_mult(a: &[Arg], _: C) -> R<Value> {
358    let v = mode_numbers(a)?;
359    let m = modes(&v);
360    if m.is_empty() {
361        return Err(CellError::NA);
362    }
363    col_value(m)
364}
365
366pub(super) fn large(a: &[Arg], _: C) -> R<Value> {
367    num_val(kth(arg_nums(a, 0)?, num(a, 1)?, true)?)
368}
369
370pub(super) fn small(a: &[Arg], _: C) -> R<Value> {
371    num_val(kth(arg_nums(a, 0)?, num(a, 1)?, false)?)
372}
373
374pub(super) fn rank_impl(a: &[Arg], avg: bool) -> R<Value> {
375    let x = num(a, 0)?;
376    let v = arg_nums(a, 1)?;
377    let asc = opt_num(a, 2, 0.0)? != 0.0;
378    let eq = v.iter().filter(|&&y| y == x).count();
379    if eq == 0 {
380        return Err(CellError::NA);
381    }
382    let before = v.iter().filter(|&&y| if asc { y < x } else { y > x }).count();
383    let r = before as f64 + 1.0;
384    num_val(if avg { r + (eq as f64 - 1.0) / 2.0 } else { r })
385}
386
387pub(super) fn percentrank_impl(a: &[Arg], exc: bool) -> R<Value> {
388    let s = sorted(arg_nums(a, 0)?);
389    let x = num(a, 1)?;
390    let sig = opt_num(a, 2, 3.0)?.trunc();
391    if sig < 1.0 || s.is_empty() {
392        return Err(CellError::Num);
393    }
394    // Excel: 10^significance overflows from 309 up.
395    if sig >= 309.0 {
396        return Err(CellError::Div0);
397    }
398    let (first, last) = (s.first().copied().unwrap_or(0.0), s.last().copied().unwrap_or(0.0));
399    if x < first || x > last {
400        return Err(CellError::NA);
401    }
402    let n = s.len() as f64;
403    let rank_of = |v: f64| -> f64 {
404        let less = s.iter().filter(|&&y| y < v).count() as f64;
405        if exc {
406            (less + 1.0) / (n + 1.0)
407        } else if n <= 1.0 {
408            1.0
409        } else {
410            less / (n - 1.0)
411        }
412    };
413    let r = if s.contains(&x) {
414        rank_of(x)
415    } else {
416        let lo = s.iter().copied().filter(|&y| y < x).fold(f64::NEG_INFINITY, f64::max);
417        let hi = s.iter().copied().filter(|&y| y > x).fold(f64::INFINITY, f64::min);
418        let (rl, rh) = (rank_of(lo), rank_of(hi));
419        rl + (x - lo) / (hi - lo) * (rh - rl)
420    };
421    // From significance 15 up Excel returns 15 significant digits, rounded.
422    if sig >= 15.0 {
423        return num_val(format!("{r:.14e}").parse().unwrap_or(r));
424    }
425    num_val(trunc_digits(r, sig))
426}
427
428pub(super) fn trimmean(a: &[Arg], _: C) -> R<Value> {
429    let v = arg_nums(a, 0)?;
430    let p = num(a, 1)?;
431    if !(0.0..1.0).contains(&p) || v.is_empty() {
432        return Err(CellError::Num);
433    }
434    let n = v.len();
435    let per_side = ((n as f64 * p) / 2.0).floor() as usize;
436    let s = sorted(v);
437    let kept = s.get(per_side..n - per_side).unwrap_or(&[]);
438    num_val(mean(kept)?)
439}
440
441pub(super) fn kurt(v: &[f64]) -> R<f64> {
442    let n = v.len() as f64;
443    if v.len() < 4 {
444        return Err(CellError::Div0);
445    }
446    let m = mean(v)?;
447    let s = var_s(v)?.sqrt();
448    if s == 0.0 {
449        return Err(CellError::Div0);
450    }
451    let sum4: f64 = v.iter().map(|x| ((x - m) / s).powi(4)).sum();
452    Ok(n * (n + 1.0) / ((n - 1.0) * (n - 2.0) * (n - 3.0)) * sum4 - 3.0 * (n - 1.0).powi(2) / ((n - 2.0) * (n - 3.0)))
453}
454
455pub(super) fn skew(v: &[f64]) -> R<f64> {
456    let n = v.len() as f64;
457    if v.len() < 3 {
458        return Err(CellError::Div0);
459    }
460    let m = mean(v)?;
461    let s = var_s(v)?.sqrt();
462    if s == 0.0 {
463        return Err(CellError::Div0);
464    }
465    let sum3: f64 = v.iter().map(|x| ((x - m) / s).powi(3)).sum();
466    Ok(n / ((n - 1.0) * (n - 2.0)) * sum3)
467}
468
469pub(super) fn skew_p(v: &[f64]) -> R<f64> {
470    let m = mean(v)?;
471    let s = var_p(v)?.sqrt();
472    if s == 0.0 {
473        return Err(CellError::Div0);
474    }
475    Ok(v.iter().map(|x| ((x - m) / s).powi(3)).sum::<f64>() / v.len() as f64)
476}
477
478pub(super) fn geomean(v: &[f64]) -> R<f64> {
479    if v.is_empty() || v.iter().any(|&x| x <= 0.0) {
480        return Err(CellError::Num);
481    }
482    Ok((v.iter().map(|x| x.ln()).sum::<f64>() / v.len() as f64).exp())
483}
484
485pub(super) fn harmean(v: &[f64]) -> R<f64> {
486    if v.is_empty() {
487        return Err(CellError::NA);
488    }
489    if v.iter().any(|&x| x <= 0.0) {
490        return Err(CellError::Num);
491    }
492    Ok(v.len() as f64 / v.iter().map(|x| 1.0 / x).sum::<f64>())
493}
494
495pub(super) fn avedev(v: &[f64]) -> R<f64> {
496    if v.is_empty() {
497        return Err(CellError::Num);
498    }
499    let m = mean(v)?;
500    Ok(v.iter().map(|x| (x - m).abs()).sum::<f64>() / v.len() as f64)
501}
502
503pub(super) fn frequency(a: &[Arg], _: C) -> R<Value> {
504    let data = arg_nums(a, 0)?;
505    let bins = arg_nums(a, 1)?;
506    if bins.is_empty() {
507        return col_value(vec![data.len() as f64]);
508    }
509    // a NaN bin equals no value (and would break the ascending order searched below)
510    let mut order: Vec<usize> = (0..bins.len()).filter(|&i| bins.get(i).is_some_and(|b| !b.is_nan())).collect();
511    order.sort_by(|&i, &j| bins.get(i).copied().unwrap_or(0.0).total_cmp(&bins.get(j).copied().unwrap_or(0.0)));
512    let mut counts = vec![0.0; bins.len() + 1];
513    for x in data {
514        // `order` sorts the bins ascending, so the bins `x` exceeds come first: binary search for
515        // the first bin at or above `x` (the first of equal bins, as a linear scan would find).
516        let pos = if x.is_nan() { order.len() } else { order.partition_point(|&i| bins.get(i).copied().unwrap_or(f64::INFINITY) < x) };
517        let slot = order.get(pos).copied().unwrap_or(bins.len());
518        if let Some(c) = counts.get_mut(slot) {
519            *c += 1.0;
520        }
521    }
522    col_value(counts)
523}
524
525pub(super) fn prob(a: &[Arg], _: C) -> R<Value> {
526    let (xs, ps) = arg_pairs(a, 0, 1)?;
527    let lo = num(a, 2)?;
528    let hi = if has(a, 3) { num(a, 3)? } else { lo };
529    if ps.iter().any(|&p| p <= 0.0 || p > 1.0) {
530        return Err(CellError::Num);
531    }
532    let total: f64 = ps.iter().sum();
533    if (total - 1.0).abs() > 1e-7 {
534        return Err(CellError::Num);
535    }
536    num_val(xs.iter().zip(&ps).filter(|(x, _)| **x >= lo && **x <= hi).map(|(_, p)| p).sum())
537}
538
539pub(super) fn standardize(a: &[Arg], _: C) -> R<Value> {
540    let (x, m, sd) = (num(a, 0)?, num(a, 1)?, num(a, 2)?);
541    if sd <= 0.0 {
542        return Err(CellError::Num);
543    }
544    num_val((x - m) / sd)
545}
546
547// ---------------------------------------------------------------------------------------------
548// SUBTOTAL / AGGREGATE support
549// ---------------------------------------------------------------------------------------------
550
551/// Evaluates SUBTOTAL/AGGREGATE function `function_num` over already-filtered cell values.
552///
553/// Codes 1–11 (and 101–111) are AVERAGE, COUNT, COUNTA, MAX, MIN, PRODUCT, STDEV.S, STDEV.P,
554/// SUM, VAR.S, VAR.P; 12 = MEDIAN, 13 = MODE.SNGL. Codes 14–19 need `k`: use
555/// [`aggregate_values_k`] (they give `#VALUE!` here). Values that are not numbers are ignored
556/// (COUNTA counts non-empty values); the first error value present is returned.
557pub fn aggregate_values(function_num: u32, values: &[Value]) -> Value {
558    crate::functions::util::finish(aggregate_impl(function_num, values, None))
559}
560
561/// AGGREGATE codes 14 = LARGE, 15 = SMALL, 16 = PERCENTILE.INC, 17 = QUARTILE.INC,
562/// 18 = PERCENTILE.EXC, 19 = QUARTILE.EXC with their `k` argument; codes 1–13 delegate to
563/// [`aggregate_values`].
564pub fn aggregate_values_k(function_num: u32, values: &[Value], k: &Value) -> Value {
565    crate::functions::util::finish(aggregate_impl(function_num, values, Some(k)))
566}
567
568fn aggregate_impl(function_num: u32, values: &[Value], k: Option<&Value>) -> R<Value> {
569    let code = if (101..=111).contains(&function_num) { function_num - 100 } else { function_num };
570    let mut xs = Vec::new();
571    let mut nonempty = 0usize;
572    let mut push = |v: &Value| -> R<()> {
573        match v {
574            Value::Number(n) => xs.push(*n),
575            Value::Error(e) => return Err(*e),
576            _ => {}
577        }
578        if !v.is_empty() {
579            nonempty += 1;
580        }
581        Ok(())
582    };
583    for v in values {
584        match v {
585            Value::Array(arr) => arr.iter().try_for_each(&mut push)?,
586            other => push(other)?,
587        }
588    }
589    let k = match (code, k) {
590        (14..=19, Some(k)) => k.scalar().to_number()?,
591        (14..=19, None) => return Err(CellError::Value),
592        _ => 0.0,
593    };
594    let r = match code {
595        1 => mean(&xs)?,
596        2 => xs.len() as f64,
597        3 => nonempty as f64,
598        4 => max_of(&xs),
599        5 => min_of(&xs),
600        6 => product(&xs),
601        7 => var_s(&xs)?.sqrt(),
602        8 => var_p(&xs)?.sqrt(),
603        9 => xs.iter().sum(),
604        10 => var_s(&xs)?,
605        11 => var_p(&xs)?,
606        12 => median(xs)?,
607        13 => mode_sngl(&xs)?,
608        14 => kth(xs, k, true)?,
609        15 => kth(xs, k, false)?,
610        16 => percentile_inc(xs, k)?,
611        17 => quartile_inc(xs, k)?,
612        18 => percentile_exc(xs, k)?,
613        19 => quartile_exc(xs, k)?,
614        _ => return Err(CellError::Value),
615    };
616    num_val(r)
617}