Files
gridcraft/crates/functions/src/math.rs
T

1778 lines
68 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Math & trigonometry functions, plus the criteria aggregates (SUMIF(S), COUNTIF(S),
//! AVERAGEIF(S), MAXIFS, MINIFS).
use std::f64::consts::PI;
use gridcraft_core::{Array, CellError, Value};
use crate::criteria::Criterion;
use crate::util::{
A, MAX_CELLS, R, S, array_val, as_array, check_size, has, num, num_val, numbers, opt_num, round_half_away, text, text_val, to_sig_digits,
};
use crate::{Arg, Ctx, FnSpec, VAR};
// ---------------------------------------------------------------------------------------------
// Lifting tables
const fn alt_table(first_crit: usize) -> [bool; 256] {
// Criteria positions: first_crit, first_crit + 2, …
let mut t = [false; 256];
let mut i = first_crit;
while i < 256 {
t[i] = true;
i += 2;
}
t
}
/// COUNTIFS(range, crit, range, crit, …)
static COUNTIFS_SCAL: [bool; 256] = alt_table(1);
/// SUMIFS(sum_range, range, crit, …)
static SUMIFS_SCAL: [bool; 256] = alt_table(2);
const IF2: &[bool] = &[false, true, false];
// ---------------------------------------------------------------------------------------------
// Small helpers
const TRIG_LIMIT: f64 = 134_217_728.0; // 2^27: Excel's argument limit for trig functions
fn unary(a: &[Arg], f: impl Fn(f64) -> R<f64>) -> R<Value> {
num_val(f(num(a, 0)?)?)
}
fn trig_arg(x: f64) -> R<f64> {
if x.abs() >= TRIG_LIMIT { Err(CellError::Num) } else { Ok(x) }
}
fn nonzero(x: f64) -> R<f64> {
if x == 0.0 { Err(CellError::Div0) } else { Ok(x) }
}
/// Removes binary noise before ceil/floor (e.g. 2.0000000000000004 → 2).
fn clean(x: f64) -> f64 {
to_sig_digits(x, 15)
}
fn digits_arg(a: &[Arg], i: usize) -> R<i32> {
let d = opt_num(a, i, 0.0)?.trunc();
Ok(d.clamp(-400.0, 400.0) as i32)
}
/// Rounds |x| scaled by 10^digits with `op` (ceil / floor), keeping sign.
fn round_dir(x: f64, digits: i32, up: bool) -> f64 {
if x == 0.0 || !x.is_finite() {
return x;
}
let ax = clean(x.abs());
let (scaled, f, neg) = if digits >= 0 {
let f = 10f64.powi(digits);
(ax * f, f, false)
} else {
let f = 10f64.powi(-digits);
(ax / f, f, true)
};
if !scaled.is_finite() || scaled >= 1e17 {
return x;
}
let s = clean(scaled);
let r = if up { s.ceil() } else { s.floor() };
// Toward zero, r == 0 with an infinite scale would be 0 * inf = NaN (#NUM!); the result is simply
// zero. Away from zero the true result (10^309 or more) is not a number, so ROUNDUP stays #NUM!.
let out = if !neg {
clean(r / f)
} else if r == 0.0 && !up {
0.0
} else {
r * f
};
if x < 0.0 { -out } else { out }
}
// ---------------------------------------------------------------------------------------------
// Sums and products
fn sum(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(numbers(a)?.iter().sum())
}
/// PERCENTOF(data_subset, data_all) = SUM(data_subset) / SUM(data_all).
fn percentof(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let part = a.get(..1).ok_or(CellError::Value)?;
let all = a.get(1..2).ok_or(CellError::Value)?;
let (part, all): (f64, f64) = (numbers(part)?.iter().sum(), numbers(all)?.iter().sum());
if all == 0.0 {
return Err(CellError::Div0);
}
num_val(part / all)
}
fn sumsq(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(numbers(a)?.iter().map(|x| x * x).sum())
}
fn product(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let v = numbers(a)?;
if v.is_empty() {
return Ok(Value::Number(0.0));
}
num_val(v.iter().product())
}
/// SUMPRODUCT: non-numeric entries of arrays and multi-cell ranges count as 0, but a scalar
/// argument holding text (a literal or a single-cell reference) is `#VALUE!` (observed:
/// `SUMPRODUCT(G1330:G1330,I1330:I1330)` with G1330 = the text "4" is `#VALUE!`).
fn sumproduct(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let arrays: Vec<_> = a.iter().map(|x| (as_array(&x.value), !matches!(x.value, Value::Array(_)))).collect();
let Some((first, _)) = arrays.first() else { return Err(CellError::Value) };
let (rows, cols) = (first.rows, first.cols);
if arrays.iter().any(|(x, _)| x.rows != rows || x.cols != cols) {
return Err(CellError::Value);
}
let mut total = 0.0;
for i in 0..rows.saturating_mul(cols) {
let mut p = 1.0;
for (arr, scalar) in &arrays {
match arr.data.get(i) {
Some(Value::Number(n)) => p *= n,
Some(Value::Error(e)) => return Err(*e),
Some(Value::Text(_)) if *scalar => return Err(CellError::Value),
_ => p = 0.0,
}
}
total += p;
}
num_val(total)
}
fn pair_sum(a: &[Arg], f: fn(f64, f64) -> f64) -> R<Value> {
let x = as_array(&crate::util::arg(a, 0)?.value);
let y = as_array(&crate::util::arg(a, 1)?.value);
if x.data.len() != y.data.len() {
return Err(CellError::NA);
}
let mut total = 0.0;
let mut any = false;
for (u, v) in x.data.iter().zip(y.data.iter()) {
if let Value::Error(e) = u {
return Err(*e);
}
if let Value::Error(e) = v {
return Err(*e);
}
if let (Value::Number(p), Value::Number(q)) = (u, v) {
total += f(*p, *q);
any = true;
}
}
if !any {
return Err(CellError::Div0);
}
num_val(total)
}
fn sumx2my2(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
pair_sum(a, |x, y| x * x - y * y)
}
fn sumx2py2(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
pair_sum(a, |x, y| x * x + y * y)
}
fn sumxmy2(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
pair_sum(a, |x, y| (x - y) * (x - y))
}
// ---------------------------------------------------------------------------------------------
// Rounding
fn round(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(round_half_away(num(a, 0)?, digits_arg(a, 1)?))
}
fn roundup(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(round_dir(num(a, 0)?, digits_arg(a, 1)?, true))
}
fn rounddown(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(round_dir(num(a, 0)?, digits_arg(a, 1)?, false))
}
fn trunc(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(round_dir(num(a, 0)?, digits_arg(a, 1)?, false))
}
fn int_fn(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(x.floor()))
}
fn mround(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let (x, m) = (num(a, 0)?, num(a, 1)?);
if m == 0.0 || x == 0.0 {
return Ok(Value::Number(0.0));
}
if (x > 0.0) != (m > 0.0) {
return Err(CellError::Num);
}
let q = round_half_away(x / m, 0);
num_val(clean(q * m))
}
fn ceiling(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let x = num(a, 0)?;
let s = num(a, 1)?;
if x == 0.0 || s == 0.0 {
return Ok(Value::Number(0.0));
}
if x > 0.0 && s < 0.0 {
return Err(CellError::Num);
}
num_val(clean(clean(x / s).ceil() * s))
}
fn floor(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let x = num(a, 0)?;
let s = num(a, 1)?;
if s == 0.0 {
return if x == 0.0 { Ok(Value::Number(0.0)) } else { Err(CellError::Div0) };
}
if x == 0.0 {
return Ok(Value::Number(0.0));
}
if x > 0.0 && s < 0.0 {
return Err(CellError::Num);
}
num_val(clean(clean(x / s).floor() * s))
}
/// CEILING.MATH / FLOOR.MATH / *.PRECISE: `up` picks the direction for positive numbers;
/// negative numbers round away from zero when `away`, toward zero otherwise.
fn ceil_floor_math(x: f64, s: f64, up: bool, away: bool) -> R<Value> {
let s = s.abs();
if s == 0.0 || x == 0.0 {
return Ok(Value::Number(0.0));
}
let q = clean(x / s);
let r = if x >= 0.0 {
if up { q.ceil() } else { q.floor() }
} else if away {
q.floor()
} else {
q.ceil()
};
num_val(clean(r * s))
}
fn ceiling_math(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let x = num(a, 0)?;
let s = if has(a, 1) { num(a, 1)? } else { 1.0 };
let mode = opt_num(a, 2, 0.0)?;
ceil_floor_math(x, s, true, mode != 0.0)
}
fn floor_math(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let x = num(a, 0)?;
let s = if has(a, 1) { num(a, 1)? } else { 1.0 };
let mode = opt_num(a, 2, 0.0)?;
// Negative numbers: mode 0 rounds away from zero (down), non-zero rounds toward zero.
ceil_floor_math(x, s, false, mode == 0.0)
}
fn ceiling_precise(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let x = num(a, 0)?;
let s = if has(a, 1) { num(a, 1)? } else { 1.0 };
ceil_floor_math(x, s, true, false)
}
fn floor_precise(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let x = num(a, 0)?;
let s = if has(a, 1) { num(a, 1)? } else { 1.0 };
ceil_floor_math(x, s, false, true)
}
fn even_odd(x: f64, odd: bool) -> f64 {
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let ax = clean(x.abs());
let mut v = ax.ceil();
if odd {
if v % 2.0 == 0.0 {
v += 1.0;
}
} else if v % 2.0 != 0.0 {
v += 1.0;
}
sign * v
}
fn even(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(even_odd(x, false)))
}
fn odd(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(even_odd(x, true)))
}
// ---------------------------------------------------------------------------------------------
// Arithmetic
fn modulo(x: f64, d: f64) -> R<f64> {
if d == 0.0 {
return Err(CellError::Div0);
}
let q = (x / d).floor();
let r = x - d * q;
// Keep the sign of the divisor and avoid returning |d| from rounding.
if r != 0.0 && (r < 0.0) != (d < 0.0) {
return Ok(r + d);
}
if r.abs() >= d.abs() {
return Ok(0.0);
}
Ok(clean(r))
}
fn mod_fn(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(modulo(num(a, 0)?, num(a, 1)?)?)
}
fn quotient(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let (x, d) = (num(a, 0)?, num(a, 1)?);
num_val(clean(x / nonzero(d)?).trunc())
}
fn abs(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(x.abs()))
}
fn sign(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| {
Ok(if x > 0.0 {
1.0
} else if x < 0.0 {
-1.0
} else {
0.0
})
})
}
fn sqrt(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| if x < 0.0 { Err(CellError::Num) } else { Ok(x.sqrt()) })
}
fn sqrtpi(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| if x < 0.0 { Err(CellError::Num) } else { Ok((x * PI).sqrt()) })
}
fn power_of(x: f64, y: f64) -> R<f64> {
if x == 0.0 {
if y == 0.0 {
return Err(CellError::Num);
}
if y < 0.0 {
return Err(CellError::Div0);
}
return Ok(0.0);
}
if x < 0.0 && y.fract() != 0.0 {
return Err(CellError::Num);
}
let r = x.powf(y);
if r.is_finite() { Ok(r) } else { Err(CellError::Num) }
}
fn power(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(power_of(num(a, 0)?, num(a, 1)?)?)
}
fn exp(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(x.exp()))
}
fn ln(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| if x <= 0.0 { Err(CellError::Num) } else { Ok(x.ln()) })
}
fn log10(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| if x <= 0.0 { Err(CellError::Num) } else { Ok(x.log10()) })
}
fn log(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let x = num(a, 0)?;
let base = opt_num(a, 1, 10.0)?;
if x <= 0.0 || base <= 0.0 {
return Err(CellError::Num);
}
if base == 1.0 {
return Err(CellError::Div0);
}
let r = if base == 10.0 {
x.log10()
} else if base == 2.0 {
x.log2()
} else {
x.ln() / base.ln()
};
num_val(r)
}
fn pi(_a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(PI)
}
fn rand(_a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
num_val(c.random())
}
fn randbetween(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
let lo = num(a, 0)?.ceil();
let hi = num(a, 1)?.floor();
if lo > hi {
return Err(CellError::Num);
}
let r = c.random().clamp(0.0, 1.0);
let v = (lo + (r * (hi - lo + 1.0)).floor()).min(hi);
num_val(v)
}
// ---------------------------------------------------------------------------------------------
// Combinatorics
fn fact_of(n: f64) -> R<f64> {
let n = n.trunc();
if n < 0.0 {
return Err(CellError::Num);
}
if n > 170.0 {
return Err(CellError::Num);
}
let mut f = 1.0f64;
let mut i = 2.0;
while i <= n {
f *= i;
i += 1.0;
}
Ok(f)
}
fn fact(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(fact_of(num(a, 0)?)?)
}
fn factdouble(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let n = num(a, 0)?.trunc();
if n < -1.0 {
return Err(CellError::Num);
}
if n > 300.0 {
return Err(CellError::Num);
}
let mut f = 1.0f64;
let mut i = n;
while i > 1.0 {
f *= i;
i -= 2.0;
}
num_val(f)
}
fn combin_of(n: f64, k: f64) -> R<f64> {
let (n, k) = (n.trunc(), k.trunc());
if n < 0.0 || k < 0.0 || k > n {
return Err(CellError::Num);
}
let k = k.min(n - k);
if k > 1e7 {
let r = (crate::special::ln_gamma(n + 1.0) - crate::special::ln_gamma(k + 1.0) - crate::special::ln_gamma(n - k + 1.0)).exp();
return if r.is_finite() { Ok(r) } else { Err(CellError::Num) };
}
let mut r = 1.0f64;
let mut i = 1.0;
while i <= k {
r = r * (n - k + i) / i;
if !r.is_finite() {
return Err(CellError::Num);
}
i += 1.0;
}
Ok(if r < 9e15 { r.round() } else { r })
}
fn combin(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(combin_of(num(a, 0)?, num(a, 1)?)?)
}
fn combina(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let (n, k) = (num(a, 0)?.trunc(), num(a, 1)?.trunc());
if n < 0.0 || k < 0.0 || n < k && n == 0.0 && k > 0.0 {
return Err(CellError::Num);
}
if k == 0.0 {
return Ok(Value::Number(1.0));
}
num_val(combin_of(n + k - 1.0, k)?)
}
fn permut(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let (n, k) = (num(a, 0)?.trunc(), num(a, 1)?.trunc());
if n < 0.0 || k < 0.0 || k > n {
return Err(CellError::Num);
}
let mut r = 1.0f64;
let mut i = 0.0;
while i < k {
r *= n - i;
if !r.is_finite() {
return Err(CellError::Num);
}
i += 1.0;
}
num_val(r)
}
fn permutationa(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let (n, k) = (num(a, 0)?.trunc(), num(a, 1)?.trunc());
if n < 0.0 || k < 0.0 {
return Err(CellError::Num);
}
num_val(n.powf(k))
}
fn integer_args(a: &[Arg]) -> R<Vec<u64>> {
let v = numbers(a)?;
v.iter()
.map(|x| {
let t = x.trunc();
if !(0.0..9.007_199_254_740_992e15).contains(&t) { Err(CellError::Num) } else { Ok(t as u64) }
})
.collect()
}
fn gcd2(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a
}
fn gcd(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let v = integer_args(a)?;
num_val(v.iter().fold(0u64, |acc, &x| gcd2(acc, x)) as f64)
}
fn lcm(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let v = integer_args(a)?;
let mut acc = 1f64;
for &x in &v {
if x == 0 {
return Ok(Value::Number(0.0));
}
let accu = acc as u64;
let g = gcd2(accu, x);
acc = acc / g as f64 * x as f64;
if acc >= 9.007_199_254_740_992e15 {
return Err(CellError::Num);
}
}
num_val(acc)
}
fn multinomial(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let v = numbers(a)?;
let mut total = 0.0;
let mut r = 1.0f64;
for x in v {
let k = x.trunc();
if k < 0.0 {
return Err(CellError::Num);
}
total += k;
r *= combin_of(total, k)?;
if !r.is_finite() {
return Err(CellError::Num);
}
}
num_val(r)
}
// ---------------------------------------------------------------------------------------------
// Trigonometry
fn sin(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(trig_arg(x)?.sin()))
}
fn cos(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(trig_arg(x)?.cos()))
}
fn tan(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(trig_arg(x)?.tan()))
}
fn asin(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| if x.abs() > 1.0 { Err(CellError::Num) } else { Ok(x.asin()) })
}
fn acos(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| if x.abs() > 1.0 { Err(CellError::Num) } else { Ok(x.acos()) })
}
fn atan(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(x.atan()))
}
fn atan2(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let (x, y) = (num(a, 0)?, num(a, 1)?);
if x == 0.0 && y == 0.0 {
return Err(CellError::Div0);
}
num_val(y.atan2(x))
}
fn sinh(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(x.sinh()))
}
fn cosh(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(x.cosh()))
}
fn tanh(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(x.tanh()))
}
fn asinh(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(x.asinh()))
}
fn acosh(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| if x < 1.0 { Err(CellError::Num) } else { Ok(x.acosh()) })
}
fn atanh(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| if x.abs() >= 1.0 { Err(CellError::Num) } else { Ok(x.atanh()) })
}
fn cot(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(1.0 / nonzero(trig_arg(x)?)?.tan()))
}
fn coth(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(1.0 / nonzero(trig_arg(x)?)?.tanh()))
}
fn csc(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(1.0 / nonzero(trig_arg(x)?)?.sin()))
}
fn csch(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(1.0 / nonzero(trig_arg(x)?)?.sinh()))
}
fn sec(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(1.0 / trig_arg(x)?.cos()))
}
fn sech(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(1.0 / trig_arg(x)?.cosh()))
}
fn acot(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(PI / 2.0 - x.atan()))
}
fn acoth(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| if x.abs() <= 1.0 { Err(CellError::Num) } else { Ok(0.5 * ((x + 1.0) / (x - 1.0)).ln()) })
}
fn degrees(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(x * 180.0 / PI))
}
fn radians(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
unary(a, |x| Ok(x * PI / 180.0))
}
// ---------------------------------------------------------------------------------------------
// Number systems
const NUMERALS: [(char, u32); 7] = [('I', 1), ('V', 5), ('X', 10), ('L', 50), ('C', 100), ('D', 500), ('M', 1000)];
/// Tokens (text, value) for a ROMAN form, largest value first. Form 0 is classic; each higher
/// form widens one of two limits on the subtracted numeral's ratio to the larger one, alternating:
/// the limit for I/X/C (10, 10, 100, 100, 1000) and for V/L (none, 20, 20, 200, 200). So form 2
/// allows IC and XM but not VD, and form 3 adds VD and VM but not ID.
fn roman_tokens(form: u32) -> Vec<(String, u32)> {
let (pow10_max, five_max) = [(10, 0), (10, 20), (100, 20), (100, 200), (1000, 200)].get(form as usize).copied().unwrap_or((10, 0));
let mut toks: Vec<(String, u32)> = NUMERALS.iter().map(|(c, v)| (c.to_string(), *v)).collect();
for (i, (sc, sv)) in NUMERALS.iter().enumerate() {
for (lc, lv) in NUMERALS.iter().skip(i + 1) {
let ratio = lv / sv;
let pow10 = matches!(sc, 'I' | 'X' | 'C');
let ok = ratio <= if pow10 { pow10_max } else { five_max };
let value = lv - sv;
// Skip pairs that equal a single numeral (VX = V) – they are never shorter.
if ok && !NUMERALS.iter().any(|(_, v)| *v == value) {
toks.push((format!("{sc}{lc}"), value));
}
}
}
// Largest value first; on ties prefer the shorter / classic token.
toks.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.len().cmp(&b.0.len())));
toks
}
fn roman(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let n = num(a, 0)?.trunc();
let form = match a.get(1).map(|x| x.value.scalar()) {
None => 0.0,
Some(Value::Bool(true)) => 0.0,
Some(Value::Bool(false)) => 4.0,
Some(_) => num(a, 1)?.trunc(),
};
if !(0.0..=3999.0).contains(&n) || !(0.0..=4.0).contains(&form) {
return Err(CellError::Value);
}
let toks = roman_tokens(form as u32);
let mut rem = n as u32;
let mut out = String::new();
while rem > 0 {
let Some((t, v)) = toks.iter().find(|(_, v)| *v <= rem) else { break };
out.push_str(t);
rem -= v;
}
text_val(out)
}
fn arabic(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = text(a, 0)?;
let s = s.trim();
if s.chars().count() > 255 {
return Err(CellError::Value);
}
let (neg, body) = match s.strip_prefix('-') {
Some(r) => (true, r),
None => (false, s),
};
let mut vals = Vec::new();
for ch in body.chars() {
let u = ch.to_ascii_uppercase();
let Some((_, v)) = NUMERALS.iter().find(|(c, _)| *c == u) else { return Err(CellError::Value) };
vals.push(*v as i64);
}
let mut total = 0i64;
for (i, v) in vals.iter().enumerate() {
if vals.get(i + 1).is_some_and(|next| next > v) {
total -= v;
} else {
total += v;
}
}
num_val(if neg { -total as f64 } else { total as f64 })
}
fn base(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let n = num(a, 0)?.trunc();
let radix = num(a, 1)?.trunc();
let min_len = opt_num(a, 2, 0.0)?.trunc();
if !(0.0..9.007_199_254_740_992e15).contains(&n) || !(2.0..=36.0).contains(&radix) || !(0.0..=255.0).contains(&min_len) {
return Err(CellError::Num);
}
let radix = radix as u64;
let mut v = n as u64;
let mut digits = Vec::new();
loop {
let d = (v % radix) as u32;
digits.push(char::from_digit(d, radix as u32).unwrap_or('0').to_ascii_uppercase());
v /= radix;
if v == 0 {
break;
}
}
while digits.len() < min_len as usize {
digits.push('0');
}
text_val(digits.iter().rev().collect::<String>())
}
fn decimal(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = text(a, 0)?;
let radix = num(a, 1)?.trunc();
if !(2.0..=36.0).contains(&radix) || s.chars().count() > 255 {
return Err(CellError::Num);
}
let radix = radix as u32;
let mut total = 0f64;
for ch in s.trim().chars() {
let Some(d) = ch.to_digit(radix) else { return Err(CellError::Num) };
total = total * radix as f64 + d as f64;
}
if total >= 9.007_199_254_740_992e15 {
return Err(CellError::Num);
}
num_val(total)
}
// ---------------------------------------------------------------------------------------------
// Matrices
/// A numeric matrix (every element must be a number, else `#VALUE!`).
fn matrix(v: &Value) -> R<(usize, usize, Vec<f64>)> {
let arr = as_array(v);
let mut out = Vec::with_capacity(arr.data.len());
for x in arr.iter() {
match x {
Value::Number(n) => out.push(*n),
Value::Error(e) => return Err(*e),
_ => return Err(CellError::Value),
}
}
Ok((arr.rows, arr.cols, out))
}
fn to_value_array(rows: usize, cols: usize, data: Vec<f64>) -> R<Value> {
let vals = data.into_iter().map(|x| if x.is_finite() { Value::Number(x) } else { Value::Error(CellError::Num) }).collect();
array_val(rows, cols, vals)
}
fn mmult(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let (ar, ac, ad) = matrix(&crate::util::arg(a, 0)?.value)?;
let (br, bc, bd) = matrix(&crate::util::arg(a, 1)?.value)?;
if ac != br {
return Err(CellError::Value);
}
if ar.saturating_mul(bc) > MAX_CELLS || ar.saturating_mul(bc).saturating_mul(ac) > 2_000_000_000 {
return Err(CellError::Num);
}
let mut out = vec![0.0; ar * bc];
for i in 0..ar {
for k in 0..ac {
let x = ad.get(i * ac + k).copied().unwrap_or(0.0);
if x == 0.0 {
continue;
}
for j in 0..bc {
if let (Some(o), Some(y)) = (out.get_mut(i * bc + j), bd.get(k * bc + j)) {
*o += x * y;
}
}
}
}
to_value_array(ar, bc, out)
}
/// LU decomposition with partial pivoting; returns (lu, perm, sign) or None when singular.
fn square(a: &[Arg]) -> R<(usize, Vec<f64>)> {
let (r, c, d) = matrix(&crate::util::arg(a, 0)?.value)?;
if r != c {
return Err(CellError::Value);
}
if r > 2000 {
return Err(CellError::Num);
}
Ok((r, d))
}
fn determinant(n: usize, mut m: Vec<f64>) -> f64 {
let mut det = 1.0;
for col in 0..n {
let mut piv = col;
let mut best = 0.0;
for row in col..n {
let v = m.get(row * n + col).copied().unwrap_or(0.0).abs();
if v > best {
best = v;
piv = row;
}
}
if best == 0.0 {
return 0.0;
}
if piv != col {
for k in 0..n {
m.swap(piv * n + k, col * n + k);
}
det = -det;
}
let p = m.get(col * n + col).copied().unwrap_or(0.0);
det *= p;
for row in col + 1..n {
let f = m.get(row * n + col).copied().unwrap_or(0.0) / p;
if f == 0.0 {
continue;
}
for k in col..n {
let v = m.get(col * n + k).copied().unwrap_or(0.0);
if let Some(x) = m.get_mut(row * n + k) {
*x -= f * v;
}
}
}
}
det
}
fn mdeterm(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let (n, m) = square(a)?;
let d = determinant(n, m);
// Tidy results that should be integers (e.g. 0.9999999999999998 → 1).
num_val(if d.abs() > 1e-300 { to_sig_digits(d, 15) } else { d })
}
fn minverse(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let (n, mut m) = square(a)?;
let mut inv = vec![0.0; n * n];
for i in 0..n {
if let Some(x) = inv.get_mut(i * n + i) {
*x = 1.0;
}
}
let scale = m.iter().fold(0.0f64, |acc, x| acc.max(x.abs())).max(1e-300);
for col in 0..n {
let mut piv = col;
let mut best = 0.0;
for row in col..n {
let v = m.get(row * n + col).copied().unwrap_or(0.0).abs();
if v > best {
best = v;
piv = row;
}
}
if best <= scale * 1e-15 {
return Err(CellError::Num);
}
if piv != col {
for k in 0..n {
m.swap(piv * n + k, col * n + k);
inv.swap(piv * n + k, col * n + k);
}
}
let p = m.get(col * n + col).copied().unwrap_or(1.0);
for k in 0..n {
if let Some(x) = m.get_mut(col * n + k) {
*x /= p;
}
if let Some(x) = inv.get_mut(col * n + k) {
*x /= p;
}
}
for row in 0..n {
if row == col {
continue;
}
let f = m.get(row * n + col).copied().unwrap_or(0.0);
if f == 0.0 {
continue;
}
for k in 0..n {
let mv = m.get(col * n + k).copied().unwrap_or(0.0);
let iv = inv.get(col * n + k).copied().unwrap_or(0.0);
if let Some(x) = m.get_mut(row * n + k) {
*x -= f * mv;
}
if let Some(x) = inv.get_mut(row * n + k) {
*x -= f * iv;
}
}
}
}
let inv = inv.into_iter().map(|x| if x.abs() < 1e-15 { 0.0 } else { x }).collect();
to_value_array(n, n, inv)
}
fn munit(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let n = num(a, 0)?.trunc();
let (r, _) = check_size(n, n)?;
let mut data = vec![Value::Number(0.0); r * r];
for i in 0..r {
if let Some(x) = data.get_mut(i * r + i) {
*x = Value::Number(1.0);
}
}
array_val(r, r, data)
}
fn seriessum(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let (x, n, m) = (num(a, 0)?, num(a, 1)?, num(a, 2)?);
let coeffs = as_array(&crate::util::arg(a, 3)?.value);
let mut total = 0.0;
for (i, c) in coeffs.iter().enumerate() {
let c = match c {
Value::Number(v) => *v,
Value::Error(e) => return Err(*e),
_ => return Err(CellError::Value),
};
total += c * power_of(x, n + i as f64 * m)?;
}
num_val(total)
}
// ---------------------------------------------------------------------------------------------
// Criteria aggregates
/// Values at the matched positions of `target` for every (range, criteria) pair.
fn criteria_mask(a: &[Arg], first_pair: usize, rows: usize, cols: usize) -> R<Vec<bool>> {
let rest = a.get(first_pair..).unwrap_or(&[]);
if rest.is_empty() || rest.len() % 2 != 0 {
return Err(CellError::Value);
}
// A text criterion over 255 characters is `#VALUE!` only when there is a single criteria
// pair. With two or more pairs Excel matches its first 255 characters instead.
let single_pair = rest.len() == 2;
let mut mask = vec![true; rows.saturating_mul(cols)];
for pair in rest.chunks(2) {
let (Some(range), Some(crit)) = (pair.first(), pair.get(1)) else { return Err(CellError::Value) };
let r = as_array(&range.value);
if r.rows != rows || r.cols != cols {
return Err(CellError::Value);
}
let scalar = crit.value.scalar();
let c = if single_pair { Criterion::parse_arg(&scalar)? } else { Criterion::parse_arg_truncated(&scalar) };
for (m, v) in mask.iter_mut().zip(r.iter()) {
if *m && !c.matches(v) {
*m = false;
}
}
}
Ok(mask)
}
/// Numbers in `target` where `mask` is set (errors at matched positions propagate).
fn masked_numbers(target: &Array, mask: &[bool]) -> R<Vec<f64>> {
let mut out = Vec::new();
for (v, m) in target.iter().zip(mask.iter()) {
if !*m {
continue;
}
match v {
Value::Number(n) => out.push(*n),
Value::Error(e) => return Err(*e),
_ => {}
}
}
Ok(out)
}
fn countifs(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let first = as_array(&crate::util::arg(a, 0)?.value);
let mask = criteria_mask(a, 0, first.rows, first.cols)?;
num_val(mask.iter().filter(|m| **m).count() as f64)
}
/// The values of the `*IFS` target range with the mask from its pairs.
fn ifs_numbers(a: &[Arg]) -> R<Vec<f64>> {
let target = as_array(&crate::util::arg(a, 0)?.value);
let mask = criteria_mask(a, 1, target.rows, target.cols)?;
masked_numbers(&target, &mask)
}
fn sumifs(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(ifs_numbers(a)?.iter().sum())
}
fn averageifs(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let v = ifs_numbers(a)?;
if v.is_empty() {
return Err(CellError::Div0);
}
num_val(v.iter().sum::<f64>() / v.len() as f64)
}
fn maxifs(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let v = ifs_numbers(a)?;
num_val(v.iter().copied().reduce(f64::max).unwrap_or(0.0))
}
fn minifs(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let v = ifs_numbers(a)?;
num_val(v.iter().copied().reduce(f64::min).unwrap_or(0.0))
}
fn countif(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let range = as_array(&crate::util::arg(a, 0)?.value);
let c = Criterion::parse_arg(&crate::util::arg(a, 1)?.value.scalar())?;
num_val(range.iter().filter(|v| c.matches(v)).count() as f64)
}
/// SUMIF / AVERAGEIF: matched numbers from the sum range, read at the criteria range's positions.
/// Excel sizes a sum range reference like the criteria range, anchored at its own top-left cell;
/// the evaluator does that resizing (it has the references), so cells line up one to one here.
fn if_numbers(a: &[Arg]) -> R<Vec<f64>> {
let range = as_array(&crate::util::arg(a, 0)?.value);
let c = Criterion::parse_arg(&crate::util::arg(a, 1)?.value.scalar())?;
let sum_range = if has(a, 2) { Some(as_array(&crate::util::arg(a, 2)?.value)) } else { None };
let mut out = Vec::new();
for r in 0..range.rows {
for col in 0..range.cols {
let Some(v) = range.get(r, col) else { continue };
if !c.matches(v) {
continue;
}
let target = match &sum_range {
Some(s) => s.get(r, col),
None => Some(v),
};
match target {
Some(Value::Number(n)) => out.push(*n),
Some(Value::Error(e)) => return Err(*e),
_ => {}
}
}
}
Ok(out)
}
fn sumif(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(if_numbers(a)?.iter().sum())
}
fn averageif(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let v = if_numbers(a)?;
if v.is_empty() {
return Err(CellError::Div0);
}
num_val(v.iter().sum::<f64>() / v.len() as f64)
}
// ---------------------------------------------------------------------------------------------
// Registry
pub(crate) fn specs() -> Vec<FnSpec> {
vec![
f!("SUM", 1, VAR, MathTrig, A, "SUM(number1, [number2], ...)", "Adds all the numbers in the arguments.", sum),
f!(
"PERCENTOF",
2,
2,
MathTrig,
A,
"PERCENTOF(data_subset, data_all)",
"The sum of a subset as a fraction of the sum of all the values.",
percentof
),
f!("SUMSQ", 1, VAR, MathTrig, A, "SUMSQ(number1, [number2], ...)", "Adds the squares of the numbers.", sumsq),
f!(
"SUMPRODUCT",
1,
VAR,
MathTrig,
A,
"SUMPRODUCT(array1, [array2], ...)",
"Multiplies matching elements of equally sized arrays and adds the products.",
sumproduct
),
f!("SUMX2MY2", 2, 2, MathTrig, A, "SUMX2MY2(array_x, array_y)", "Sums the differences of squares of matching elements.", sumx2my2),
f!("SUMX2PY2", 2, 2, MathTrig, A, "SUMX2PY2(array_x, array_y)", "Sums the sums of squares of matching elements.", sumx2py2),
f!("SUMXMY2", 2, 2, MathTrig, A, "SUMXMY2(array_x, array_y)", "Sums the squared differences of matching elements.", sumxmy2),
f!("PRODUCT", 1, VAR, MathTrig, A, "PRODUCT(number1, [number2], ...)", "Multiplies all the numbers in the arguments.", product),
f!("ROUND", 2, 2, MathTrig, S, "ROUND(number, num_digits)", "Rounds a number to a given number of digits, halves away from zero.", round),
f!("ROUNDUP", 2, 2, MathTrig, S, "ROUNDUP(number, num_digits)", "Rounds a number away from zero to a given number of digits.", roundup),
f!("ROUNDDOWN", 2, 2, MathTrig, S, "ROUNDDOWN(number, num_digits)", "Rounds a number toward zero to a given number of digits.", rounddown),
f!("MROUND", 2, 2, MathTrig, S, "MROUND(number, multiple)", "Rounds a number to the nearest multiple.", mround),
f!("CEILING", 2, 2, Compatibility, S, "CEILING(number, significance)", "Rounds a number up to a multiple of significance.", ceiling),
f!(
"CEILING.MATH",
1,
3,
MathTrig,
S,
"CEILING.MATH(number, [significance], [mode])",
"Rounds a number up to the nearest integer or multiple.",
ceiling_math
),
f!(
"CEILING.PRECISE",
1,
2,
MathTrig,
S,
"CEILING.PRECISE(number, [significance])",
"Rounds a number toward positive infinity to a multiple.",
ceiling_precise
),
f!(
"ISO.CEILING",
1,
2,
MathTrig,
S,
"ISO.CEILING(number, [significance])",
"Rounds a number toward positive infinity to a multiple.",
ceiling_precise
),
f!("FLOOR", 2, 2, Compatibility, S, "FLOOR(number, significance)", "Rounds a number down to a multiple of significance.", floor),
f!(
"FLOOR.MATH",
1,
3,
MathTrig,
S,
"FLOOR.MATH(number, [significance], [mode])",
"Rounds a number down to the nearest integer or multiple.",
floor_math
),
f!(
"FLOOR.PRECISE",
1,
2,
MathTrig,
S,
"FLOOR.PRECISE(number, [significance])",
"Rounds a number toward negative infinity to a multiple.",
floor_precise
),
f!("INT", 1, 1, MathTrig, S, "INT(number)", "Rounds a number down to the nearest integer.", int_fn),
f!("TRUNC", 1, 2, MathTrig, S, "TRUNC(number, [num_digits])", "Cuts off the fractional part of a number at the given digits.", trunc),
f!("MOD", 2, 2, MathTrig, S, "MOD(number, divisor)", "Remainder of a division, with the sign of the divisor.", mod_fn),
f!("QUOTIENT", 2, 2, MathTrig, S, "QUOTIENT(numerator, denominator)", "Integer part of a division.", quotient),
f!("ABS", 1, 1, MathTrig, S, "ABS(number)", "Absolute value of a number.", abs),
f!("SIGN", 1, 1, MathTrig, S, "SIGN(number)", "1 for positive, -1 for negative and 0 for zero.", sign),
f!("SQRT", 1, 1, MathTrig, S, "SQRT(number)", "Positive square root.", sqrt),
f!("SQRTPI", 1, 1, MathTrig, S, "SQRTPI(number)", "Square root of the number times pi.", sqrtpi),
f!("POWER", 2, 2, MathTrig, S, "POWER(number, power)", "Raises a number to a power.", power),
f!("EXP", 1, 1, MathTrig, S, "EXP(number)", "e raised to the given power.", exp),
f!("LN", 1, 1, MathTrig, S, "LN(number)", "Natural logarithm.", ln),
f!("LOG", 1, 2, MathTrig, S, "LOG(number, [base])", "Logarithm to the given base (10 by default).", log),
f!("LOG10", 1, 1, MathTrig, S, "LOG10(number)", "Base-10 logarithm.", log10),
f!("PI", 0, 0, MathTrig, S, "PI()", "The constant pi to 15 digits.", pi),
f!(volatile; "RAND", 0, 0, MathTrig, S, "RAND()", "A random number between 0 and 1, recalculated every time.", rand),
f!(volatile; "RANDBETWEEN", 2, 2, MathTrig, S, "RANDBETWEEN(bottom, top)", "A random integer between two bounds, inclusive.", randbetween),
f!("FACT", 1, 1, MathTrig, S, "FACT(number)", "Factorial of a number.", fact),
f!("FACTDOUBLE", 1, 1, MathTrig, S, "FACTDOUBLE(number)", "Double factorial of a number.", factdouble),
f!("COMBIN", 2, 2, MathTrig, S, "COMBIN(number, number_chosen)", "Number of combinations without repetition.", combin),
f!("COMBINA", 2, 2, MathTrig, S, "COMBINA(number, number_chosen)", "Number of combinations with repetition.", combina),
f!("PERMUT", 2, 2, Statistical, S, "PERMUT(number, number_chosen)", "Number of ordered selections without repetition.", permut),
f!(
"PERMUTATIONA",
2,
2,
Statistical,
S,
"PERMUTATIONA(number, number_chosen)",
"Number of ordered selections with repetition.",
permutationa
),
f!("GCD", 1, VAR, MathTrig, A, "GCD(number1, [number2], ...)", "Greatest common divisor of integers.", gcd),
f!("LCM", 1, VAR, MathTrig, A, "LCM(number1, [number2], ...)", "Least common multiple of integers.", lcm),
f!("EVEN", 1, 1, MathTrig, S, "EVEN(number)", "Rounds away from zero to the nearest even integer.", even),
f!("ODD", 1, 1, MathTrig, S, "ODD(number)", "Rounds away from zero to the nearest odd integer.", odd),
f!("SIN", 1, 1, MathTrig, S, "SIN(number)", "Sine of an angle in radians.", sin),
f!("COS", 1, 1, MathTrig, S, "COS(number)", "Cosine of an angle in radians.", cos),
f!("TAN", 1, 1, MathTrig, S, "TAN(number)", "Tangent of an angle in radians.", tan),
f!("ASIN", 1, 1, MathTrig, S, "ASIN(number)", "Arcsine in radians.", asin),
f!("ACOS", 1, 1, MathTrig, S, "ACOS(number)", "Arccosine in radians.", acos),
f!("ATAN", 1, 1, MathTrig, S, "ATAN(number)", "Arctangent in radians.", atan),
f!("ATAN2", 2, 2, MathTrig, S, "ATAN2(x_num, y_num)", "Angle of the point (x, y) from the x-axis in radians.", atan2),
f!("SINH", 1, 1, MathTrig, S, "SINH(number)", "Hyperbolic sine.", sinh),
f!("COSH", 1, 1, MathTrig, S, "COSH(number)", "Hyperbolic cosine.", cosh),
f!("TANH", 1, 1, MathTrig, S, "TANH(number)", "Hyperbolic tangent.", tanh),
f!("ASINH", 1, 1, MathTrig, S, "ASINH(number)", "Inverse hyperbolic sine.", asinh),
f!("ACOSH", 1, 1, MathTrig, S, "ACOSH(number)", "Inverse hyperbolic cosine.", acosh),
f!("ATANH", 1, 1, MathTrig, S, "ATANH(number)", "Inverse hyperbolic tangent.", atanh),
f!("COT", 1, 1, MathTrig, S, "COT(number)", "Cotangent of an angle in radians.", cot),
f!("COTH", 1, 1, MathTrig, S, "COTH(number)", "Hyperbolic cotangent.", coth),
f!("CSC", 1, 1, MathTrig, S, "CSC(number)", "Cosecant of an angle in radians.", csc),
f!("CSCH", 1, 1, MathTrig, S, "CSCH(number)", "Hyperbolic cosecant.", csch),
f!("SEC", 1, 1, MathTrig, S, "SEC(number)", "Secant of an angle in radians.", sec),
f!("SECH", 1, 1, MathTrig, S, "SECH(number)", "Hyperbolic secant.", sech),
f!("ACOT", 1, 1, MathTrig, S, "ACOT(number)", "Arccotangent in radians (0 to pi).", acot),
f!("ACOTH", 1, 1, MathTrig, S, "ACOTH(number)", "Inverse hyperbolic cotangent.", acoth),
f!("DEGREES", 1, 1, MathTrig, S, "DEGREES(angle)", "Converts radians to degrees.", degrees),
f!("RADIANS", 1, 1, MathTrig, S, "RADIANS(angle)", "Converts degrees to radians.", radians),
f!("ROMAN", 1, 2, MathTrig, S, "ROMAN(number, [form])", "Converts a number to Roman numerals in one of five styles.", roman),
f!("ARABIC", 1, 1, MathTrig, S, "ARABIC(text)", "Converts Roman numerals to a number.", arabic),
f!("BASE", 2, 3, MathTrig, S, "BASE(number, radix, [min_length])", "Writes a number in another base, padded with zeros.", base),
f!("DECIMAL", 2, 2, MathTrig, S, "DECIMAL(text, radix)", "Reads text in a given base as a number.", decimal),
f!("MMULT", 2, 2, MathTrig, A, "MMULT(array1, array2)", "Matrix product of two arrays.", mmult),
f!("MDETERM", 1, 1, MathTrig, A, "MDETERM(array)", "Determinant of a square matrix.", mdeterm),
f!("MINVERSE", 1, 1, MathTrig, A, "MINVERSE(array)", "Inverse of a square matrix.", minverse),
f!("MUNIT", 1, 1, MathTrig, S, "MUNIT(dimension)", "Identity matrix of the given size.", munit),
f!("SERIESSUM", 4, 4, MathTrig, &[true, true, true, false], "SERIESSUM(x, n, m, coefficients)", "Sum of a power series.", seriessum),
f!(
"MULTINOMIAL",
1,
VAR,
MathTrig,
A,
"MULTINOMIAL(number1, [number2], ...)",
"Ratio of the factorial of a sum to the product of factorials.",
multinomial
),
f!("SUMIF", 2, 3, MathTrig, IF2, "SUMIF(range, criteria, [sum_range])", "Adds the cells that meet a condition.", sumif),
f!(
"SUMIFS",
3,
VAR,
MathTrig,
&SUMIFS_SCAL,
"SUMIFS(sum_range, criteria_range1, criteria1, ...)",
"Adds the cells that meet every condition.",
sumifs
),
f!("COUNTIF", 2, 2, Statistical, NUM_RANGE_REV, "COUNTIF(range, criteria)", "Counts the cells that meet a condition.", countif),
f!(
"COUNTIFS",
2,
VAR,
Statistical,
&COUNTIFS_SCAL,
"COUNTIFS(criteria_range1, criteria1, ...)",
"Counts the cells that meet every condition.",
countifs
),
f!(
"AVERAGEIF",
2,
3,
Statistical,
IF2,
"AVERAGEIF(range, criteria, [average_range])",
"Average of the cells that meet a condition.",
averageif
),
f!(
"AVERAGEIFS",
3,
VAR,
Statistical,
&SUMIFS_SCAL,
"AVERAGEIFS(average_range, criteria_range1, criteria1, ...)",
"Average of the cells that meet every condition.",
averageifs
),
f!(
"MAXIFS",
3,
VAR,
Statistical,
&SUMIFS_SCAL,
"MAXIFS(max_range, criteria_range1, criteria1, ...)",
"Largest value among cells that meet every condition.",
maxifs
),
f!(
"MINIFS",
3,
VAR,
Statistical,
&SUMIFS_SCAL,
"MINIFS(min_range, criteria_range1, criteria1, ...)",
"Smallest value among cells that meet every condition.",
minifs
),
]
}
const NUM_RANGE_REV: &[bool] = &[false, true];
#[cfg(test)]
mod tests {
use super::*;
use crate::util::testutil::*;
use CellError::{Div0, NA, Num};
fn ev1(name: &str, x: f64) -> Value {
ev(name, vec![n(x)])
}
fn ev2(name: &str, x: f64, y: f64) -> Value {
ev(name, vec![n(x), n(y)])
}
#[test]
fn percentof() {
// SUM(subset) / SUM(all): text, booleans and blanks inside ranges are skipped like SUM.
let all = arr(vec![vec![nv(10.0)], vec![nv(30.0)], vec![tv("x")], vec![Value::Empty], vec![nv(60.0)]]);
let part = arr(vec![vec![nv(10.0)], vec![nv(30.0)]]);
close(ev("PERCENTOF", vec![rf(part), rf(all.clone())]), 0.4);
close(ev("PERCENTOF", vec![n(25.0), n(200.0)]), 0.125);
is_err(ev("PERCENTOF", vec![n(1.0), rf(arr(vec![vec![nv(5.0), nv(-5.0)]]))]), Div0);
is_err(ev("PERCENTOF", vec![rf(arr(vec![vec![Value::Error(NA)]])), rf(all)]), NA);
is_err(ev("PERCENTOF", vec![t("abc"), n(1.0)]), CellError::Value);
}
#[test]
fn sums() {
close(ev("SUM", vec![n(1.0), n(2.0), t("3"), b(true)]), 7.0);
let r = arr(vec![vec![nv(1.0), tv("x"), Value::Bool(true), Value::Empty, nv(4.0)]]);
close(ev("SUM", vec![rf(r.clone())]), 5.0);
is_err(ev("SUM", vec![t("abc")]), CellError::Value);
is_err(ev("SUM", vec![rf(arr(vec![vec![nv(1.0), Value::Error(Div0)]]))]), Div0);
close(ev("SUM", vec![rf(tv("abc"))]), 0.0);
close(ev("SUMSQ", vec![n(3.0), n(4.0)]), 25.0);
close(ev("PRODUCT", vec![rf(r), n(2.0)]), 8.0);
close(ev("PRODUCT", vec![rf(arr(vec![vec![tv("a")]]))]), 0.0);
let a1 = arr(vec![vec![nv(3.0), nv(4.0)], vec![nv(8.0), nv(6.0)], vec![nv(1.0), nv(9.0)]]);
let a2 = arr(vec![vec![nv(2.0), nv(7.0)], vec![nv(6.0), nv(7.0)], vec![nv(5.0), nv(3.0)]]);
close(ev("SUMPRODUCT", vec![rf(a1.clone()), rf(a2)]), 156.0);
close(ev("SUMPRODUCT", vec![rf(a1.clone())]), 31.0);
is_err(ev("SUMPRODUCT", vec![rf(a1), rf(row(&[1.0, 2.0]))]), CellError::Value);
close(ev("SUMPRODUCT", vec![av(row(&[1.0, 2.0])), av(arr(vec![vec![tv("x"), nv(3.0)]]))]), 6.0);
let x = row(&[2.0, 3.0, 9.0, 1.0, 8.0, 7.0, 5.0]);
let y = row(&[6.0, 5.0, 11.0, 7.0, 5.0, 4.0, 4.0]);
close(ev("SUMX2MY2", vec![av(x.clone()), av(y.clone())]), -55.0);
close(ev("SUMX2PY2", vec![av(x.clone()), av(y.clone())]), 521.0);
close(ev("SUMXMY2", vec![av(x.clone()), av(y)]), 79.0);
is_err(ev("SUMXMY2", vec![av(x), av(row(&[1.0]))]), NA);
}
/// SUMPRODUCT cases observed in Excel (FormulaEvalTestData, row "Sumproduct"): scalars and
/// single-cell references multiply in, text in a scalar argument is `#VALUE!`, sizes must match.
#[test]
fn sumproduct_scalar_arguments() {
let cases: Vec<(Vec<crate::Arg>, Result<f64, CellError>)> = vec![
(vec![n(3.0), rf(nv(534.0)), rf(nv(1.1))], Ok(1762.2)),
(vec![rf(nv(1.1)), n(3.0), rf(nv(534.0)), rf(nv(1.1))], Ok(1938.42)),
(vec![n(2.0), rf(col(&[1.00001, 2.99999]))], Err(CellError::Value)),
(vec![rf(tv("4")), rf(nv(2.0))], Err(CellError::Value)),
(vec![t("4"), n(2.0)], Err(CellError::Value)),
(vec![rf(nv(3.0)), rf(Value::Error(CellError::Ref))], Err(CellError::Ref)),
(vec![av(arr(vec![vec![tv("4")]])), n(2.0)], Ok(0.0)),
];
for (args, want) in cases {
match want {
Ok(x) => close(ev("SUMPRODUCT", args), x),
Err(err) => is_err(ev("SUMPRODUCT", args), err),
}
}
}
#[test]
fn rounding() {
close(ev2("ROUND", 2.675, 2.0), 2.68);
close(ev2("ROUND", -1.5, 0.0), -2.0);
close(ev2("ROUND", 1234.567, -2.0), 1200.0);
close(ev2("ROUND", 2.15, 1.0), 2.2);
close(ev2("ROUNDUP", 3.2, 0.0), 4.0);
close(ev2("ROUNDUP", -3.14259, 1.0), -3.2);
close(ev2("ROUNDUP", 31415.92654, -2.0), 31500.0);
close(ev2("ROUNDDOWN", -3.9, 0.0), -3.0);
close(ev2("ROUNDDOWN", 2.71989, 3.0), 2.719);
close(ev2("ROUNDDOWN", 31415.92654, -2.0), 31400.0);
for digits in [-308.0, -309.0, -400.0] {
for x in [1234.0, -1234.0, 0.5] {
close(ev2("ROUNDDOWN", x, digits), 0.0);
close(ev2("TRUNC", x, digits), 0.0);
}
}
assert!(matches!(ev2("ROUNDUP", 1234.0, -309.0), Value::Error(CellError::Num)));
close(ev2("ROUNDUP", 0.1 + 0.2, 1.0), 0.3);
close(ev2("TRUNC", 8.987, 2.0), 8.98);
close(ev1("TRUNC", -8.9), -8.0);
close(ev1("INT", -8.9), -9.0);
close(ev1("INT", 8.9), 8.0);
close(ev2("MROUND", 10.0, 3.0), 9.0);
close(ev2("MROUND", -10.0, -3.0), -9.0);
close(ev2("MROUND", 1.3, 0.2), 1.4);
is_err(ev2("MROUND", 5.0, -2.0), Num);
close(ev2("CEILING", 2.5, 1.0), 3.0);
close(ev2("CEILING", -2.5, -2.0), -4.0);
close(ev2("CEILING", -2.5, 2.0), -2.0);
close(ev2("CEILING", 1.5, 0.1), 1.5);
close(ev2("CEILING", 0.234, 0.01), 0.24);
is_err(ev2("CEILING", 2.5, -1.0), Num);
close(ev2("FLOOR", 3.7, 2.0), 2.0);
close(ev2("FLOOR", -2.5, -2.0), -2.0);
close(ev2("FLOOR", -2.5, 2.0), -4.0);
is_err(ev2("FLOOR", 2.5, -2.0), Num);
is_err(ev2("FLOOR", 2.5, 0.0), Div0);
close(ev2("FLOOR", 1.58, 0.1), 1.5);
close(ev2("FLOOR", 0.234, 0.01), 0.23);
close(ev2("CEILING.MATH", 24.3, 5.0), 25.0);
close(ev1("CEILING.MATH", 6.7), 7.0);
close(ev2("CEILING.MATH", -8.1, 2.0), -8.0);
close(ev("CEILING.MATH", vec![n(-5.5), n(2.0), n(-1.0)]), -6.0);
close(ev2("FLOOR.MATH", 24.3, 5.0), 20.0);
close(ev1("FLOOR.MATH", 6.7), 6.0);
close(ev2("FLOOR.MATH", -8.1, 2.0), -10.0);
close(ev("FLOOR.MATH", vec![n(-5.5), n(2.0), n(-1.0)]), -4.0);
close(ev2("CEILING.PRECISE", -4.6, 2.0), -4.0);
close(ev2("CEILING.PRECISE", 4.3, -2.0), 6.0);
close(ev2("ISO.CEILING", 4.3, 0.0), 0.0);
close(ev2("FLOOR.PRECISE", -3.2, -1.0), -4.0);
close(ev1("FLOOR.PRECISE", 3.2), 3.0);
close(ev1("EVEN", 1.5), 2.0);
close(ev1("EVEN", 3.0), 4.0);
close(ev1("EVEN", 2.0), 2.0);
close(ev1("EVEN", -1.0), -2.0);
close(ev1("EVEN", 0.0), 0.0);
close(ev1("ODD", 1.5), 3.0);
close(ev1("ODD", 3.0), 3.0);
close(ev1("ODD", 2.0), 3.0);
close(ev1("ODD", -1.0), -1.0);
close(ev1("ODD", -2.0), -3.0);
close(ev1("ODD", 0.0), 1.0);
}
#[test]
fn arithmetic() {
close(ev2("MOD", 3.0, 2.0), 1.0);
close(ev2("MOD", -3.0, 2.0), 1.0);
close(ev2("MOD", 3.0, -2.0), -1.0);
close(ev2("MOD", -3.0, -2.0), -1.0);
close(ev2("MOD", 5.5, 1.0), 0.5);
is_err(ev2("MOD", 1.0, 0.0), Div0);
close(ev2("QUOTIENT", 5.0, 2.0), 2.0);
close(ev2("QUOTIENT", -10.0, 3.0), -3.0);
is_err(ev2("QUOTIENT", 1.0, 0.0), Div0);
close(ev1("ABS", -4.0), 4.0);
close(ev1("SIGN", -0.0001), -1.0);
close(ev1("SIGN", 0.0), 0.0);
close(ev1("SQRT", 16.0), 4.0);
is_err(ev1("SQRT", -16.0), Num);
close(ev1("SQRTPI", 1.0), 1.772_453_850_905_516);
close(ev2("POWER", 5.0, 2.0), 25.0);
close(ev2("POWER", 4.0, 1.25), 5.656_854_249_492_381);
close(ev2("POWER", -2.0, 3.0), -8.0);
is_err(ev2("POWER", 0.0, 0.0), Num);
is_err(ev2("POWER", 0.0, -1.0), Div0);
is_err(ev2("POWER", -8.0, 1.0 / 3.0), Num);
is_err(ev2("POWER", 10.0, 400.0), Num);
close(ev1("EXP", 1.0), std::f64::consts::E);
close(ev2("LOG", 8.0, 2.0), 3.0);
close(ev1("LOG", 10.0), 1.0);
close(ev2("LOG", 81.0, 3.0), 4.0);
is_err(ev1("LN", 0.0), Num);
is_err(ev2("LOG", 10.0, 1.0), Div0);
is_err(ev1("LOG10", -1.0), Num);
close(ev1("LN", std::f64::consts::E), 1.0);
close(ev1("LOG10", 1e5), 5.0);
close(ev("PI", vec![]), PI);
close(ev("SQRT", vec![t("16")]), 4.0);
is_err(ev("SQRT", vec![t("x")]), CellError::Value);
}
#[test]
fn combinatorics() {
close(ev1("FACT", 5.0), 120.0);
close(ev1("FACT", 1.9), 1.0);
close(ev1("FACT", 0.0), 1.0);
is_err(ev1("FACT", -1.0), Num);
is_err(ev1("FACT", 171.0), Num);
close(ev1("FACTDOUBLE", 6.0), 48.0);
close(ev1("FACTDOUBLE", 7.0), 105.0);
close(ev1("FACTDOUBLE", -1.0), 1.0);
is_err(ev1("FACTDOUBLE", -2.0), Num);
close(ev2("COMBIN", 8.0, 2.0), 28.0);
close(ev2("COMBIN", 8.9, 2.9), 28.0);
is_err(ev2("COMBIN", 2.0, 3.0), Num);
close(ev2("COMBIN", 60.0, 30.0), 118_264_581_564_861_424.0);
close(ev2("COMBINA", 4.0, 3.0), 20.0);
close(ev2("COMBINA", 10.0, 3.0), 220.0);
close(ev2("PERMUT", 100.0, 3.0), 970_200.0);
close(ev2("PERMUT", 3.0, 2.0), 6.0);
is_err(ev2("PERMUT", 2.0, 3.0), Num);
close(ev2("PERMUTATIONA", 3.0, 2.0), 9.0);
close(ev2("GCD", 24.0, 36.0), 12.0);
close(ev2("GCD", 5.0, 0.0), 5.0);
close(ev2("GCD", 7.0, 1.0), 1.0);
is_err(ev2("GCD", -1.0, 5.0), Num);
close(ev("GCD", vec![av(row(&[12.0, 18.0, 30.0]))]), 6.0);
close(ev2("LCM", 24.0, 36.0), 72.0);
close(ev2("LCM", 5.0, 2.0), 10.0);
close(ev2("LCM", 5.0, 0.0), 0.0);
close(ev("MULTINOMIAL", vec![n(2.0), n(3.0), n(4.0)]), 1260.0);
is_err(ev("MULTINOMIAL", vec![n(-1.0), n(2.0)]), Num);
}
#[test]
fn trig() {
close(ev1("SIN", PI / 2.0), 1.0);
close(ev1("COS", 0.0), 1.0);
close(ev1("TAN", PI / 4.0), 1.0);
is_err(ev1("SIN", 1e10), Num);
close(ev1("ASIN", 1.0), PI / 2.0);
is_err(ev1("ACOS", 1.5), Num);
close(ev1("ACOT", 2.0), 0.463_647_609_000_806);
close(ev1("ACOTH", 6.0), 0.168_236_118_310_606);
is_err(ev1("ACOTH", 0.5), Num);
close(ev1("COT", 30.0), -0.156_119_952_161_659);
is_err(ev1("COT", 0.0), Div0);
close(ev1("COTH", 2.0), 1.037_314_720_727_548);
close(ev1("CSC", 15.0), 1.537_780_562_208_209);
close(ev1("CSCH", 1.5), 0.469_642_440_595_653);
close(ev1("SEC", 45.0), 1.903_594_407_014_6);
close_tol(ev1("SECH", 45.0), 5.725_037_161_098_787e-20, 1e-9);
close(ev2("ATAN2", 1.0, 1.0), PI / 4.0);
close(ev2("ATAN2", -1.0, -1.0), -3.0 * PI / 4.0);
is_err(ev2("ATAN2", 0.0, 0.0), Div0);
close(ev1("ASINH", -2.5), -1.647_231_146_371_096);
close(ev1("ACOSH", 10.0), 2.993_222_846_126_381);
is_err(ev1("ACOSH", 0.5), Num);
close(ev1("ATANH", 0.5), 0.549_306_144_334_055);
is_err(ev1("ATANH", 1.0), Num);
close(ev1("SINH", 1.0), 1.175_201_193_643_801);
close(ev1("DEGREES", PI), 180.0);
close(ev1("RADIANS", 270.0), 4.712_388_980_384_69);
}
#[test]
fn number_systems() {
is_text(ev1("ROMAN", 499.0), "CDXCIX");
is_text(ev2("ROMAN", 499.0, 1.0), "LDVLIV");
is_text(ev2("ROMAN", 499.0, 2.0), "XDIX");
is_text(ev2("ROMAN", 499.0, 3.0), "VDIV");
is_text(ev2("ROMAN", 499.0, 4.0), "ID");
is_text(ev("ROMAN", vec![n(499.0), b(false)]), "ID");
is_text(ev1("ROMAN", 1999.0), "MCMXCIX");
is_text(ev1("ROMAN", 3999.0), "MMMCMXCIX");
is_text(ev1("ROMAN", 0.0), "");
is_err(ev1("ROMAN", 4000.0), CellError::Value);
is_err(ev1("ROMAN", -1.0), CellError::Value);
close(ev("ARABIC", vec![t("LVII")]), 57.0);
close(ev("ARABIC", vec![t("mcmxii")]), 1912.0);
close(ev("ARABIC", vec![t("")]), 0.0);
close(ev("ARABIC", vec![t("-XIV")]), -14.0);
is_err(ev("ARABIC", vec![t("ABC")]), CellError::Value);
is_text(ev2("BASE", 7.0, 2.0), "111");
is_text(ev2("BASE", 100.0, 16.0), "64");
is_text(ev("BASE", vec![n(15.0), n(2.0), n(10.0)]), "0000001111");
is_text(ev2("BASE", 0.0, 36.0), "0");
is_err(ev2("BASE", 7.0, 1.0), Num);
is_err(ev2("BASE", -7.0, 2.0), Num);
close(ev("DECIMAL", vec![t("FF"), n(16.0)]), 255.0);
close(ev("DECIMAL", vec![t("111"), n(2.0)]), 7.0);
close(ev("DECIMAL", vec![t("zap"), n(36.0)]), 45745.0);
is_err(ev("DECIMAL", vec![t("12"), n(2.0)]), Num);
}
/// Every number where Excel's forms 2 and 3 differ from a plain ratio limit (issue #733):
/// form 2 subtracts I from C and X from M, form 3 also V from M.
#[test]
fn roman_forms_two_and_three_match_excel() {
const HUNDREDS: [&str; 10] = ["", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"];
const ONES: [&str; 10] = ["", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"];
let mut cases = Vec::new();
for thousands in 0..4usize {
let m = "M".repeat(thousands);
for (h, hundreds) in HUNDREDS.iter().enumerate() {
if h != 4 && h != 9 {
cases.push((thousands * 1000 + h * 100 + 99, 2.0, format!("{m}{hundreds}IC")));
}
}
for (o, ones) in ONES.iter().enumerate() {
cases.push((thousands * 1000 + 990 + o, 2.0, format!("{m}XM{ones}")));
}
for (o, ones) in ONES.iter().enumerate().take(5) {
cases.push((thousands * 1000 + 995 + o, 3.0, format!("{m}VM{ones}")));
}
}
assert_eq!(cases.len(), 92);
for (number, form, want) in &cases {
is_text(ev2("ROMAN", *number as f64, *form), want);
}
// Neighbours that already matched: form 2 still has no VD or ID, form 3 no ID.
is_text(ev2("ROMAN", 495.0, 2.0), "XDV");
is_text(ev2("ROMAN", 495.0, 3.0), "VD");
is_text(ev2("ROMAN", 1499.0, 2.0), "MXDIX");
is_text(ev2("ROMAN", 999.0, 0.0), "CMXCIX");
is_text(ev2("ROMAN", 999.0, 1.0), "LMVLIV");
is_text(ev2("ROMAN", 999.0, 4.0), "IM");
is_text(ev2("ROMAN", 995.0, 4.0), "VM");
is_text(ev2("ROMAN", 99.0, 1.0), "VCIV");
is_text(ev2("ROMAN", 99.0, 4.0), "IC");
}
#[test]
fn matrices() {
let a = arr(vec![vec![nv(1.0), nv(2.0)], vec![nv(3.0), nv(4.0)]]);
let b2 = arr(vec![vec![nv(5.0), nv(6.0)], vec![nv(7.0), nv(8.0)]]);
assert_eq!(rows_of(&ev("MMULT", vec![av(a.clone()), av(b2)])), vec![vec![nv(19.0), nv(22.0)], vec![nv(43.0), nv(50.0)]]);
is_err(ev("MMULT", vec![av(a.clone()), av(row(&[1.0, 2.0]))]), CellError::Value);
is_err(ev("MMULT", vec![av(arr(vec![vec![tv("x")]])), av(row(&[1.0]))]), CellError::Value);
close(ev("MDETERM", vec![av(a.clone())]), -2.0);
let m3 = arr(vec![vec![nv(3.0), nv(6.0), nv(1.0)], vec![nv(1.0), nv(1.0), nv(0.0)], vec![nv(3.0), nv(10.0), nv(2.0)]]);
close(ev("MDETERM", vec![av(m3)]), 1.0);
let m4 = arr(vec![
vec![nv(1.0), nv(3.0), nv(8.0), nv(5.0)],
vec![nv(1.0), nv(3.0), nv(6.0), nv(1.0)],
vec![nv(1.0), nv(1.0), nv(1.0), nv(0.0)],
vec![nv(7.0), nv(3.0), nv(10.0), nv(2.0)],
]);
close(ev("MDETERM", vec![av(m4)]), 88.0);
is_err(ev("MDETERM", vec![av(row(&[1.0, 2.0]))]), CellError::Value);
let inv = ev("MINVERSE", vec![av(arr(vec![vec![nv(4.0), nv(-1.0)], vec![nv(2.0), nv(0.0)]]))]);
let r = rows_of(&inv);
close(r[0][0].clone(), 0.0);
close(r[0][1].clone(), 0.5);
close(r[1][0].clone(), -1.0);
close(r[1][1].clone(), 2.0);
is_err(ev("MINVERSE", vec![av(arr(vec![vec![nv(1.0), nv(2.0)], vec![nv(2.0), nv(4.0)]]))]), Num);
assert_eq!(rows_of(&ev1("MUNIT", 2.0)), vec![vec![nv(1.0), nv(0.0)], vec![nv(0.0), nv(1.0)]]);
is_err(ev1("MUNIT", 0.0), CellError::Value);
is_err(ev1("MUNIT", 1e6), Num);
let coeffs = row(&[1.0, -0.5, 1.0 / 24.0, -1.0 / 720.0]);
close_tol(ev("SERIESSUM", vec![n(PI / 4.0), n(0.0), n(2.0), av(coeffs)]), 0.707_103_214_823, 1e-9);
}
#[test]
fn random() {
let mut ctx = TestCtx::default();
let spec = crate::lookup("RAND").unwrap();
assert!(spec.volatile);
for _ in 0..50 {
let v = crate::call(spec, &[], &mut ctx);
let x = v.as_f64().unwrap();
assert!((0.0..1.0).contains(&x));
}
let spec = crate::lookup("RANDBETWEEN").unwrap();
for _ in 0..50 {
let x = crate::call(spec, &[n(1.0), n(6.0)], &mut ctx).as_f64().unwrap();
assert!((1.0..=6.0).contains(&x) && x.fract() == 0.0);
}
is_err(ev2("RANDBETWEEN", 5.0, 1.0), Num);
}
#[test]
fn lifting() {
assert_eq!(rows_of(&ev("ABS", vec![av(row(&[-1.0, 2.0, -3.0]))])), vec![vec![nv(1.0), nv(2.0), nv(3.0)]]);
let v = ev("POWER", vec![av(col(&[1.0, 2.0])), av(row(&[2.0, 3.0]))]);
assert_eq!(rows_of(&v), vec![vec![nv(1.0), nv(1.0)], vec![nv(4.0), nv(8.0)]]);
close(ev("ROUND", vec![av(arr(vec![vec![nv(1.25)]])), n(1.0)]), 1.3);
}
fn data() -> (Value, Value) {
let names = arr(vec![vec![tv("apple")], vec![tv("banana")], vec![tv("Apple")], vec![Value::Empty], vec![tv("cherry")]]);
let qty = col(&[10.0, 20.0, 30.0, 40.0, 50.0]);
(names, qty)
}
#[test]
fn criteria_functions() {
let (names, qty) = data();
close(ev("COUNTIF", vec![rf(names.clone()), t("apple")]), 2.0);
close(ev("COUNTIF", vec![rf(names.clone()), t("a*")]), 2.0);
close(ev("COUNTIF", vec![rf(names.clone()), t("<>apple")]), 3.0);
close(ev("COUNTIF", vec![rf(names.clone()), t("")]), 1.0);
close(ev("COUNTIF", vec![rf(qty.clone()), t(">20")]), 3.0);
close(ev("COUNTIF", vec![rf(qty.clone()), n(30.0)]), 1.0);
let multi = ev("COUNTIF", vec![rf(names.clone()), av(arr(vec![vec![tv("apple"), tv("cherry")]]))]);
assert_eq!(rows_of(&multi), vec![vec![nv(2.0), nv(1.0)]]);
close(ev("SUMIF", vec![rf(names.clone()), t("apple"), rf(qty.clone())]), 40.0);
close(ev("SUMIF", vec![rf(qty.clone()), t(">=30")]), 120.0);
close(ev("SUMIF", vec![rf(names.clone()), t("?????"), rf(col(&[1.0]))]), 1.0);
close(ev("AVERAGEIF", vec![rf(qty.clone()), t(">20")]), 40.0);
is_err(ev("AVERAGEIF", vec![rf(qty.clone()), t(">100")]), Div0);
close(ev("SUMIFS", vec![rf(qty.clone()), rf(names.clone()), t("apple"), rf(qty.clone()), t(">15")]), 30.0);
close(ev("COUNTIFS", vec![rf(names.clone()), t("*a*"), rf(qty.clone()), t("<25")]), 2.0);
close(ev("AVERAGEIFS", vec![rf(qty.clone()), rf(names.clone()), t("<>banana")]), 32.5);
close(ev("MAXIFS", vec![rf(qty.clone()), rf(names.clone()), t("apple")]), 30.0);
close(ev("MINIFS", vec![rf(qty.clone()), rf(names.clone()), t("apple")]), 10.0);
close(ev("MAXIFS", vec![rf(qty.clone()), rf(names.clone()), t("zzz")]), 0.0);
is_err(ev("SUMIFS", vec![rf(qty.clone()), rf(row(&[1.0, 2.0])), t("1")]), CellError::Value);
is_err(ev("COUNTIFS", vec![rf(qty.clone()), t(">1"), rf(qty)]), CellError::Value);
is_err(ev("SUMIF", vec![rf(col(&[1.0, 2.0])), t(">0"), rf(arr(vec![vec![Value::Error(NA)], vec![nv(1.0)]]))]), NA);
}
#[test]
fn criteria_over_255_chars_are_value_errors_with_one_pair_and_truncated_with_more() {
let ok = "x".repeat(255);
let long = "x".repeat(256);
let cells = arr(vec![vec![tv(&ok)], vec![tv(&long)]]);
let qty = col(&[1.0, 2.0]);
close(ev("COUNTIF", vec![rf(cells.clone()), t(&ok)]), 1.0);
is_err(ev("COUNTIF", vec![rf(cells.clone()), t(&long)]), CellError::Value);
close(ev("SUMIFS", vec![rf(qty.clone()), rf(cells.clone()), t(&ok)]), 1.0);
is_err(ev("SUMIFS", vec![rf(qty.clone()), rf(cells.clone()), t(&long)]), CellError::Value);
is_err(ev("COUNTIFS", vec![rf(cells.clone()), t(&long)]), CellError::Value);
// With two or more pairs Excel matches the first 255 characters: the 255-character cell
// (quantity 1), not the 256-character one (quantity 2). Checked on Excel 2016 and 365.
let over = format!("{ok}y");
close(ev("COUNTIFS", vec![rf(cells.clone()), t(&long), rf(qty.clone()), t(">0")]), 1.0);
close(ev("SUMIFS", vec![rf(qty.clone()), rf(cells.clone()), t(&long), rf(qty.clone()), t(">0")]), 1.0);
close(ev("SUMIFS", vec![rf(qty.clone()), rf(qty.clone()), t(">0"), rf(cells.clone()), t(&over)]), 1.0);
close(ev("AVERAGEIFS", vec![rf(qty.clone()), rf(cells.clone()), t(&long), rf(qty.clone()), t(">0")]), 1.0);
close(ev("MAXIFS", vec![rf(qty.clone()), rf(cells.clone()), t(&"x".repeat(300)), rf(qty), t(">0")]), 1.0);
}
#[test]
fn criteria_over_255_utf16_units_follow_excel() {
// The tester's sheet, identical on Excel 2016 and Microsoft 365: A1:A6 hold these
// strings, B is the row number, and each row uses its own string as the criterion.
let e = "😀";
let strings =
[e.repeat(127), e.repeat(128), format!("{}{e}", "a".repeat(254)), format!("{}{e}", "a".repeat(253)), e.repeat(200), "я".repeat(256)];
let cells = arr(strings.iter().map(|s| vec![tv(s)]).collect());
let rows = col(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
// COUNTIF, SUMIF, COUNTIFS with one pair, SUMIFS with a second pair `B,">0"`.
let expected: [Option<[f64; 4]>; 6] = [Some([1.0, 1.0, 1.0, 1.0]), None, None, Some([1.0, 4.0, 1.0, 4.0]), None, None];
for (s, want) in strings.iter().zip(expected) {
let countif = ev("COUNTIF", vec![rf(cells.clone()), t(s)]);
let sumif = ev("SUMIF", vec![rf(cells.clone()), t(s), rf(rows.clone())]);
let countifs = ev("COUNTIFS", vec![rf(cells.clone()), t(s)]);
let sumifs = ev("SUMIFS", vec![rf(rows.clone()), rf(cells.clone()), t(s), rf(rows.clone()), t(">0")]);
match want {
Some([a, b, c, d]) => {
close(countif, a);
close(sumif, b);
close(countifs, c);
close(sumifs, d);
}
// Over 255 units: `#VALUE!` with one pair; cut to 255 units with two, which
// matches no cell (for emoji it splits a surrogate pair).
None => {
is_err(countif, CellError::Value);
is_err(sumif, CellError::Value);
is_err(countifs, CellError::Value);
close(sumifs, 0.0);
}
}
}
}
#[test]
fn criteria_cut_inside_a_surrogate_pair() {
let e = "😀";
let cells = arr(vec![vec![tv(&e.repeat(127))], vec![tv(&e.repeat(128))], vec![tv("zzz")], vec![Value::Empty]]);
let rows = col(&[1.0, 2.0, 3.0, 4.0]);
let sumifs = |crit: String| ev("SUMIFS", vec![rf(rows.clone()), rf(cells.clone()), t(&crit), rf(rows.clone()), t(">0")]);
// "=" + 😀×128 is 257 units: the cut after 255 keeps "=" and 127 whole emoji.
close(sumifs(format!("={}", e.repeat(128))), 1.0);
// With a two-unit prefix the cut keeps 126 emoji and half of the 127th. Such a criterion
// equals no cell text: "=" matches nothing and "<>" everything, blanks included.
close(sumifs(format!("=a{}", e.repeat(128))), 0.0);
close(sumifs(format!("<>{}", e.repeat(128))), 10.0);
// Comparisons treat it as sorting just above the 126 kept emoji.
close(sumifs(format!(">={}", e.repeat(128))), 1.0 + 2.0);
close(sumifs(format!("<={}", e.repeat(128))), 3.0);
}
}