mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-29 05:06:51 -06:00
548 lines
17 KiB
Rust
548 lines
17 KiB
Rust
use super::{Arg, C, CellError, R, Value, arg, array_numbers, array_val, as_array, has, num, num_val, opt_num};
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// Basic helpers over number lists
|
||
// ---------------------------------------------------------------------------------------------
|
||
|
||
pub(super) fn mean(x: &[f64]) -> R<f64> {
|
||
if x.is_empty() {
|
||
return Err(CellError::Div0);
|
||
}
|
||
Ok(x.iter().sum::<f64>() / x.len() as f64)
|
||
}
|
||
|
||
pub(super) fn devsq(x: &[f64]) -> f64 {
|
||
if x.is_empty() {
|
||
return 0.0;
|
||
}
|
||
let m = x.iter().sum::<f64>() / x.len() as f64;
|
||
x.iter().map(|v| (v - m) * (v - m)).sum()
|
||
}
|
||
|
||
pub(super) fn var_s(x: &[f64]) -> R<f64> {
|
||
if x.len() < 2 {
|
||
return Err(CellError::Div0);
|
||
}
|
||
Ok(devsq(x) / (x.len() - 1) as f64)
|
||
}
|
||
|
||
pub(super) fn var_p(x: &[f64]) -> R<f64> {
|
||
if x.is_empty() {
|
||
return Err(CellError::Div0);
|
||
}
|
||
Ok(devsq(x) / x.len() as f64)
|
||
}
|
||
|
||
fn sorted(mut v: Vec<f64>) -> Vec<f64> {
|
||
v.sort_by(f64::total_cmp);
|
||
v
|
||
}
|
||
|
||
pub(super) fn median(v: Vec<f64>) -> R<f64> {
|
||
if v.is_empty() {
|
||
return Err(CellError::Num);
|
||
}
|
||
let s = sorted(v);
|
||
let n = s.len();
|
||
let hi = s.get(n / 2).copied().unwrap_or(0.0);
|
||
if n % 2 == 1 { Ok(hi) } else { Ok(s.get(n / 2 - 1).copied().unwrap_or(0.0).midpoint(hi)) }
|
||
}
|
||
|
||
/// Values occurring at least twice, ordered by first occurrence, with their (equal, maximal) count.
|
||
fn modes(v: &[f64]) -> Vec<f64> {
|
||
let mut counts: Vec<(f64, usize)> = Vec::new();
|
||
for &x in v {
|
||
if let Some(e) = counts.iter_mut().find(|e| e.0 == x) {
|
||
e.1 += 1;
|
||
} else {
|
||
counts.push((x, 1));
|
||
}
|
||
}
|
||
let best = counts.iter().map(|e| e.1).max().unwrap_or(0);
|
||
if best < 2 {
|
||
return Vec::new();
|
||
}
|
||
counts.into_iter().filter(|e| e.1 == best).map(|e| e.0).collect()
|
||
}
|
||
|
||
pub(super) fn mode_sngl(v: &[f64]) -> R<f64> {
|
||
modes(v).first().copied().ok_or(CellError::NA)
|
||
}
|
||
|
||
/// Numbers for MODE / MODE.SNGL / MODE.MULT. Inside multi-cell ranges and arrays only numbers
|
||
/// count, like [`numbers`]. Scalar arguments differ from SUM: in Excel 365, text and logical
|
||
/// values are skipped whether typed or referenced (even numeric text such as `"1"`, so
|
||
/// `MODE(1,"1",2,3)` is `#N/A`, and `MODE(1,TRUE,TRUE,2)` is `#N/A`), but a single-cell reference
|
||
/// to a blank cell is `#VALUE!` (`MODE(B9,2,D7)` with D7 blank). Arguments are processed left to
|
||
/// right, so the first error wins. The results cached in POI's FormulaEvalTestData come from an
|
||
/// older Excel that answered `#VALUE!` for typed text; they are not the reference here.
|
||
pub(super) fn mode_numbers(args: &[Arg]) -> R<Vec<f64>> {
|
||
let mut out = Vec::new();
|
||
for a in args {
|
||
match &a.value {
|
||
Value::Array(arr) => {
|
||
for v in arr.iter() {
|
||
match v {
|
||
Value::Number(n) => out.push(*n),
|
||
Value::Error(e) => return Err(*e),
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
Value::Number(n) => out.push(*n),
|
||
Value::Error(e) => return Err(*e),
|
||
Value::Text(_) | Value::Bool(_) => {}
|
||
Value::Empty => return Err(CellError::Value),
|
||
}
|
||
}
|
||
Ok(out)
|
||
}
|
||
|
||
fn product(v: &[f64]) -> f64 {
|
||
if v.is_empty() { 0.0 } else { v.iter().product() }
|
||
}
|
||
|
||
pub(super) fn min_of(v: &[f64]) -> f64 {
|
||
if v.is_empty() { 0.0 } else { v.iter().copied().fold(f64::INFINITY, f64::min) }
|
||
}
|
||
|
||
pub(super) fn max_of(v: &[f64]) -> f64 {
|
||
if v.is_empty() { 0.0 } else { v.iter().copied().fold(f64::NEG_INFINITY, f64::max) }
|
||
}
|
||
|
||
/// k-th largest (k is rounded up like Excel).
|
||
fn kth(v: Vec<f64>, k: f64, largest: bool) -> R<f64> {
|
||
let k = k.ceil();
|
||
if k.is_nan() || k < 1.0 || k > v.len() as f64 {
|
||
return Err(CellError::Num);
|
||
}
|
||
let s = sorted(v);
|
||
let k = k as usize;
|
||
let idx = if largest { s.len() - k } else { k - 1 };
|
||
s.get(idx).copied().ok_or(CellError::Num)
|
||
}
|
||
|
||
fn interp(s: &[f64], pos: f64) -> R<f64> {
|
||
let i = pos.floor();
|
||
let frac = pos - i;
|
||
let i = i as usize;
|
||
let lo = *s.get(i).ok_or(CellError::Num)?;
|
||
match s.get(i + 1) {
|
||
Some(hi) if frac > 0.0 => Ok(lo + frac * (hi - lo)),
|
||
_ => Ok(lo),
|
||
}
|
||
}
|
||
|
||
pub(super) fn percentile_inc(v: Vec<f64>, k: f64) -> R<f64> {
|
||
if v.is_empty() || !(0.0..=1.0).contains(&k) {
|
||
return Err(CellError::Num);
|
||
}
|
||
let s = sorted(v);
|
||
interp(&s, k * (s.len() - 1) as f64)
|
||
}
|
||
|
||
pub(super) fn percentile_exc(v: Vec<f64>, k: f64) -> R<f64> {
|
||
let n = v.len() as f64;
|
||
if v.is_empty() || !(k > 0.0 && k < 1.0) {
|
||
return Err(CellError::Num);
|
||
}
|
||
let pos = k * (n + 1.0);
|
||
if pos < 1.0 || pos > n {
|
||
return Err(CellError::Num);
|
||
}
|
||
let s = sorted(v);
|
||
interp(&s, pos - 1.0)
|
||
}
|
||
|
||
pub(super) fn quartile_inc(v: Vec<f64>, q: f64) -> R<f64> {
|
||
let q = q.trunc();
|
||
if !(0.0..=4.0).contains(&q) {
|
||
return Err(CellError::Num);
|
||
}
|
||
percentile_inc(v, q / 4.0)
|
||
}
|
||
|
||
pub(super) fn quartile_exc(v: Vec<f64>, q: f64) -> R<f64> {
|
||
let q = q.trunc();
|
||
if !(1.0..=3.0).contains(&q) {
|
||
return Err(CellError::Num);
|
||
}
|
||
percentile_exc(v, q / 4.0)
|
||
}
|
||
|
||
pub(super) fn arg_nums(a: &[Arg], i: usize) -> R<Vec<f64>> {
|
||
array_numbers(&arg(a, i)?.value)
|
||
}
|
||
|
||
/// Pairs of numbers at matching positions of two arrays (pairs with a non-number skipped).
|
||
/// Different sizes give `#N/A`; errors propagate.
|
||
fn pairs(x: &Value, y: &Value) -> R<(Vec<f64>, Vec<f64>)> {
|
||
let xa = as_array(x);
|
||
let ya = as_array(y);
|
||
if xa.data.len() != ya.data.len() {
|
||
return Err(CellError::NA);
|
||
}
|
||
let mut xs = Vec::new();
|
||
let mut ys = Vec::new();
|
||
for (a, b) in xa.data.iter().zip(ya.data.iter()) {
|
||
if let Value::Error(e) = a {
|
||
return Err(*e);
|
||
}
|
||
if let Value::Error(e) = b {
|
||
return Err(*e);
|
||
}
|
||
if let (Value::Number(p), Value::Number(q)) = (a, b) {
|
||
xs.push(*p);
|
||
ys.push(*q);
|
||
}
|
||
}
|
||
Ok((xs, ys))
|
||
}
|
||
|
||
pub(super) fn arg_pairs(a: &[Arg], i: usize, j: usize) -> R<(Vec<f64>, Vec<f64>)> {
|
||
pairs(&arg(a, i)?.value, &arg(a, j)?.value)
|
||
}
|
||
|
||
/// (sum of cross deviations, devsq x, devsq y, mean x, mean y).
|
||
pub(super) fn moments(x: &[f64], y: &[f64]) -> R<(f64, f64, f64, f64, f64)> {
|
||
let mx = mean(x)?;
|
||
let my = mean(y)?;
|
||
let mut sxy = 0.0;
|
||
let mut sxx = 0.0;
|
||
let mut syy = 0.0;
|
||
for (a, b) in x.iter().zip(y) {
|
||
sxy += (a - mx) * (b - my);
|
||
sxx += (a - mx) * (a - mx);
|
||
syy += (b - my) * (b - my);
|
||
}
|
||
Ok((sxy, sxx, syy, mx, my))
|
||
}
|
||
|
||
pub(super) fn correl(x: &[f64], y: &[f64]) -> R<f64> {
|
||
if x.len() < 2 {
|
||
return Err(CellError::Div0);
|
||
}
|
||
let (sxy, sxx, syy, _, _) = moments(x, y)?;
|
||
if sxx == 0.0 || syy == 0.0 {
|
||
return Err(CellError::Div0);
|
||
}
|
||
Ok(sxy / (sxx * syy).sqrt())
|
||
}
|
||
|
||
pub(super) fn slope(y: &[f64], x: &[f64]) -> R<f64> {
|
||
let (sxy, sxx, _, _, _) = moments(x, y)?;
|
||
if sxx == 0.0 {
|
||
return Err(CellError::Div0);
|
||
}
|
||
Ok(sxy / sxx)
|
||
}
|
||
|
||
pub(super) fn intercept(y: &[f64], x: &[f64]) -> R<f64> {
|
||
let (sxy, sxx, _, mx, my) = moments(x, y)?;
|
||
if sxx == 0.0 {
|
||
return Err(CellError::Div0);
|
||
}
|
||
Ok(my - sxy / sxx * mx)
|
||
}
|
||
|
||
fn trunc_digits(r: f64, sig: f64) -> f64 {
|
||
let f = 10f64.powi(sig as i32);
|
||
if !f.is_finite() {
|
||
return r;
|
||
}
|
||
((r * f) + 1e-9).floor() / f
|
||
}
|
||
|
||
fn col_value(v: Vec<f64>) -> R<Value> {
|
||
let n = v.len();
|
||
array_val(n, 1, v.into_iter().map(Value::Number).collect())
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// Aggregates
|
||
// ---------------------------------------------------------------------------------------------
|
||
|
||
pub(super) fn count_impl(a: &[Arg]) -> f64 {
|
||
let mut c = 0usize;
|
||
for x in a {
|
||
match &x.value {
|
||
Value::Array(arr) => c += arr.iter().filter(|v| v.is_number()).count(),
|
||
Value::Number(_) => c += 1,
|
||
_ if x.from_ref => {}
|
||
Value::Bool(_) => c += 1,
|
||
Value::Text(t) if gridcraft_core::parse::parse_number_text(t).is_some() => c += 1,
|
||
_ => {}
|
||
}
|
||
}
|
||
c as f64
|
||
}
|
||
|
||
pub(super) fn counta_impl(a: &[Arg]) -> f64 {
|
||
let mut c = 0usize;
|
||
for x in a {
|
||
match &x.value {
|
||
Value::Array(arr) => c += arr.iter().filter(|v| !v.is_empty()).count(),
|
||
Value::Empty if x.from_ref => {}
|
||
_ => c += 1,
|
||
}
|
||
}
|
||
c as f64
|
||
}
|
||
|
||
pub(super) fn countblank(a: &[Arg], _: C) -> R<Value> {
|
||
let v = &arg(a, 0)?.value;
|
||
let blank = |x: &Value| matches!(x, Value::Empty) || matches!(x, Value::Text(t) if t.is_empty());
|
||
let c = match v {
|
||
Value::Array(arr) => arr.iter().filter(|x| blank(x)).count(),
|
||
x => usize::from(blank(x)),
|
||
};
|
||
num_val(c as f64)
|
||
}
|
||
|
||
pub(super) fn mode_mult(a: &[Arg], _: C) -> R<Value> {
|
||
let v = mode_numbers(a)?;
|
||
let m = modes(&v);
|
||
if m.is_empty() {
|
||
return Err(CellError::NA);
|
||
}
|
||
col_value(m)
|
||
}
|
||
|
||
pub(super) fn large(a: &[Arg], _: C) -> R<Value> {
|
||
num_val(kth(arg_nums(a, 0)?, num(a, 1)?, true)?)
|
||
}
|
||
|
||
pub(super) fn small(a: &[Arg], _: C) -> R<Value> {
|
||
num_val(kth(arg_nums(a, 0)?, num(a, 1)?, false)?)
|
||
}
|
||
|
||
pub(super) fn rank_impl(a: &[Arg], avg: bool) -> R<Value> {
|
||
let x = num(a, 0)?;
|
||
let v = arg_nums(a, 1)?;
|
||
let asc = opt_num(a, 2, 0.0)? != 0.0;
|
||
let eq = v.iter().filter(|&&y| y == x).count();
|
||
if eq == 0 {
|
||
return Err(CellError::NA);
|
||
}
|
||
let before = v.iter().filter(|&&y| if asc { y < x } else { y > x }).count();
|
||
let r = before as f64 + 1.0;
|
||
num_val(if avg { r + (eq as f64 - 1.0) / 2.0 } else { r })
|
||
}
|
||
|
||
pub(super) fn percentrank_impl(a: &[Arg], exc: bool) -> R<Value> {
|
||
let s = sorted(arg_nums(a, 0)?);
|
||
let x = num(a, 1)?;
|
||
let sig = opt_num(a, 2, 3.0)?.trunc();
|
||
if sig < 1.0 || s.is_empty() {
|
||
return Err(CellError::Num);
|
||
}
|
||
let (first, last) = (s.first().copied().unwrap_or(0.0), s.last().copied().unwrap_or(0.0));
|
||
if x < first || x > last {
|
||
return Err(CellError::NA);
|
||
}
|
||
let n = s.len() as f64;
|
||
let rank_of = |v: f64| -> f64 {
|
||
let less = s.iter().filter(|&&y| y < v).count() as f64;
|
||
if exc {
|
||
(less + 1.0) / (n + 1.0)
|
||
} else if n <= 1.0 {
|
||
1.0
|
||
} else {
|
||
less / (n - 1.0)
|
||
}
|
||
};
|
||
let r = if s.contains(&x) {
|
||
rank_of(x)
|
||
} else {
|
||
let lo = s.iter().copied().filter(|&y| y < x).fold(f64::NEG_INFINITY, f64::max);
|
||
let hi = s.iter().copied().filter(|&y| y > x).fold(f64::INFINITY, f64::min);
|
||
let (rl, rh) = (rank_of(lo), rank_of(hi));
|
||
rl + (x - lo) / (hi - lo) * (rh - rl)
|
||
};
|
||
num_val(trunc_digits(r, sig))
|
||
}
|
||
|
||
pub(super) fn trimmean(a: &[Arg], _: C) -> R<Value> {
|
||
let v = arg_nums(a, 0)?;
|
||
let p = num(a, 1)?;
|
||
if !(0.0..1.0).contains(&p) || v.is_empty() {
|
||
return Err(CellError::Num);
|
||
}
|
||
let n = v.len();
|
||
let per_side = ((n as f64 * p) / 2.0).floor() as usize;
|
||
let s = sorted(v);
|
||
let kept = s.get(per_side..n - per_side).unwrap_or(&[]);
|
||
num_val(mean(kept)?)
|
||
}
|
||
|
||
pub(super) fn kurt(v: &[f64]) -> R<f64> {
|
||
let n = v.len() as f64;
|
||
if v.len() < 4 {
|
||
return Err(CellError::Div0);
|
||
}
|
||
let m = mean(v)?;
|
||
let s = var_s(v)?.sqrt();
|
||
if s == 0.0 {
|
||
return Err(CellError::Div0);
|
||
}
|
||
let sum4: f64 = v.iter().map(|x| ((x - m) / s).powi(4)).sum();
|
||
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)))
|
||
}
|
||
|
||
pub(super) fn skew(v: &[f64]) -> R<f64> {
|
||
let n = v.len() as f64;
|
||
if v.len() < 3 {
|
||
return Err(CellError::Div0);
|
||
}
|
||
let m = mean(v)?;
|
||
let s = var_s(v)?.sqrt();
|
||
if s == 0.0 {
|
||
return Err(CellError::Div0);
|
||
}
|
||
let sum3: f64 = v.iter().map(|x| ((x - m) / s).powi(3)).sum();
|
||
Ok(n / ((n - 1.0) * (n - 2.0)) * sum3)
|
||
}
|
||
|
||
pub(super) fn skew_p(v: &[f64]) -> R<f64> {
|
||
let m = mean(v)?;
|
||
let s = var_p(v)?.sqrt();
|
||
if s == 0.0 {
|
||
return Err(CellError::Div0);
|
||
}
|
||
Ok(v.iter().map(|x| ((x - m) / s).powi(3)).sum::<f64>() / v.len() as f64)
|
||
}
|
||
|
||
pub(super) fn geomean(v: &[f64]) -> R<f64> {
|
||
if v.is_empty() || v.iter().any(|&x| x <= 0.0) {
|
||
return Err(CellError::Num);
|
||
}
|
||
Ok((v.iter().map(|x| x.ln()).sum::<f64>() / v.len() as f64).exp())
|
||
}
|
||
|
||
pub(super) fn harmean(v: &[f64]) -> R<f64> {
|
||
if v.is_empty() || v.iter().any(|&x| x <= 0.0) {
|
||
return Err(CellError::Num);
|
||
}
|
||
Ok(v.len() as f64 / v.iter().map(|x| 1.0 / x).sum::<f64>())
|
||
}
|
||
|
||
pub(super) fn avedev(v: &[f64]) -> R<f64> {
|
||
if v.is_empty() {
|
||
return Err(CellError::Num);
|
||
}
|
||
let m = mean(v)?;
|
||
Ok(v.iter().map(|x| (x - m).abs()).sum::<f64>() / v.len() as f64)
|
||
}
|
||
|
||
pub(super) fn frequency(a: &[Arg], _: C) -> R<Value> {
|
||
let data = arg_nums(a, 0)?;
|
||
let bins = arg_nums(a, 1)?;
|
||
if bins.is_empty() {
|
||
return col_value(vec![data.len() as f64]);
|
||
}
|
||
let mut order: Vec<usize> = (0..bins.len()).collect();
|
||
order.sort_by(|&i, &j| bins.get(i).copied().unwrap_or(0.0).total_cmp(&bins.get(j).copied().unwrap_or(0.0)));
|
||
let mut counts = vec![0.0; bins.len() + 1];
|
||
for x in data {
|
||
let slot = order.iter().find(|&&i| x <= bins.get(i).copied().unwrap_or(f64::INFINITY)).copied().unwrap_or(bins.len());
|
||
if let Some(c) = counts.get_mut(slot) {
|
||
*c += 1.0;
|
||
}
|
||
}
|
||
col_value(counts)
|
||
}
|
||
|
||
pub(super) fn prob(a: &[Arg], _: C) -> R<Value> {
|
||
let (xs, ps) = arg_pairs(a, 0, 1)?;
|
||
let lo = num(a, 2)?;
|
||
let hi = if has(a, 3) { num(a, 3)? } else { lo };
|
||
if ps.iter().any(|&p| p <= 0.0 || p > 1.0) {
|
||
return Err(CellError::Num);
|
||
}
|
||
let total: f64 = ps.iter().sum();
|
||
if (total - 1.0).abs() > 1e-7 {
|
||
return Err(CellError::Num);
|
||
}
|
||
num_val(xs.iter().zip(&ps).filter(|(x, _)| **x >= lo && **x <= hi).map(|(_, p)| p).sum())
|
||
}
|
||
|
||
pub(super) fn standardize(a: &[Arg], _: C) -> R<Value> {
|
||
let (x, m, sd) = (num(a, 0)?, num(a, 1)?, num(a, 2)?);
|
||
if sd <= 0.0 {
|
||
return Err(CellError::Num);
|
||
}
|
||
num_val((x - m) / sd)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// SUBTOTAL / AGGREGATE support
|
||
// ---------------------------------------------------------------------------------------------
|
||
|
||
/// Evaluates SUBTOTAL/AGGREGATE function `function_num` over already-filtered cell values.
|
||
///
|
||
/// Codes 1–11 (and 101–111) are AVERAGE, COUNT, COUNTA, MAX, MIN, PRODUCT, STDEV.S, STDEV.P,
|
||
/// SUM, VAR.S, VAR.P; 12 = MEDIAN, 13 = MODE.SNGL. Codes 14–19 need `k`: use
|
||
/// [`aggregate_values_k`] (they give `#VALUE!` here). Values that are not numbers are ignored
|
||
/// (COUNTA counts non-empty values); the first error value present is returned.
|
||
pub fn aggregate_values(function_num: u32, values: &[Value]) -> Value {
|
||
crate::util::finish(aggregate_impl(function_num, values, None))
|
||
}
|
||
|
||
/// AGGREGATE codes 14 = LARGE, 15 = SMALL, 16 = PERCENTILE.INC, 17 = QUARTILE.INC,
|
||
/// 18 = PERCENTILE.EXC, 19 = QUARTILE.EXC with their `k` argument; codes 1–13 delegate to
|
||
/// [`aggregate_values`].
|
||
pub fn aggregate_values_k(function_num: u32, values: &[Value], k: &Value) -> Value {
|
||
crate::util::finish(aggregate_impl(function_num, values, Some(k)))
|
||
}
|
||
|
||
fn aggregate_impl(function_num: u32, values: &[Value], k: Option<&Value>) -> R<Value> {
|
||
let code = if (101..=111).contains(&function_num) { function_num - 100 } else { function_num };
|
||
let mut xs = Vec::new();
|
||
let mut nonempty = 0usize;
|
||
let mut push = |v: &Value| -> R<()> {
|
||
match v {
|
||
Value::Number(n) => xs.push(*n),
|
||
Value::Error(e) => return Err(*e),
|
||
_ => {}
|
||
}
|
||
if !v.is_empty() {
|
||
nonempty += 1;
|
||
}
|
||
Ok(())
|
||
};
|
||
for v in values {
|
||
match v {
|
||
Value::Array(arr) => arr.iter().try_for_each(&mut push)?,
|
||
other => push(other)?,
|
||
}
|
||
}
|
||
let k = match (code, k) {
|
||
(14..=19, Some(k)) => k.scalar().to_number()?,
|
||
(14..=19, None) => return Err(CellError::Value),
|
||
_ => 0.0,
|
||
};
|
||
let r = match code {
|
||
1 => mean(&xs)?,
|
||
2 => xs.len() as f64,
|
||
3 => nonempty as f64,
|
||
4 => max_of(&xs),
|
||
5 => min_of(&xs),
|
||
6 => product(&xs),
|
||
7 => var_s(&xs)?.sqrt(),
|
||
8 => var_p(&xs)?.sqrt(),
|
||
9 => xs.iter().sum(),
|
||
10 => var_s(&xs)?,
|
||
11 => var_p(&xs)?,
|
||
12 => median(xs)?,
|
||
13 => mode_sngl(&xs)?,
|
||
14 => kth(xs, k, true)?,
|
||
15 => kth(xs, k, false)?,
|
||
16 => percentile_inc(xs, k)?,
|
||
17 => quartile_inc(xs, k)?,
|
||
18 => percentile_exc(xs, k)?,
|
||
19 => quartile_exc(xs, k)?,
|
||
_ => return Err(CellError::Value),
|
||
};
|
||
num_val(r)
|
||
}
|