Files
gridcraft/crates/functions/src/math.rs
T
vangel76andClaude Opus 5.5 a2894eff83 Functions: PERCENTOF and EUROCONVERT (#279)
PERCENTOF = SUM(subset) / SUM(all), written as _xlfn.PERCENTOF.
EUROCONVERT uses the fixed EU conversion rates for the currencies the
function documents, with currency rounding and triangulation precision.
New Add-in category for EUROCONVERT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:56:18 +02:00

1622 lines
59 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() };
let out = if neg { r * f } else { clean(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())
}
fn sumproduct(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let arrays: Vec<_> = a.iter().map(|x| as_array(&x.value)).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 in &arrays {
match arr.data.get(i) {
Some(Value::Number(n)) => p *= n,
Some(Value::Error(e)) => return Err(*e),
_ => 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; higher forms
/// allow subtracting progressively smaller numerals (max ratio 10, 20, 50, 100, 1000).
fn roman_tokens(form: u32) -> Vec<(String, u32)> {
let max_ratio = [10, 20, 50, 100, 1000].get(form as usize).copied().unwrap_or(10);
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 = if form == 0 { pow10 && ratio <= 10 } else { ratio <= max_ratio };
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);
}
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 c = Criterion::parse(&crit.value.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(&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(&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);
}
#[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);
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);
}
#[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);
}
}