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:
249
calcpad-engine/src/functions/logarithmic.rs
Normal file
249
calcpad-engine/src/functions/logarithmic.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
//! Logarithmic, exponential, and root functions.
|
||||
//!
|
||||
//! - `ln` — natural logarithm (base e)
|
||||
//! - `log` — common logarithm (base 10)
|
||||
//! - `log2` — binary logarithm (base 2)
|
||||
//! - `exp` — e raised to a power
|
||||
//! - `pow` — base raised to an exponent (2 args)
|
||||
//! - `sqrt` — square root
|
||||
//! - `cbrt` — cube root
|
||||
|
||||
use super::{FunctionError, FunctionRegistry};
|
||||
|
||||
fn ln_fn(args: &[f64]) -> Result<f64, FunctionError> {
|
||||
let x = args[0];
|
||||
if x <= 0.0 {
|
||||
return Err(FunctionError::new(
|
||||
"Argument out of domain for ln (must be positive)",
|
||||
));
|
||||
}
|
||||
Ok(x.ln())
|
||||
}
|
||||
|
||||
fn log_fn(args: &[f64]) -> Result<f64, FunctionError> {
|
||||
let x = args[0];
|
||||
if x <= 0.0 {
|
||||
return Err(FunctionError::new(
|
||||
"Argument out of domain for log (must be positive)",
|
||||
));
|
||||
}
|
||||
Ok(x.log10())
|
||||
}
|
||||
|
||||
fn log2_fn(args: &[f64]) -> Result<f64, FunctionError> {
|
||||
let x = args[0];
|
||||
if x <= 0.0 {
|
||||
return Err(FunctionError::new(
|
||||
"Argument out of domain for log2 (must be positive)",
|
||||
));
|
||||
}
|
||||
Ok(x.log2())
|
||||
}
|
||||
|
||||
fn exp_fn(args: &[f64]) -> Result<f64, FunctionError> {
|
||||
Ok(args[0].exp())
|
||||
}
|
||||
|
||||
fn pow_fn(args: &[f64]) -> Result<f64, FunctionError> {
|
||||
Ok(args[0].powf(args[1]))
|
||||
}
|
||||
|
||||
fn sqrt_fn(args: &[f64]) -> Result<f64, FunctionError> {
|
||||
let x = args[0];
|
||||
if x < 0.0 {
|
||||
return Err(FunctionError::new(
|
||||
"Argument out of domain for sqrt (must be non-negative)",
|
||||
));
|
||||
}
|
||||
Ok(x.sqrt())
|
||||
}
|
||||
|
||||
fn cbrt_fn(args: &[f64]) -> Result<f64, FunctionError> {
|
||||
Ok(args[0].cbrt())
|
||||
}
|
||||
|
||||
/// Register all logarithmic/exponential/root functions.
|
||||
pub fn register(reg: &mut FunctionRegistry) {
|
||||
reg.register_fixed("ln", 1, ln_fn);
|
||||
reg.register_fixed("log", 1, log_fn);
|
||||
reg.register_fixed("log2", 1, log2_fn);
|
||||
reg.register_fixed("exp", 1, exp_fn);
|
||||
reg.register_fixed("pow", 2, pow_fn);
|
||||
reg.register_fixed("sqrt", 1, sqrt_fn);
|
||||
reg.register_fixed("cbrt", 1, cbrt_fn);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn reg() -> FunctionRegistry {
|
||||
FunctionRegistry::new()
|
||||
}
|
||||
|
||||
// --- ln ---
|
||||
|
||||
#[test]
|
||||
fn ln_one_is_zero() {
|
||||
let v = reg().call("ln", &[1.0]).unwrap();
|
||||
assert!(v.abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ln_e_is_one() {
|
||||
let v = reg().call("ln", &[std::f64::consts::E]).unwrap();
|
||||
assert!((v - 1.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ln_zero_domain_error() {
|
||||
let err = reg().call("ln", &[0.0]).unwrap_err();
|
||||
assert!(err.message.contains("out of domain"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ln_negative_domain_error() {
|
||||
let err = reg().call("ln", &[-1.0]).unwrap_err();
|
||||
assert!(err.message.contains("out of domain"));
|
||||
}
|
||||
|
||||
// --- log (base 10) ---
|
||||
|
||||
#[test]
|
||||
fn log_100_is_2() {
|
||||
let v = reg().call("log", &[100.0]).unwrap();
|
||||
assert!((v - 2.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_1000_is_3() {
|
||||
let v = reg().call("log", &[1000.0]).unwrap();
|
||||
assert!((v - 3.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_negative_domain_error() {
|
||||
let err = reg().call("log", &[-1.0]).unwrap_err();
|
||||
assert!(err.message.contains("out of domain"));
|
||||
}
|
||||
|
||||
// --- log2 ---
|
||||
|
||||
#[test]
|
||||
fn log2_256_is_8() {
|
||||
let v = reg().call("log2", &[256.0]).unwrap();
|
||||
assert!((v - 8.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log2_one_is_zero() {
|
||||
let v = reg().call("log2", &[1.0]).unwrap();
|
||||
assert!(v.abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log2_negative_domain_error() {
|
||||
let err = reg().call("log2", &[-5.0]).unwrap_err();
|
||||
assert!(err.message.contains("out of domain"));
|
||||
}
|
||||
|
||||
// --- exp ---
|
||||
|
||||
#[test]
|
||||
fn exp_zero_is_one() {
|
||||
let v = reg().call("exp", &[0.0]).unwrap();
|
||||
assert!((v - 1.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exp_one_is_e() {
|
||||
let v = reg().call("exp", &[1.0]).unwrap();
|
||||
assert!((v - std::f64::consts::E).abs() < 1e-10);
|
||||
}
|
||||
|
||||
// --- pow ---
|
||||
|
||||
#[test]
|
||||
fn pow_2_10_is_1024() {
|
||||
let v = reg().call("pow", &[2.0, 10.0]).unwrap();
|
||||
assert!((v - 1024.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pow_3_0_is_1() {
|
||||
let v = reg().call("pow", &[3.0, 0.0]).unwrap();
|
||||
assert!((v - 1.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
// --- sqrt ---
|
||||
|
||||
#[test]
|
||||
fn sqrt_144_is_12() {
|
||||
let v = reg().call("sqrt", &[144.0]).unwrap();
|
||||
assert!((v - 12.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqrt_2_approx() {
|
||||
let v = reg().call("sqrt", &[2.0]).unwrap();
|
||||
assert!((v - std::f64::consts::SQRT_2).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqrt_zero_is_zero() {
|
||||
let v = reg().call("sqrt", &[0.0]).unwrap();
|
||||
assert!(v.abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqrt_negative_domain_error() {
|
||||
let err = reg().call("sqrt", &[-4.0]).unwrap_err();
|
||||
assert!(err.message.contains("out of domain"));
|
||||
}
|
||||
|
||||
// --- cbrt ---
|
||||
|
||||
#[test]
|
||||
fn cbrt_27_is_3() {
|
||||
let v = reg().call("cbrt", &[27.0]).unwrap();
|
||||
assert!((v - 3.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbrt_8_is_2() {
|
||||
let v = reg().call("cbrt", &[8.0]).unwrap();
|
||||
assert!((v - 2.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbrt_neg_8_is_neg_2() {
|
||||
let v = reg().call("cbrt", &[-8.0]).unwrap();
|
||||
assert!((v - (-2.0)).abs() < 1e-10);
|
||||
}
|
||||
|
||||
// --- composition ---
|
||||
|
||||
#[test]
|
||||
fn ln_exp_roundtrip() {
|
||||
let r = reg();
|
||||
let inner = r.call("exp", &[5.0]).unwrap();
|
||||
let v = r.call("ln", &[inner]).unwrap();
|
||||
assert!((v - 5.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exp_ln_roundtrip() {
|
||||
let r = reg();
|
||||
let inner = r.call("ln", &[10.0]).unwrap();
|
||||
let v = r.call("exp", &[inner]).unwrap();
|
||||
assert!((v - 10.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqrt_pow_roundtrip() {
|
||||
let r = reg();
|
||||
let inner = r.call("pow", &[3.0, 2.0]).unwrap();
|
||||
let v = r.call("sqrt", &[inner]).unwrap();
|
||||
assert!((v - 3.0).abs() < 1e-10);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user