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:
223
calcpad-engine/src/functions/list_ops.rs
Normal file
223
calcpad-engine/src/functions/list_ops.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user