From 69729ffa0988bffa968be3a8fb00e790cb3a0ec2 Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Fri, 1 Aug 2025 14:12:59 -0700 Subject: [PATCH 1/5] Implement statistical distribution functions --- Cargo.lock | 149 ++++++ base/Cargo.toml | 1 + .../src/expressions/parser/static_analysis.rs | 26 ++ base/src/functions/mod.rs | 67 ++- base/src/functions/statistical.rs | 434 ++++++++++++++++++ base/src/test/mod.rs | 1 + base/src/test/test_statistical_dist.rs | 60 +++ docs/src/functions/statistical.md | 26 +- docs/src/functions/statistical/beta.dist.md | 2 +- docs/src/functions/statistical/beta.inv.md | 2 +- docs/src/functions/statistical/binom.dist.md | 2 +- .../functions/statistical/binom.dist.range.md | 2 +- docs/src/functions/statistical/binom.inv.md | 2 +- docs/src/functions/statistical/expon.dist.md | 2 +- docs/src/functions/statistical/gamma.dist.md | 2 +- docs/src/functions/statistical/gamma.inv.md | 2 +- docs/src/functions/statistical/gamma.md | 2 +- docs/src/functions/statistical/gammaln.md | 2 +- .../functions/statistical/gammaln.precise.md | 2 +- .../src/functions/statistical/poisson.dist.md | 2 +- .../src/functions/statistical/weibull.dist.md | 2 +- 21 files changed, 763 insertions(+), 27 deletions(-) create mode 100644 base/src/test/test_statistical_dist.rs diff --git a/Cargo.lock b/Cargo.lock index 677cbf79f..bc3c6bee7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,15 @@ dependencies = [ "libc", ] +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -443,6 +452,7 @@ dependencies = [ "ryu", "serde", "serde_json", + "statrs", ] [[package]] @@ -505,12 +515,28 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "memchr" version = "2.7.2" @@ -535,6 +561,23 @@ dependencies = [ "adler", ] +[[package]] +name = "nalgebra" +version = "0.33.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b" +dependencies = [ + "approx", + "matrixmultiply", + "num-complex", + "num-rational", + "num-traits", + "rand", + "rand_distr", + "simba", + "typenum", +] + [[package]] name = "napi" version = "2.16.13" @@ -594,12 +637,51 @@ dependencies = [ "libloading", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.18" @@ -607,6 +689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -635,6 +718,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbkdf2" version = "0.11.0" @@ -830,6 +919,22 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "regex" version = "1.10.4" @@ -883,6 +988,15 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -959,12 +1073,37 @@ dependencies = [ "digest", ] +[[package]] +name = "simba" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "statrs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" +dependencies = [ + "approx", + "nalgebra", + "num-traits", + "rand", +] + [[package]] name = "subtle" version = "2.5.0" @@ -1189,6 +1328,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "windows-core" version = "0.52.0" diff --git a/base/Cargo.toml b/base/Cargo.toml index cb97b34b9..63e5fa9f4 100644 --- a/base/Cargo.toml +++ b/base/Cargo.toml @@ -19,6 +19,7 @@ regex = { version = "1.0", optional = true} regex-lite = { version = "0.1.6", optional = true} bitcode = "0.6.3" csv = "1.3.0" +statrs = "0.18" [features] default = ["use_regex_full"] diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 80f194360..03ed0d7f4 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -784,6 +784,19 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec args_signature_scalars(arg_count, 2, 0), Function::Formulatext => args_signature_scalars(arg_count, 1, 0), Function::Unicode => args_signature_scalars(arg_count, 1, 0), + Function::BetaDist => args_signature_scalars(arg_count, 4, 2), + Function::BetaInv => args_signature_scalars(arg_count, 3, 2), + Function::BinomDist => args_signature_scalars(arg_count, 4, 0), + Function::BinomDistRange => args_signature_scalars(arg_count, 3, 1), + Function::BinomInv => args_signature_scalars(arg_count, 3, 0), + Function::ExponDist => args_signature_scalars(arg_count, 3, 0), + Function::Gamma => args_signature_scalars(arg_count, 1, 0), + Function::GammaDist => args_signature_scalars(arg_count, 4, 0), + Function::GammaInv => args_signature_scalars(arg_count, 3, 0), + Function::Gammaln => args_signature_scalars(arg_count, 1, 0), + Function::GammalnPrecise => args_signature_scalars(arg_count, 1, 0), + Function::PoissonDist => args_signature_scalars(arg_count, 3, 0), + Function::WeibullDist => args_signature_scalars(arg_count, 4, 0), Function::Geomean => vec![Signature::Vector; arg_count], } } @@ -989,6 +1002,19 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Randbetween => scalar_arguments(args), Function::Eomonth => scalar_arguments(args), Function::Formulatext => not_implemented(args), + Function::BetaDist => scalar_arguments(args), + Function::BetaInv => scalar_arguments(args), + Function::BinomDist => scalar_arguments(args), + Function::BinomDistRange => scalar_arguments(args), + Function::BinomInv => scalar_arguments(args), + Function::ExponDist => scalar_arguments(args), + Function::Gamma => scalar_arguments(args), + Function::GammaDist => scalar_arguments(args), + Function::GammaInv => scalar_arguments(args), + Function::Gammaln => scalar_arguments(args), + Function::GammalnPrecise => scalar_arguments(args), + Function::PoissonDist => scalar_arguments(args), + Function::WeibullDist => scalar_arguments(args), Function::Geomean => not_implemented(args), } } diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 21c8f72da..d52f6f401 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -145,6 +145,19 @@ pub enum Function { Maxifs, Minifs, Geomean, + BetaDist, + BetaInv, + BinomDist, + BinomDistRange, + BinomInv, + ExponDist, + Gamma, + GammaDist, + GammaInv, + Gammaln, + GammalnPrecise, + PoissonDist, + WeibullDist, // Date and time Date, @@ -253,7 +266,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -357,6 +370,19 @@ impl Function { Function::Maxifs, Function::Minifs, Function::Geomean, + Function::BetaDist, + Function::BetaInv, + Function::BinomDist, + Function::BinomDistRange, + Function::BinomInv, + Function::ExponDist, + Function::Gamma, + Function::GammaDist, + Function::GammaInv, + Function::Gammaln, + Function::GammalnPrecise, + Function::PoissonDist, + Function::WeibullDist, Function::Year, Function::Day, Function::Month, @@ -625,6 +651,19 @@ impl Function { "MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs), "MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs), "GEOMEAN" => Some(Function::Geomean), + "BETA.DIST" => Some(Function::BetaDist), + "BETA.INV" => Some(Function::BetaInv), + "BINOM.DIST" => Some(Function::BinomDist), + "BINOM.DIST.RANGE" => Some(Function::BinomDistRange), + "BINOM.INV" => Some(Function::BinomInv), + "EXPON.DIST" => Some(Function::ExponDist), + "GAMMA" => Some(Function::Gamma), + "GAMMA.DIST" => Some(Function::GammaDist), + "GAMMA.INV" => Some(Function::GammaInv), + "GAMMALN" => Some(Function::Gammaln), + "GAMMALN.PRECISE" => Some(Function::GammalnPrecise), + "POISSON.DIST" => Some(Function::PoissonDist), + "WEIBULL.DIST" => Some(Function::WeibullDist), // Date and Time "YEAR" => Some(Function::Year), "DAY" => Some(Function::Day), @@ -836,6 +875,19 @@ impl fmt::Display for Function { Function::Maxifs => write!(f, "MAXIFS"), Function::Minifs => write!(f, "MINIFS"), Function::Geomean => write!(f, "GEOMEAN"), + Function::BetaDist => write!(f, "BETA.DIST"), + Function::BetaInv => write!(f, "BETA.INV"), + Function::BinomDist => write!(f, "BINOM.DIST"), + Function::BinomDistRange => write!(f, "BINOM.DIST.RANGE"), + Function::BinomInv => write!(f, "BINOM.INV"), + Function::ExponDist => write!(f, "EXPON.DIST"), + Function::Gamma => write!(f, "GAMMA"), + Function::GammaDist => write!(f, "GAMMA.DIST"), + Function::GammaInv => write!(f, "GAMMA.INV"), + Function::Gammaln => write!(f, "GAMMALN"), + Function::GammalnPrecise => write!(f, "GAMMALN.PRECISE"), + Function::PoissonDist => write!(f, "POISSON.DIST"), + Function::WeibullDist => write!(f, "WEIBULL.DIST"), Function::Year => write!(f, "YEAR"), Function::Day => write!(f, "DAY"), Function::Month => write!(f, "MONTH"), @@ -1076,6 +1128,19 @@ impl Model { Function::Maxifs => self.fn_maxifs(args, cell), Function::Minifs => self.fn_minifs(args, cell), Function::Geomean => self.fn_geomean(args, cell), + Function::BetaDist => self.fn_beta_dist(args, cell), + Function::BetaInv => self.fn_beta_inv(args, cell), + Function::BinomDist => self.fn_binom_dist(args, cell), + Function::BinomDistRange => self.fn_binom_dist_range(args, cell), + Function::BinomInv => self.fn_binom_inv(args, cell), + Function::ExponDist => self.fn_expon_dist(args, cell), + Function::Gamma => self.fn_gamma(args, cell), + Function::GammaDist => self.fn_gamma_dist(args, cell), + Function::GammaInv => self.fn_gamma_inv(args, cell), + Function::Gammaln => self.fn_gammaln(args, cell), + Function::GammalnPrecise => self.fn_gammaln_precise(args, cell), + Function::PoissonDist => self.fn_poisson_dist(args, cell), + Function::WeibullDist => self.fn_weibull_dist(args, cell), // Date and Time Function::Year => self.fn_year(args, cell), Function::Day => self.fn_day(args, cell), diff --git a/base/src/functions/statistical.rs b/base/src/functions/statistical.rs index cdb936406..edac6c8d9 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -730,4 +730,438 @@ impl Model { } CalcResult::Number(product.powf(1.0 / count)) } + + pub(crate) fn fn_beta_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + use statrs::distribution::{Beta, Continuous, ContinuousCDF}; + + if args.len() < 4 || args.len() > 6 { + return CalcResult::new_args_number_error(cell); + } + + let x = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let alpha = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let beta = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let cumulative = match self.get_boolean(&args[3], cell) { + Ok(b) => b, + Err(e) => return e, + }; + let a = if args.len() > 4 { + match self.get_number_no_bools(&args[4], cell) { + Ok(f) => f, + Err(e) => return e, + } + } else { + 0.0 + }; + let b = if args.len() > 5 { + match self.get_number_no_bools(&args[5], cell) { + Ok(f) => f, + Err(e) => return e, + } + } else { + 1.0 + }; + + if alpha <= 0.0 || beta <= 0.0 || b <= a || x < a || x > b { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + } + + let dist = match Beta::new(alpha, beta) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + }; + + let z = (x - a) / (b - a); + if cumulative { + CalcResult::Number(dist.cdf(z)) + } else { + CalcResult::Number(dist.pdf(z) / (b - a)) + } + } + + pub(crate) fn fn_beta_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + use statrs::distribution::{Beta, ContinuousCDF}; + + if args.len() < 3 || args.len() > 5 { + return CalcResult::new_args_number_error(cell); + } + + let p = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let alpha = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let beta = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let a = if args.len() > 3 { + match self.get_number_no_bools(&args[3], cell) { + Ok(f) => f, + Err(e) => return e, + } + } else { + 0.0 + }; + let b = if args.len() > 4 { + match self.get_number_no_bools(&args[4], cell) { + Ok(f) => f, + Err(e) => return e, + } + } else { + 1.0 + }; + + if p < 0.0 || p > 1.0 || alpha <= 0.0 || beta <= 0.0 || b <= a { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + } + + let dist = match Beta::new(alpha, beta) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + }; + + let res = dist.inverse_cdf(p) * (b - a) + a; + CalcResult::Number(res) + } + + pub(crate) fn fn_gamma_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + use statrs::distribution::{Continuous, ContinuousCDF, Gamma}; + + if args.len() != 4 { + return CalcResult::new_args_number_error(cell); + } + + let x = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let alpha = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let beta = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let cumulative = match self.get_boolean(&args[3], cell) { + Ok(b) => b, + Err(e) => return e, + }; + + if x < 0.0 || alpha <= 0.0 || beta <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + } + + let dist = match Gamma::new(alpha, 1.0 / beta) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + }; + + if cumulative { + CalcResult::Number(dist.cdf(x)) + } else { + CalcResult::Number(dist.pdf(x)) + } + } + + pub(crate) fn fn_gamma_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + use statrs::distribution::{Gamma, ContinuousCDF}; + + if args.len() != 3 { + return CalcResult::new_args_number_error(cell); + } + + let p = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let alpha = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let beta = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(e) => return e, + }; + + if p < 0.0 || p > 1.0 || alpha <= 0.0 || beta <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + } + + let dist = match Gamma::new(alpha, 1.0 / beta) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + }; + + CalcResult::Number(dist.inverse_cdf(p)) + } + + pub(crate) fn fn_gamma(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + use statrs::function::gamma::gamma; + + if args.len() != 1 { + return CalcResult::new_args_number_error(cell); + } + + let x = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(e) => return e, + }; + + if x == 0.0 || (x < 0.0 && (x.fract() == 0.0)) { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + } + + CalcResult::Number(gamma(x)) + } + + pub(crate) fn fn_gammaln(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + use statrs::function::gamma::ln_gamma; + + if args.len() != 1 { + return CalcResult::new_args_number_error(cell); + } + let x = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(e) => return e, + }; + if x <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + } + CalcResult::Number(ln_gamma(x)) + } + + pub(crate) fn fn_gammaln_precise(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + self.fn_gammaln(args, cell) + } + + pub(crate) fn fn_expon_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + use statrs::distribution::{Exp, Continuous, ContinuousCDF}; + + if args.len() != 3 { + return CalcResult::new_args_number_error(cell); + } + let x = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let lambda = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let cumulative = match self.get_boolean(&args[2], cell) { + Ok(b) => b, + Err(e) => return e, + }; + + if x < 0.0 || lambda <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + } + + let dist = match Exp::new(lambda) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + }; + if cumulative { + CalcResult::Number(dist.cdf(x)) + } else { + CalcResult::Number(dist.pdf(x)) + } + } + + pub(crate) fn fn_weibull_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + use statrs::distribution::{Continuous, ContinuousCDF, Weibull}; + + if args.len() != 4 { + return CalcResult::new_args_number_error(cell); + } + let x = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let alpha = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let beta = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let cumulative = match self.get_boolean(&args[3], cell) { + Ok(b) => b, + Err(e) => return e, + }; + if x < 0.0 || alpha <= 0.0 || beta <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + } + let dist = match Weibull::new(alpha, beta) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + }; + if cumulative { + CalcResult::Number(dist.cdf(x)) + } else { + CalcResult::Number(dist.pdf(x)) + } + } + + pub(crate) fn fn_poisson_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + use statrs::distribution::{Poisson, DiscreteCDF, Discrete}; + + if args.len() != 3 { + return CalcResult::new_args_number_error(cell); + } + let x = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let mean = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let cumulative = match self.get_boolean(&args[2], cell) { + Ok(b) => b, + Err(e) => return e, + }; + if x < 0.0 || mean <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + } + let dist = match Poisson::new(mean) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + }; + let k = x.floor() as u64; + if cumulative { + CalcResult::Number(dist.cdf(k)) + } else { + CalcResult::Number(dist.pmf(k)) + } + } + + pub(crate) fn fn_binom_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + use statrs::distribution::{Binomial, DiscreteCDF, Discrete}; + + if args.len() != 4 { + return CalcResult::new_args_number_error(cell); + } + let number_s = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let trials = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let p = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let cumulative = match self.get_boolean(&args[3], cell) { + Ok(b) => b, + Err(e) => return e, + }; + + if trials < 0.0 || p < 0.0 || p > 1.0 || number_s < 0.0 || number_s > trials { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + } + let dist = match Binomial::new(p, trials.round() as u64) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + }; + let k = number_s.floor() as u64; + if cumulative { + CalcResult::Number(dist.cdf(k)) + } else { + CalcResult::Number(dist.pmf(k)) + } + } + + pub(crate) fn fn_binom_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + use statrs::distribution::{Binomial, DiscreteCDF}; + + if args.len() != 3 { + return CalcResult::new_args_number_error(cell); + } + let trials = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let p = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let alpha = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(e) => return e, + }; + + if trials < 0.0 || p < 0.0 || p > 1.0 || alpha < 0.0 || alpha > 1.0 { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + } + let dist = match Binomial::new(p, trials.round() as u64) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + }; + let result = dist.inverse_cdf(alpha) as f64; + CalcResult::Number(result) + } + + pub(crate) fn fn_binom_dist_range(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + use statrs::distribution::{Binomial, Discrete}; + + if !(3..=4).contains(&args.len()) { + return CalcResult::new_args_number_error(cell); + } + + let trials = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let p = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let s1 = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(e) => return e, + }; + let s2 = if args.len() == 4 { + match self.get_number_no_bools(&args[3], cell) { + Ok(f) => f, + Err(e) => return e, + } + } else { + s1 + }; + + if trials < 0.0 || p < 0.0 || p > 1.0 || s1 < 0.0 || s2 < s1 { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + } + + let dist = match Binomial::new(p, trials.round() as u64) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + }; + + let mut prob = 0.0; + let start = s1.floor() as u64; + let end = s2.floor() as u64; + for k in start..=end { + prob += dist.pmf(k); + } + CalcResult::Number(prob) + } } diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index a0a0d69d6..b616555c1 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -68,4 +68,5 @@ mod test_percentage; mod test_set_functions_error_handling; mod test_today; mod test_types; +mod test_statistical_dist; mod user_model; diff --git a/base/src/test/test_statistical_dist.rs b/base/src/test/test_statistical_dist.rs new file mode 100644 index 000000000..cbc5473bb --- /dev/null +++ b/base/src/test/test_statistical_dist.rs @@ -0,0 +1,60 @@ +#![allow(clippy::unwrap_used)] +use crate::test::util::new_empty_model; + +#[test] +fn test_beta_dist_inv() { + let mut model = new_empty_model(); + model._set("A1", "=BETA.DIST(2,8,10,TRUE,1,3)"); + model._set("A2", "=BETA.DIST(2,8,10,FALSE,1,3)"); + model._set("A3", "=BETA.INV(0.685470581,8,10,1,3)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"0.685470581"); + assert_eq!(model._get_text("A2"), *"1.483764648"); + assert_eq!(model._get_text("A3"), *"2"); +} + +#[test] +fn test_gamma_functions() { + let mut model = new_empty_model(); + model._set("A1", "=GAMMA.DIST(10.00001131,9,2,FALSE)"); + model._set("A2", "=GAMMA.DIST(10.00001131,9,2,TRUE)"); + model._set("A3", "=GAMMA.INV(0.068094004,9,2)"); + model._set("A4", "=GAMMA(2.5)"); + model._set("A5", "=GAMMALN.PRECISE(4.5)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"0.03263913"); + assert_eq!(model._get_text("A2"), *"0.068094004"); + assert_eq!(model._get_text("A3"), *"10.000011314"); + assert_eq!(model._get_text("A4"), *"1.329340388"); + assert_eq!(model._get_text("A5"), *"2.453736571"); +} + +#[test] +fn test_expon_weibull_poisson() { + let mut model = new_empty_model(); + model._set("A1", "=EXPON.DIST(0.2,10,TRUE)"); + model._set("A2", "=EXPON.DIST(0.2,10,FALSE)"); + model._set("A3", "=WEIBULL.DIST(105,20,100,TRUE)"); + model._set("A4", "=WEIBULL.DIST(105,20,100,FALSE)"); + model._set("A5", "=POISSON.DIST(2,5,TRUE)"); + model._set("A6", "=POISSON.DIST(2,5,FALSE)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"0.864664717"); + assert_eq!(model._get_text("A2"), *"1.353352832"); + assert_eq!(model._get_text("A3"), *"0.92958139"); + assert_eq!(model._get_text("A4"), *"0.035588864"); + assert_eq!(model._get_text("A5"), *"0.124652019"); + assert_eq!(model._get_text("A6"), *"0.084224337"); +} + +#[test] +fn test_binomial_functions() { + let mut model = new_empty_model(); + model._set("A1", "=BINOM.DIST(6,10,0.5,FALSE)"); + model._set("A2", "=BINOM.INV(6,0.5,0.75)"); + model._set("A3", "=BINOM.DIST.RANGE(60,0.75,45,50)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"0.205078125"); + assert_eq!(model._get_text("A2"), *"4"); + assert_eq!(model._get_text("A3"), *"0.523629793"); +} diff --git a/docs/src/functions/statistical.md b/docs/src/functions/statistical.md index 6842212c3..c53297e6c 100644 --- a/docs/src/functions/statistical.md +++ b/docs/src/functions/statistical.md @@ -16,11 +16,11 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | AVERAGEA | | – | | AVERAGEIF | | – | | AVERAGEIFS | | – | -| BETA.DIST | | – | -| BETA.INV | | – | -| BINOM.DIST | | – | -| BINOM.DIST.RANGE | | – | -| BINOM.INV | | – | +| BETA.DIST | | – | +| BETA.INV | | – | +| BINOM.DIST | | – | +| BINOM.DIST.RANGE | | – | +| BINOM.INV | | – | | CHISQ.DIST | | – | | CHISQ.DIST.RT | | – | | CHISQ.INV | | – | @@ -37,7 +37,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | COVARIANCE.P | | – | | COVARIANCE.S | | – | | DEVSQ | | – | -| EXPON.DIST | | – | +| EXPON.DIST | | – | | F.DIST | | – | | F.DIST.RT | | – | | F.INV | | – | @@ -52,11 +52,11 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | FORECAST.ETS.STAT | | – | | FORECAST.LINEAR | | – | | FREQUENCY | | – | -| GAMMA | | – | -| GAMMA.DIST | | – | -| GAMMA.INV | | – | -| GAMMALN | | – | -| GAMMALN.PRECISE | | – | +| GAMMA | | – | +| GAMMA.DIST | | – | +| GAMMA.INV | | – | +| GAMMALN | | – | +| GAMMALN.PRECISE | | – | | GAUSS | | – | | GEOMEAN | | – | | GROWTH | | – | @@ -88,7 +88,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | PERMUT | | – | | PERMUTATIONA | | – | | PHI | | – | -| POISSON.DIST | | – | +| POISSON.DIST | | – | | PROB | | – | | QUARTILE.EXC | | – | | QUARTILE.INC | | – | @@ -117,5 +117,5 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | VAR.S | | – | | VARA | | – | | VARPA | | – | -| WEIBULL.DIST | | – | +| WEIBULL.DIST | | – | | Z.TEST | | – | diff --git a/docs/src/functions/statistical/beta.dist.md b/docs/src/functions/statistical/beta.dist.md index aa31f2af0..30cc1fce6 100644 --- a/docs/src/functions/statistical/beta.dist.md +++ b/docs/src/functions/statistical/beta.dist.md @@ -7,6 +7,6 @@ lang: en-US # BETA.DIST ::: warning -🚧 This function is not yet available in IronCalc. +🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) ::: \ No newline at end of file diff --git a/docs/src/functions/statistical/beta.inv.md b/docs/src/functions/statistical/beta.inv.md index b08eb16c2..bbe822ec1 100644 --- a/docs/src/functions/statistical/beta.inv.md +++ b/docs/src/functions/statistical/beta.inv.md @@ -7,6 +7,6 @@ lang: en-US # BETA.INV ::: warning -🚧 This function is not yet available in IronCalc. +🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) ::: \ No newline at end of file diff --git a/docs/src/functions/statistical/binom.dist.md b/docs/src/functions/statistical/binom.dist.md index 1374a7a3e..2c359dac8 100644 --- a/docs/src/functions/statistical/binom.dist.md +++ b/docs/src/functions/statistical/binom.dist.md @@ -7,6 +7,6 @@ lang: en-US # BINOM.DIST ::: warning -🚧 This function is not yet available in IronCalc. +🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) ::: \ No newline at end of file diff --git a/docs/src/functions/statistical/binom.dist.range.md b/docs/src/functions/statistical/binom.dist.range.md index 4e43da1ae..fa073861e 100644 --- a/docs/src/functions/statistical/binom.dist.range.md +++ b/docs/src/functions/statistical/binom.dist.range.md @@ -7,6 +7,6 @@ lang: en-US # BINOM.DIST.RANGE ::: warning -🚧 This function is not yet available in IronCalc. +🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) ::: \ No newline at end of file diff --git a/docs/src/functions/statistical/binom.inv.md b/docs/src/functions/statistical/binom.inv.md index b2322988b..e527c0e7a 100644 --- a/docs/src/functions/statistical/binom.inv.md +++ b/docs/src/functions/statistical/binom.inv.md @@ -7,6 +7,6 @@ lang: en-US # BINOM.INV ::: warning -🚧 This function is not yet available in IronCalc. +🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) ::: \ No newline at end of file diff --git a/docs/src/functions/statistical/expon.dist.md b/docs/src/functions/statistical/expon.dist.md index 178f3800b..3e4748436 100644 --- a/docs/src/functions/statistical/expon.dist.md +++ b/docs/src/functions/statistical/expon.dist.md @@ -7,6 +7,6 @@ lang: en-US # EXPON.DIST ::: warning -🚧 This function is not yet available in IronCalc. +🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) ::: \ No newline at end of file diff --git a/docs/src/functions/statistical/gamma.dist.md b/docs/src/functions/statistical/gamma.dist.md index ad9c615b7..cac23b474 100644 --- a/docs/src/functions/statistical/gamma.dist.md +++ b/docs/src/functions/statistical/gamma.dist.md @@ -7,6 +7,6 @@ lang: en-US # GAMMA.DIST ::: warning -🚧 This function is not yet available in IronCalc. +🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) ::: \ No newline at end of file diff --git a/docs/src/functions/statistical/gamma.inv.md b/docs/src/functions/statistical/gamma.inv.md index 7b4092b2e..e05208d4a 100644 --- a/docs/src/functions/statistical/gamma.inv.md +++ b/docs/src/functions/statistical/gamma.inv.md @@ -7,6 +7,6 @@ lang: en-US # GAMMA.INV ::: warning -🚧 This function is not yet available in IronCalc. +🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) ::: \ No newline at end of file diff --git a/docs/src/functions/statistical/gamma.md b/docs/src/functions/statistical/gamma.md index 005ea9fd4..9bbd9f484 100644 --- a/docs/src/functions/statistical/gamma.md +++ b/docs/src/functions/statistical/gamma.md @@ -7,6 +7,6 @@ lang: en-US # GAMMA ::: warning -🚧 This function is not yet available in IronCalc. +🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) ::: \ No newline at end of file diff --git a/docs/src/functions/statistical/gammaln.md b/docs/src/functions/statistical/gammaln.md index 3055d5afe..60293b785 100644 --- a/docs/src/functions/statistical/gammaln.md +++ b/docs/src/functions/statistical/gammaln.md @@ -7,6 +7,6 @@ lang: en-US # GAMMALN ::: warning -🚧 This function is not yet available in IronCalc. +🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) ::: \ No newline at end of file diff --git a/docs/src/functions/statistical/gammaln.precise.md b/docs/src/functions/statistical/gammaln.precise.md index 147820889..1618b9212 100644 --- a/docs/src/functions/statistical/gammaln.precise.md +++ b/docs/src/functions/statistical/gammaln.precise.md @@ -7,6 +7,6 @@ lang: en-US # GAMMALN.PRECISE ::: warning -🚧 This function is not yet available in IronCalc. +🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) ::: \ No newline at end of file diff --git a/docs/src/functions/statistical/poisson.dist.md b/docs/src/functions/statistical/poisson.dist.md index 2e463f204..ebbdc193b 100644 --- a/docs/src/functions/statistical/poisson.dist.md +++ b/docs/src/functions/statistical/poisson.dist.md @@ -7,6 +7,6 @@ lang: en-US # POISSON.DIST ::: warning -🚧 This function is not yet available in IronCalc. +🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) ::: \ No newline at end of file diff --git a/docs/src/functions/statistical/weibull.dist.md b/docs/src/functions/statistical/weibull.dist.md index 38bbbc1df..dde598c12 100644 --- a/docs/src/functions/statistical/weibull.dist.md +++ b/docs/src/functions/statistical/weibull.dist.md @@ -7,6 +7,6 @@ lang: en-US # WEIBULL.DIST ::: warning -🚧 This function is not yet available in IronCalc. +🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) ::: \ No newline at end of file From 816873ec4cd8115e928e43a70d98758615af05b0 Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Fri, 1 Aug 2025 14:36:27 -0700 Subject: [PATCH 2/5] fmt --- base/Cargo.toml | 2 +- base/src/functions/statistical.rs | 131 ++++++++++++++----------- base/src/test/mod.rs | 2 +- base/src/test/test_statistical_dist.rs | 12 ++- 4 files changed, 84 insertions(+), 63 deletions(-) diff --git a/base/Cargo.toml b/base/Cargo.toml index 63e5fa9f4..0ce85bd68 100644 --- a/base/Cargo.toml +++ b/base/Cargo.toml @@ -19,7 +19,7 @@ regex = { version = "1.0", optional = true} regex-lite = { version = "0.1.6", optional = true} bitcode = "0.6.3" csv = "1.3.0" -statrs = "0.18" +statrs = { version = "0.18", default-features = false } [features] default = ["use_regex_full"] diff --git a/base/src/functions/statistical.rs b/base/src/functions/statistical.rs index edac6c8d9..7dd155f9c 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -8,6 +8,10 @@ use crate::{ }; use super::util::build_criteria; +use statrs::distribution::{ + Beta, Binomial, Continuous, ContinuousCDF, Discrete, DiscreteCDF, Exp, Gamma, Poisson, Weibull, +}; +use statrs::function::gamma::{gamma, ln_gamma}; impl Model { pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { @@ -732,8 +736,6 @@ impl Model { } pub(crate) fn fn_beta_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - use statrs::distribution::{Beta, Continuous, ContinuousCDF}; - if args.len() < 4 || args.len() > 6 { return CalcResult::new_args_number_error(cell); } @@ -772,12 +774,14 @@ impl Model { }; if alpha <= 0.0 || beta <= 0.0 || b <= a || x < a || x > b { - return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } let dist = match Beta::new(alpha, beta) { Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) + } }; let z = (x - a) / (b - a); @@ -789,8 +793,6 @@ impl Model { } pub(crate) fn fn_beta_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - use statrs::distribution::{Beta, ContinuousCDF}; - if args.len() < 3 || args.len() > 5 { return CalcResult::new_args_number_error(cell); } @@ -825,12 +827,14 @@ impl Model { }; if p < 0.0 || p > 1.0 || alpha <= 0.0 || beta <= 0.0 || b <= a { - return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } let dist = match Beta::new(alpha, beta) { Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) + } }; let res = dist.inverse_cdf(p) * (b - a) + a; @@ -838,8 +842,6 @@ impl Model { } pub(crate) fn fn_gamma_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - use statrs::distribution::{Continuous, ContinuousCDF, Gamma}; - if args.len() != 4 { return CalcResult::new_args_number_error(cell); } @@ -862,12 +864,14 @@ impl Model { }; if x < 0.0 || alpha <= 0.0 || beta <= 0.0 { - return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } let dist = match Gamma::new(alpha, 1.0 / beta) { Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) + } }; if cumulative { @@ -878,8 +882,6 @@ impl Model { } pub(crate) fn fn_gamma_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - use statrs::distribution::{Gamma, ContinuousCDF}; - if args.len() != 3 { return CalcResult::new_args_number_error(cell); } @@ -898,20 +900,20 @@ impl Model { }; if p < 0.0 || p > 1.0 || alpha <= 0.0 || beta <= 0.0 { - return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } let dist = match Gamma::new(alpha, 1.0 / beta) { Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) + } }; CalcResult::Number(dist.inverse_cdf(p)) } pub(crate) fn fn_gamma(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - use statrs::function::gamma::gamma; - if args.len() != 1 { return CalcResult::new_args_number_error(cell); } @@ -922,15 +924,13 @@ impl Model { }; if x == 0.0 || (x < 0.0 && (x.fract() == 0.0)) { - return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } CalcResult::Number(gamma(x)) } pub(crate) fn fn_gammaln(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - use statrs::function::gamma::ln_gamma; - if args.len() != 1 { return CalcResult::new_args_number_error(cell); } @@ -939,18 +939,20 @@ impl Model { Err(e) => return e, }; if x <= 0.0 { - return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } CalcResult::Number(ln_gamma(x)) } - pub(crate) fn fn_gammaln_precise(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + pub(crate) fn fn_gammaln_precise( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + ) -> CalcResult { self.fn_gammaln(args, cell) } pub(crate) fn fn_expon_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - use statrs::distribution::{Exp, Continuous, ContinuousCDF}; - if args.len() != 3 { return CalcResult::new_args_number_error(cell); } @@ -968,12 +970,14 @@ impl Model { }; if x < 0.0 || lambda <= 0.0 { - return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } let dist = match Exp::new(lambda) { Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) + } }; if cumulative { CalcResult::Number(dist.cdf(x)) @@ -982,9 +986,11 @@ impl Model { } } - pub(crate) fn fn_weibull_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - use statrs::distribution::{Continuous, ContinuousCDF, Weibull}; - + pub(crate) fn fn_weibull_dist( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + ) -> CalcResult { if args.len() != 4 { return CalcResult::new_args_number_error(cell); } @@ -1005,11 +1011,13 @@ impl Model { Err(e) => return e, }; if x < 0.0 || alpha <= 0.0 || beta <= 0.0 { - return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } let dist = match Weibull::new(alpha, beta) { Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) + } }; if cumulative { CalcResult::Number(dist.cdf(x)) @@ -1018,9 +1026,11 @@ impl Model { } } - pub(crate) fn fn_poisson_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - use statrs::distribution::{Poisson, DiscreteCDF, Discrete}; - + pub(crate) fn fn_poisson_dist( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + ) -> CalcResult { if args.len() != 3 { return CalcResult::new_args_number_error(cell); } @@ -1037,11 +1047,13 @@ impl Model { Err(e) => return e, }; if x < 0.0 || mean <= 0.0 { - return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } let dist = match Poisson::new(mean) { Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) + } }; let k = x.floor() as u64; if cumulative { @@ -1052,8 +1064,6 @@ impl Model { } pub(crate) fn fn_binom_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - use statrs::distribution::{Binomial, DiscreteCDF, Discrete}; - if args.len() != 4 { return CalcResult::new_args_number_error(cell); } @@ -1075,11 +1085,13 @@ impl Model { }; if trials < 0.0 || p < 0.0 || p > 1.0 || number_s < 0.0 || number_s > trials { - return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } let dist = match Binomial::new(p, trials.round() as u64) { Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) + } }; let k = number_s.floor() as u64; if cumulative { @@ -1090,8 +1102,6 @@ impl Model { } pub(crate) fn fn_binom_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - use statrs::distribution::{Binomial, DiscreteCDF}; - if args.len() != 3 { return CalcResult::new_args_number_error(cell); } @@ -1109,19 +1119,23 @@ impl Model { }; if trials < 0.0 || p < 0.0 || p > 1.0 || alpha < 0.0 || alpha > 1.0 { - return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } let dist = match Binomial::new(p, trials.round() as u64) { Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) + } }; let result = dist.inverse_cdf(alpha) as f64; CalcResult::Number(result) } - pub(crate) fn fn_binom_dist_range(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - use statrs::distribution::{Binomial, Discrete}; - + pub(crate) fn fn_binom_dist_range( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + ) -> CalcResult { if !(3..=4).contains(&args.len()) { return CalcResult::new_args_number_error(cell); } @@ -1147,21 +1161,24 @@ impl Model { s1 }; - if trials < 0.0 || p < 0.0 || p > 1.0 || s1 < 0.0 || s2 < s1 { - return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()); + if trials < 0.0 || p < 0.0 || p > 1.0 || s1 < 0.0 || s2 < s1 || s2 > trials { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } let dist = match Binomial::new(p, trials.round() as u64) { Ok(d) => d, - Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid parameter".to_string()), + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) + } }; - let mut prob = 0.0; - let start = s1.floor() as u64; - let end = s2.floor() as u64; - for k in start..=end { - prob += dist.pmf(k); + // Use CDF-based calculation for better numerical stability + let k1 = s1.floor() as u64; + let k2 = s2.floor() as u64; + let mut result = dist.cdf(k2); + if k1 > 0 { + result -= dist.cdf(k1 - 1); } - CalcResult::Number(prob) + CalcResult::Number(result) } } diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index b616555c1..d00d85cd5 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -66,7 +66,7 @@ mod test_log; mod test_log10; mod test_percentage; mod test_set_functions_error_handling; +mod test_statistical_dist; mod test_today; mod test_types; -mod test_statistical_dist; mod user_model; diff --git a/base/src/test/test_statistical_dist.rs b/base/src/test/test_statistical_dist.rs index cbc5473bb..c04504927 100644 --- a/base/src/test/test_statistical_dist.rs +++ b/base/src/test/test_statistical_dist.rs @@ -51,10 +51,14 @@ fn test_expon_weibull_poisson() { fn test_binomial_functions() { let mut model = new_empty_model(); model._set("A1", "=BINOM.DIST(6,10,0.5,FALSE)"); - model._set("A2", "=BINOM.INV(6,0.5,0.75)"); - model._set("A3", "=BINOM.DIST.RANGE(60,0.75,45,50)"); + model._set("A2", "=BINOM.DIST(6,10,0.5,TRUE)"); + model._set("A3", "=BINOM.INV(6,0.5,0.75)"); + model._set("A4", "=BINOM.DIST.RANGE(60,0.75,48)"); + model._set("A5", "=BINOM.DIST.RANGE(60,0.75,45,50)"); model.evaluate(); assert_eq!(model._get_text("A1"), *"0.205078125"); - assert_eq!(model._get_text("A2"), *"4"); - assert_eq!(model._get_text("A3"), *"0.523629793"); + assert_eq!(model._get_text("A2"), *"0.828125"); + assert_eq!(model._get_text("A3"), *"4"); + assert_eq!(model._get_text("A4"), *"0.083974967"); + assert_eq!(model._get_text("A5"), *"0.523629793"); } From ef2f015f9b45e34f35347d386b00d4ddd5d4f04a Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Fri, 1 Aug 2025 14:38:00 -0700 Subject: [PATCH 3/5] cargo lock --- Cargo.lock | 129 ----------------------------------------------------- 1 file changed, 129 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc3c6bee7..be087de38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -515,28 +515,12 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" -[[package]] -name = "matrixmultiply" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" -dependencies = [ - "autocfg", - "rawpointer", -] - [[package]] name = "memchr" version = "2.7.2" @@ -561,23 +545,6 @@ dependencies = [ "adler", ] -[[package]] -name = "nalgebra" -version = "0.33.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b" -dependencies = [ - "approx", - "matrixmultiply", - "num-complex", - "num-rational", - "num-traits", - "rand", - "rand_distr", - "simba", - "typenum", -] - [[package]] name = "napi" version = "2.16.13" @@ -637,51 +604,12 @@ dependencies = [ "libloading", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.18" @@ -689,7 +617,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -718,12 +645,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pbkdf2" version = "0.11.0" @@ -919,22 +840,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand", -] - -[[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - [[package]] name = "regex" version = "1.10.4" @@ -988,15 +893,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" -[[package]] -name = "safe_arch" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" -dependencies = [ - "bytemuck", -] - [[package]] name = "scoped-tls" version = "1.0.1" @@ -1073,19 +969,6 @@ dependencies = [ "digest", ] -[[package]] -name = "simba" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa" -dependencies = [ - "approx", - "num-complex", - "num-traits", - "paste", - "wide", -] - [[package]] name = "siphasher" version = "0.3.11" @@ -1099,9 +982,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" dependencies = [ "approx", - "nalgebra", "num-traits", - "rand", ] [[package]] @@ -1328,16 +1209,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "wide" -version = "0.7.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" -dependencies = [ - "bytemuck", - "safe_arch", -] - [[package]] name = "windows-core" version = "0.52.0" From 02b0f9f5b5024704b2f23bcf4327836814bacd99 Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Sat, 2 Aug 2025 00:21:14 -0700 Subject: [PATCH 4/5] fix build --- base/src/functions/statistical.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/base/src/functions/statistical.rs b/base/src/functions/statistical.rs index 7dd155f9c..6f30edb98 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -826,7 +826,7 @@ impl Model { 1.0 }; - if p < 0.0 || p > 1.0 || alpha <= 0.0 || beta <= 0.0 || b <= a { + if !(0.0..=1.0).contains(&p) || alpha <= 0.0 || beta <= 0.0 || b <= a { return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } @@ -899,7 +899,7 @@ impl Model { Err(e) => return e, }; - if p < 0.0 || p > 1.0 || alpha <= 0.0 || beta <= 0.0 { + if !(0.0..=1.0).contains(&p) || alpha <= 0.0 || beta <= 0.0 { return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } @@ -1084,7 +1084,7 @@ impl Model { Err(e) => return e, }; - if trials < 0.0 || p < 0.0 || p > 1.0 || number_s < 0.0 || number_s > trials { + if trials < 0.0 || !(0.0..=1.0).contains(&p) || number_s < 0.0 || number_s > trials { return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } let dist = match Binomial::new(p, trials.round() as u64) { @@ -1118,7 +1118,7 @@ impl Model { Err(e) => return e, }; - if trials < 0.0 || p < 0.0 || p > 1.0 || alpha < 0.0 || alpha > 1.0 { + if trials < 0.0 || !(0.0..=1.0).contains(&p) || !(0.0..=1.0).contains(&alpha) { return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } let dist = match Binomial::new(p, trials.round() as u64) { @@ -1161,7 +1161,7 @@ impl Model { s1 }; - if trials < 0.0 || p < 0.0 || p > 1.0 || s1 < 0.0 || s2 < s1 || s2 > trials { + if trials < 0.0 || !(0.0..=1.0).contains(&p) || s1 < 0.0 || s2 < s1 || s2 > trials { return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } From 61875660d1c4d4dec27983c870b0a63059d13257 Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Sat, 2 Aug 2025 01:43:13 -0700 Subject: [PATCH 5/5] safe floating to uint --- base/src/functions/statistical.rs | 80 +++++++++++++++++++++++--- base/src/test/test_statistical_dist.rs | 37 ++++++++++++ 2 files changed, 110 insertions(+), 7 deletions(-) diff --git a/base/src/functions/statistical.rs b/base/src/functions/statistical.rs index 6f30edb98..5885755f5 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -13,6 +13,48 @@ use statrs::distribution::{ }; use statrs::function::gamma::{gamma, ln_gamma}; +/// Safely converts an f64 to u64, returning an error for invalid values +fn safe_f64_to_u64(value: f64, cell: CellReferenceIndex) -> Result { + // Check for NaN + if value.is_nan() { + return Err(CalcResult::new_error( + Error::NUM, + cell, + "Invalid number (NaN)".to_string(), + )); + } + + // Check for infinity + if value.is_infinite() { + return Err(CalcResult::new_error( + Error::NUM, + cell, + "Invalid number (infinity)".to_string(), + )); + } + + // Check for negative values + if value < 0.0 { + return Err(CalcResult::new_error( + Error::NUM, + cell, + "Number must be non-negative".to_string(), + )); + } + + // Check if value exceeds u64::MAX + if value > u64::MAX as f64 { + return Err(CalcResult::new_error( + Error::NUM, + cell, + "Number too large".to_string(), + )); + } + + // Safe conversion + Ok(value.floor() as u64) +} + impl Model { pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { if args.is_empty() { @@ -1055,7 +1097,10 @@ impl Model { return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) } }; - let k = x.floor() as u64; + let k = match safe_f64_to_u64(x, cell) { + Ok(val) => val, + Err(e) => return e, + }; if cumulative { CalcResult::Number(dist.cdf(k)) } else { @@ -1087,13 +1132,20 @@ impl Model { if trials < 0.0 || !(0.0..=1.0).contains(&p) || number_s < 0.0 || number_s > trials { return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } - let dist = match Binomial::new(p, trials.round() as u64) { + let trials_u64 = match safe_f64_to_u64(trials.round(), cell) { + Ok(val) => val, + Err(e) => return e, + }; + let dist = match Binomial::new(p, trials_u64) { Ok(d) => d, Err(_) => { return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) } }; - let k = number_s.floor() as u64; + let k = match safe_f64_to_u64(number_s, cell) { + Ok(val) => val, + Err(e) => return e, + }; if cumulative { CalcResult::Number(dist.cdf(k)) } else { @@ -1121,7 +1173,11 @@ impl Model { if trials < 0.0 || !(0.0..=1.0).contains(&p) || !(0.0..=1.0).contains(&alpha) { return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } - let dist = match Binomial::new(p, trials.round() as u64) { + let trials_u64 = match safe_f64_to_u64(trials.round(), cell) { + Ok(val) => val, + Err(e) => return e, + }; + let dist = match Binomial::new(p, trials_u64) { Ok(d) => d, Err(_) => { return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) @@ -1165,7 +1221,11 @@ impl Model { return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); } - let dist = match Binomial::new(p, trials.round() as u64) { + let trials_u64 = match safe_f64_to_u64(trials.round(), cell) { + Ok(val) => val, + Err(e) => return e, + }; + let dist = match Binomial::new(p, trials_u64) { Ok(d) => d, Err(_) => { return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) @@ -1173,8 +1233,14 @@ impl Model { }; // Use CDF-based calculation for better numerical stability - let k1 = s1.floor() as u64; - let k2 = s2.floor() as u64; + let k1 = match safe_f64_to_u64(s1, cell) { + Ok(val) => val, + Err(e) => return e, + }; + let k2 = match safe_f64_to_u64(s2, cell) { + Ok(val) => val, + Err(e) => return e, + }; let mut result = dist.cdf(k2); if k1 > 0 { result -= dist.cdf(k1 - 1); diff --git a/base/src/test/test_statistical_dist.rs b/base/src/test/test_statistical_dist.rs index c04504927..493b098f8 100644 --- a/base/src/test/test_statistical_dist.rs +++ b/base/src/test/test_statistical_dist.rs @@ -62,3 +62,40 @@ fn test_binomial_functions() { assert_eq!(model._get_text("A4"), *"0.083974967"); assert_eq!(model._get_text("A5"), *"0.523629793"); } + +#[test] +fn test_statistical_functions_edge_cases() { + let mut model = new_empty_model(); + + // Test cases that would previously cause panics due to unsafe f64 to u64 conversions + + // Test negative values (should return NUM error, not panic) + model._set("A1", "=POISSON.DIST(-1,5,TRUE)"); + model._set("A2", "=BINOM.DIST(-1,10,0.5,FALSE)"); + model._set("A3", "=BINOM.DIST.RANGE(10,0.5,-1)"); + + // Test very large values exceeding u64::MAX + model._set("B1", "=POISSON.DIST(1E20,5,TRUE)"); + model._set("B2", "=BINOM.DIST(1E20,10,0.5,FALSE)"); + model._set("B3", "=BINOM.DIST.RANGE(1E20,0.5,1)"); + + // Test large trial values that exceed u64::MAX + model._set("E1", "=BINOM.DIST(5,1E20,0.5,FALSE)"); + model._set("E2", "=BINOM.INV(1E20,0.5,0.5)"); + model._set("E3", "=BINOM.DIST.RANGE(1E20,0.5,1,2)"); + + model.evaluate(); + + // All of these should return #NUM! error instead of panicking + assert!(model._get_text("A1").contains("#NUM!")); + assert!(model._get_text("A2").contains("#NUM!")); + assert!(model._get_text("A3").contains("#NUM!")); + + assert!(model._get_text("B1").contains("#NUM!")); + assert!(model._get_text("B2").contains("#NUM!")); + assert!(model._get_text("B3").contains("#NUM!")); + + assert!(model._get_text("E1").contains("#NUM!")); + assert!(model._get_text("E2").contains("#NUM!")); + assert!(model._get_text("E3").contains("#NUM!")); +}