feat(engine): add units, currency, datetime, variables, and functions modules

Extracted and integrated unique feature modules from Epic 2-6 branches:

- units/: 200+ unit conversions across 14 categories, SI prefixes
  (nano-tera), CSS/screen units, binary vs decimal data, custom units
- currency/: fiat (180+ currencies, cached rates, offline fallback),
  crypto (63 coins, CoinGecko), symbol recognition, rate caching
- datetime/: date/time math, 150+ city timezone mappings (chrono-tz),
  business day calculations, unix timestamps, relative expressions
- variables/: line references (lineN, #N, prev/ans), section
  aggregators (sum/total/avg/min/max/count), subtotals, autocomplete
- functions/: trig, log, combinatorics, financial, rounding,
  list operations (min/max/gcd/lcm), video timecodes

585 tests passing across workspace.
This commit is contained in:
C. Cassel
2026-03-17 09:01:13 -04:00
committed by C. Cassel
parent 6a8fecd03e
commit 68fa54615a
41 changed files with 11601 additions and 12 deletions

View File

@@ -0,0 +1,223 @@
//! Variadic list operations: min, max, gcd, lcm.
//!
//! All accept 1 or more arguments. `gcd` and `lcm` require integer arguments.
use super::{FunctionError, FunctionRegistry};
fn min_fn(args: &[f64]) -> Result<f64, FunctionError> {
// We already know args.len() >= 1 from the variadic guard.
let mut m = args[0];
for &v in &args[1..] {
if v < m {
m = v;
}
}
Ok(m)
}
fn max_fn(args: &[f64]) -> Result<f64, FunctionError> {
let mut m = args[0];
for &v in &args[1..] {
if v > m {
m = v;
}
}
Ok(m)
}
/// GCD of two non-negative integers using Euclidean algorithm.
fn gcd_pair(mut a: i64, mut b: i64) -> i64 {
a = a.abs();
b = b.abs();
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
/// LCM of two non-negative integers.
fn lcm_pair(a: i64, b: i64) -> i64 {
if a == 0 || b == 0 {
return 0;
}
(a.abs() / gcd_pair(a, b)) * b.abs()
}
fn gcd_fn(args: &[f64]) -> Result<f64, FunctionError> {
for &v in args {
if v.fract() != 0.0 {
return Err(FunctionError::new("gcd requires integer arguments"));
}
}
let mut result = args[0] as i64;
for &v in &args[1..] {
result = gcd_pair(result, v as i64);
}
Ok(result as f64)
}
fn lcm_fn(args: &[f64]) -> Result<f64, FunctionError> {
for &v in args {
if v.fract() != 0.0 {
return Err(FunctionError::new("lcm requires integer arguments"));
}
}
let mut result = args[0] as i64;
for &v in &args[1..] {
result = lcm_pair(result, v as i64);
}
Ok(result as f64)
}
/// Register list-operation functions.
pub fn register(reg: &mut FunctionRegistry) {
reg.register_variadic("min", 1, min_fn);
reg.register_variadic("max", 1, max_fn);
reg.register_variadic("gcd", 1, gcd_fn);
reg.register_variadic("lcm", 1, lcm_fn);
}
#[cfg(test)]
mod tests {
use super::*;
fn reg() -> FunctionRegistry {
FunctionRegistry::new()
}
// --- min ---
#[test]
fn min_single() {
let v = reg().call("min", &[42.0]).unwrap();
assert!((v - 42.0).abs() < 1e-10);
}
#[test]
fn min_two() {
let v = reg().call("min", &[3.0, 7.0]).unwrap();
assert!((v - 3.0).abs() < 1e-10);
}
#[test]
fn min_many() {
let v = reg().call("min", &[10.0, 3.0, 7.0, 1.0, 5.0]).unwrap();
assert!((v - 1.0).abs() < 1e-10);
}
#[test]
fn min_negative() {
let v = reg().call("min", &[-5.0, -2.0, -10.0]).unwrap();
assert!((v - (-10.0)).abs() < 1e-10);
}
#[test]
fn min_no_args_error() {
let err = reg().call("min", &[]).unwrap_err();
assert!(err.message.contains("at least 1"));
}
// --- max ---
#[test]
fn max_single() {
let v = reg().call("max", &[42.0]).unwrap();
assert!((v - 42.0).abs() < 1e-10);
}
#[test]
fn max_two() {
let v = reg().call("max", &[3.0, 7.0]).unwrap();
assert!((v - 7.0).abs() < 1e-10);
}
#[test]
fn max_many() {
let v = reg().call("max", &[10.0, 3.0, 7.0, 1.0, 50.0]).unwrap();
assert!((v - 50.0).abs() < 1e-10);
}
#[test]
fn max_no_args_error() {
let err = reg().call("max", &[]).unwrap_err();
assert!(err.message.contains("at least 1"));
}
// --- gcd ---
#[test]
fn gcd_two_numbers() {
let v = reg().call("gcd", &[12.0, 8.0]).unwrap();
assert!((v - 4.0).abs() < 1e-10);
}
#[test]
fn gcd_three_numbers() {
let v = reg().call("gcd", &[12.0, 8.0, 6.0]).unwrap();
assert!((v - 2.0).abs() < 1e-10);
}
#[test]
fn gcd_coprime() {
let v = reg().call("gcd", &[7.0, 13.0]).unwrap();
assert!((v - 1.0).abs() < 1e-10);
}
#[test]
fn gcd_with_zero() {
let v = reg().call("gcd", &[0.0, 5.0]).unwrap();
assert!((v - 5.0).abs() < 1e-10);
}
#[test]
fn gcd_single() {
let v = reg().call("gcd", &[42.0]).unwrap();
assert!((v - 42.0).abs() < 1e-10);
}
#[test]
fn gcd_non_integer_error() {
let err = reg().call("gcd", &[3.5, 2.0]).unwrap_err();
assert!(err.message.contains("integer"));
}
// --- lcm ---
#[test]
fn lcm_two_numbers() {
let v = reg().call("lcm", &[4.0, 6.0]).unwrap();
assert!((v - 12.0).abs() < 1e-10);
}
#[test]
fn lcm_three_numbers() {
let v = reg().call("lcm", &[4.0, 6.0, 10.0]).unwrap();
assert!((v - 60.0).abs() < 1e-10);
}
#[test]
fn lcm_with_zero() {
let v = reg().call("lcm", &[0.0, 5.0]).unwrap();
assert!((v - 0.0).abs() < 1e-10);
}
#[test]
fn lcm_single() {
let v = reg().call("lcm", &[42.0]).unwrap();
assert!((v - 42.0).abs() < 1e-10);
}
#[test]
fn lcm_coprime() {
let v = reg().call("lcm", &[7.0, 13.0]).unwrap();
assert!((v - 91.0).abs() < 1e-10);
}
#[test]
fn lcm_non_integer_error() {
let err = reg().call("lcm", &[3.5, 2.0]).unwrap_err();
assert!(err.message.contains("integer"));
}
}