From 1db55e4e5fb41dbd1a18b30233d7098d374248cd Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 20:02:35 +0000 Subject: [PATCH] refactor(functions): move math.rs items to math/ --- crates/functions/src/math.rs | 431 +----------------------- crates/functions/src/math/criteria.rs | 128 +++++++ crates/functions/src/math/matrix.rs | 182 ++++++++++ crates/functions/src/math/roman_base.rs | 121 +++++++ 4 files changed, 439 insertions(+), 423 deletions(-) create mode 100644 crates/functions/src/math/criteria.rs create mode 100644 crates/functions/src/math/matrix.rs create mode 100644 crates/functions/src/math/roman_base.rs diff --git a/crates/functions/src/math.rs b/crates/functions/src/math.rs index 964af2e5e..6d4cdcf58 100644 --- a/crates/functions/src/math.rs +++ b/crates/functions/src/math.rs @@ -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 { 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 { - 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 { - 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 { - 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::()) -} - -fn decimal(a: &[Arg], _c: &mut dyn Ctx) -> R { - 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)> { - 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) -> R { - 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 { - 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)> { - 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 { - 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 { - 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 { - 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 { - 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 { 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 { 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> { - 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> { - 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 { - 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> { - 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 { - num_val(ifs_numbers(a)?.iter().sum()) -} -fn averageifs(a: &[Arg], _c: &mut dyn Ctx) -> R { - let v = ifs_numbers(a)?; - if v.is_empty() { - return Err(CellError::Div0); - } - num_val(v.iter().sum::() / v.len() as f64) -} -fn maxifs(a: &[Arg], _c: &mut dyn Ctx) -> R { - 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 { - 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 { - 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> { - 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 { - num_val(if_numbers(a)?.iter().sum()) -} -fn averageif(a: &[Arg], _c: &mut dyn Ctx) -> R { - let v = if_numbers(a)?; - if v.is_empty() { - return Err(CellError::Div0); - } - num_val(v.iter().sum::() / v.len() as f64) -} - // --------------------------------------------------------------------------------------------- // Registry diff --git a/crates/functions/src/math/criteria.rs b/crates/functions/src/math/criteria.rs new file mode 100644 index 000000000..fd77aed34 --- /dev/null +++ b/crates/functions/src/math/criteria.rs @@ -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> { + 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> { + 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 { + 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> { + 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 { + num_val(ifs_numbers(a)?.iter().sum()) +} + +pub(super) fn averageifs(a: &[Arg], _c: &mut dyn Ctx) -> R { + let v = ifs_numbers(a)?; + if v.is_empty() { + return Err(CellError::Div0); + } + num_val(v.iter().sum::() / v.len() as f64) +} + +pub(super) fn maxifs(a: &[Arg], _c: &mut dyn Ctx) -> R { + 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 { + 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 { + 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> { + 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 { + num_val(if_numbers(a)?.iter().sum()) +} + +pub(super) fn averageif(a: &[Arg], _c: &mut dyn Ctx) -> R { + let v = if_numbers(a)?; + if v.is_empty() { + return Err(CellError::Div0); + } + num_val(v.iter().sum::() / v.len() as f64) +} diff --git a/crates/functions/src/math/matrix.rs b/crates/functions/src/math/matrix.rs new file mode 100644 index 000000000..9bfb75c53 --- /dev/null +++ b/crates/functions/src/math/matrix.rs @@ -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)> { + 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) -> R { + 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 { + 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)> { + 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 { + 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 { + 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 { + 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 { + 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) +} diff --git a/crates/functions/src/math/roman_base.rs b/crates/functions/src/math/roman_base.rs new file mode 100644 index 000000000..931552e93 --- /dev/null +++ b/crates/functions/src/math/roman_base.rs @@ -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 { + 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 { + 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 { + 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::()) +} + +pub(super) fn decimal(a: &[Arg], _c: &mut dyn Ctx) -> R { + 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) +}