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.
224 lines
5.1 KiB
Rust
224 lines
5.1 KiB
Rust
//! 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"));
|
|
}
|
|
}
|