mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-27 12:16:54 -06:00
refactor(functions): move math.rs items to math/
This commit is contained in:
@@ -11,6 +11,14 @@ use crate::util::{
|
||||
};
|
||||
use crate::{Arg, Ctx, FnSpec, VAR};
|
||||
|
||||
mod criteria;
|
||||
mod matrix;
|
||||
mod roman_base;
|
||||
|
||||
use criteria::{averageif, averageifs, countif, countifs, maxifs, minifs, sumif, sumifs};
|
||||
use matrix::{mdeterm, minverse, mmult, munit};
|
||||
use roman_base::{arabic, base, decimal, roman};
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Lifting tables
|
||||
|
||||
@@ -635,307 +643,6 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Rounding noise in the inverse scales with 1/scale of the input matrix.
|
||||
let noise = 1e-15 / scale;
|
||||
let inv = inv.into_iter().map(|x| if x.abs() < noise { 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);
|
||||
@@ -951,128 +658,6 @@ fn seriessum(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
use super::*;
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// 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)
|
||||
}
|
||||
|
||||
pub(super) 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)
|
||||
}
|
||||
|
||||
pub(super) fn sumifs(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
num_val(ifs_numbers(a)?.iter().sum())
|
||||
}
|
||||
|
||||
pub(super) 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)
|
||||
}
|
||||
|
||||
pub(super) 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))
|
||||
}
|
||||
|
||||
pub(super) 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))
|
||||
}
|
||||
|
||||
pub(super) 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)
|
||||
}
|
||||
|
||||
pub(super) fn sumif(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
num_val(if_numbers(a)?.iter().sum())
|
||||
}
|
||||
|
||||
pub(super) 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)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use super::*;
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// 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)
|
||||
}
|
||||
|
||||
pub(super) 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
|
||||
}
|
||||
|
||||
pub(super) 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 })
|
||||
}
|
||||
|
||||
pub(super) 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Rounding noise in the inverse scales with 1/scale of the input matrix.
|
||||
let noise = 1e-15 / scale;
|
||||
let inv = inv.into_iter().map(|x| if x.abs() < noise { 0.0 } else { x }).collect();
|
||||
to_value_array(n, n, inv)
|
||||
}
|
||||
|
||||
pub(super) 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)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use super::*;
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// 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
|
||||
}
|
||||
|
||||
pub(super) 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)
|
||||
}
|
||||
|
||||
pub(super) 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 })
|
||||
}
|
||||
|
||||
pub(super) 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>())
|
||||
}
|
||||
|
||||
pub(super) 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)
|
||||
}
|
||||
Reference in New Issue
Block a user