From a768bc597466497742ca6ce51b0cf6a41462ad26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher=20Andr=C3=A9s?= Date: Thu, 30 Oct 2025 23:24:47 +0100 Subject: [PATCH] Bugfix/nicolas bufixes (#491) * UPDATE: package lock * FIX: Add function definitions * FIX: Small fix to get FACT working * FIX: We only need integer FACT and FACTDOUBLE * FIX: Make clippy happy --- base/src/functions/engineering/mod.rs | 2 - .../engineering/transcendental/gamma.rs | 106 ------------------ .../engineering/transcendental/mod.rs | 2 - base/src/functions/mathematical.rs | 32 +++++- base/src/functions/mod.rs | 5 + webapp/IronCalc/package-lock.json | 2 +- xlsx/tests/calc_tests/FACT_DOUBLEFACT.xlsx | Bin 0 -> 9140 bytes 7 files changed, 35 insertions(+), 114 deletions(-) delete mode 100644 base/src/functions/engineering/transcendental/gamma.rs create mode 100644 xlsx/tests/calc_tests/FACT_DOUBLEFACT.xlsx diff --git a/base/src/functions/engineering/mod.rs b/base/src/functions/engineering/mod.rs index 5a125ba..1f549cf 100644 --- a/base/src/functions/engineering/mod.rs +++ b/base/src/functions/engineering/mod.rs @@ -5,5 +5,3 @@ mod convert; mod misc; mod number_basis; mod transcendental; - -pub use transcendental::{fact, fact_double}; diff --git a/base/src/functions/engineering/transcendental/gamma.rs b/base/src/functions/engineering/transcendental/gamma.rs deleted file mode 100644 index cffec59..0000000 --- a/base/src/functions/engineering/transcendental/gamma.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::f64::consts::PI; - -// FIXME: Please do a better approximation - -/// Lanczos approximation for Gamma(z). -/// Valid for z > 0. For z <= 0 use `gamma` below (which does reflection). -fn gamma_lanczos(z: f64) -> f64 { - const COEFFS: [f64; 9] = [ - 0.999_999_999_999_809_9, - 676.5203681218851, - -1259.1392167224028, - 771.323_428_777_653_1, - -176.615_029_162_140_6, - 12.507343278686905, - -0.13857109526572012, - 9.984_369_578_019_572e-6, - 1.5056327351493116e-7, - ]; - - let g = 7.0; - let mut x = COEFFS[0]; - - // sum_{i=1}^{n} c_i / (z + i) - for (i, c) in COEFFS.iter().enumerate().skip(1) { - x += c / (z + i as f64); - } - - let t = z + g + 0.5; - // √(2π) * t^(z+0.5) * e^{-t} * x - (2.0 * PI).sqrt() * t.powf(z + 0.5) * (-t).exp() * x -} - -/// Gamma function for all real x (except poles), -/// using Lanczos + reflection formula. -fn gamma(x: f64) -> f64 { - if x.is_sign_negative() { - // Reflection formula: Γ(x) = π / (sin(πx) * Γ(1 - x)) - // Beware of sin(πx) near zeros. - let sin_pix = (PI * x).sin(); - if sin_pix == 0.0 { - return f64::NAN; - } - PI / (sin_pix * gamma_lanczos(1.0 - x)) - } else if x == 0.0 { - f64::INFINITY - } else { - gamma_lanczos(x) - } -} - -/// Factorial for real x: x! = Γ(x+1) -pub fn fact(x: f64) -> f64 { - if x < 0.0 { - return f64::NAN; - } - gamma(x + 1.0) -} - -/// Double factorial for real x. -/// -/// Strategy: -/// 1. If x is (almost) integer and >= 0 → do exact product (stable, no surprises) -/// 2. Else: -/// - If x is “even-ish”: x = 2t → x!! = 2^t * Γ(t+1) -/// - If x is “odd-ish”: x = 2t-1 → x!! = Γ(2t+1) / (2^t * Γ(t+1)) -pub fn fact_double(x: f64) -> f64 { - if x < 0.0 { - return f64::NAN; - } - - let xi = x.round(); - let is_int = (x - xi).abs() < 1e-9; - - if is_int { - // integer path - let mut acc = 1.0; - let mut k = xi; - while k > 1.0 { - acc *= k; - k -= 2.0; - } - return acc; - } - - // non-integer path: use continuous extension - let frac = x % 2.0; - // even-ish if close to 0 mod 2 - if frac.abs() < 1e-6 || (frac - 2.0).abs() < 1e-6 { - // x = 2t - let t = x / 2.0; - 2f64.powf(t) * gamma(t + 1.0) - } else if (frac - 1.0).abs() < 1e-6 || (frac + 1.0).abs() < 1e-6 { - // x = 2t - 1 => t = (x+1)/2 - let t = (x + 1.0) / 2.0; - // (2t - 1)!! = Γ(2t + 1) / (2^t Γ(t+1)) - // but 2t - 1 = x, so 2t + 1 = x + 2 - let num = gamma(x + 2.0); - let den = 2f64.powf(t) * gamma(t + 1.0); - num / den - } else { - // x is neither clearly even-ish nor odd-ish – fall back to a generic formula. - // A safe thing to do is to treat it as even-ish: - let t = x / 2.0; - 2f64.powf(t) * gamma(t + 1.0) - } -} diff --git a/base/src/functions/engineering/transcendental/mod.rs b/base/src/functions/engineering/transcendental/mod.rs index d4d301f..9a90a48 100644 --- a/base/src/functions/engineering/transcendental/mod.rs +++ b/base/src/functions/engineering/transcendental/mod.rs @@ -5,7 +5,6 @@ mod bessel_jn_yn; mod bessel_k; mod bessel_util; mod erf; -mod gamma; #[cfg(test)] mod test_bessel; @@ -15,4 +14,3 @@ pub(crate) use bessel_jn_yn::jn as bessel_j; pub(crate) use bessel_jn_yn::yn as bessel_y; pub(crate) use bessel_k::bessel_k; pub(crate) use erf::erf; -pub use gamma::{fact, fact_double}; diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index e1e522e..549d9c6 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -2,7 +2,6 @@ use crate::cast::NumberOrArray; use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::expressions::parser::ArrayNode; use crate::expressions::types::CellReferenceIndex; -use crate::functions::engineering::{fact, fact_double}; use crate::number_format::to_precision; use crate::single_number_fn; use crate::{ @@ -456,8 +455,35 @@ impl Model { Ok(1.0 / f64::cosh(f)) }); single_number_fn!(fn_exp, |f: f64| Ok(f64::exp(f))); - single_number_fn!(fn_fact, |f| Ok(fact(f))); - single_number_fn!(fn_factdouble, |f| Ok(fact_double(f))); + single_number_fn!(fn_fact, |x: f64| { + let x = x.floor(); + if x < 0.0 { + return Err(Error::NUM); + } + let mut acc = 1.0; + let mut k = 2.0; + while k <= x { + acc *= k; + k += 1.0; + } + Ok(acc) + }); + single_number_fn!(fn_factdouble, |x: f64| { + let x = x.floor(); + if x < -1.0 { + return Err(Error::NUM); + } + if x < 0.0 { + return Ok(1.0); + } + let mut acc = 1.0; + let mut k = if x % 2.0 == 0.0 { 2.0 } else { 1.0 }; + while k <= x { + acc *= k; + k += 2.0; + } + Ok(acc) + }); single_number_fn!(fn_sign, |f| Ok(f64::signum(f))); pub(crate) fn fn_pi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 0841fa7..fcc5f9f 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -602,6 +602,11 @@ impl Function { "SECH" => Some(Function::Sech), "ACOTH" => Some(Function::Acoth), + "FACT" => Some(Function::Fact), + "FACTDOUBLE" => Some(Function::Factdouble), + "EXP" => Some(Function::Exp), + "SIGN" => Some(Function::Sign), + "PI" => Some(Function::Pi), "ABS" => Some(Function::Abs), "SQRT" => Some(Function::Sqrt), diff --git a/webapp/IronCalc/package-lock.json b/webapp/IronCalc/package-lock.json index 8df1630..f782380 100644 --- a/webapp/IronCalc/package-lock.json +++ b/webapp/IronCalc/package-lock.json @@ -40,7 +40,7 @@ }, "../../bindings/wasm/pkg": { "name": "@ironcalc/wasm", - "version": "0.5.0", + "version": "0.6.0", "license": "MIT/Apache-2.0" }, "node_modules/@adobe/css-tools": { diff --git a/xlsx/tests/calc_tests/FACT_DOUBLEFACT.xlsx b/xlsx/tests/calc_tests/FACT_DOUBLEFACT.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..04e814f5dde7f434f6188ddc32c5bdada3512b52 GIT binary patch literal 9140 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 literal 0 HcmV?d00001