Files
calctext/calcpad-engine/src/functions/logarithmic.rs
C. Cassel 68fa54615a 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.
2026-03-17 09:01:13 -04:00

250 lines
5.9 KiB
Rust

//! 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);
}
}