diff --git a/Cargo.lock b/Cargo.lock index 677cbf79f..be087de38 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]] @@ -965,6 +975,16 @@ 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", + "num-traits", +] + [[package]] name = "subtle" version = "2.5.0" diff --git a/base/Cargo.toml b/base/Cargo.toml index cb97b34b9..0ce85bd68 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 = { version = "0.18", default-features = false } [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..5885755f5 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -8,6 +8,52 @@ 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}; + +/// 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 { @@ -730,4 +776,475 @@ impl Model { } CalcResult::Number(product.powf(1.0 / count)) } + + pub(crate) fn fn_beta_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + 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 parameters".to_string()); + } + + let dist = match Beta::new(alpha, beta) { + Ok(d) => d, + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".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 { + 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 !(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()); + } + + let dist = match Beta::new(alpha, beta) { + Ok(d) => d, + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".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 { + 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 parameters".to_string()); + } + + let dist = match Gamma::new(alpha, 1.0 / beta) { + Ok(d) => d, + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".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 { + 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 !(0.0..=1.0).contains(&p) || alpha <= 0.0 || beta <= 0.0 { + 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 parameters".to_string()) + } + }; + + CalcResult::Number(dist.inverse_cdf(p)) + } + + pub(crate) fn fn_gamma(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + 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 parameters".to_string()); + } + + CalcResult::Number(gamma(x)) + } + + pub(crate) fn fn_gammaln(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + 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 parameters".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 { + 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 parameters".to_string()); + } + + let dist = match Exp::new(lambda) { + Ok(d) => d, + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".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 { + 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 parameters".to_string()); + } + let dist = match Weibull::new(alpha, beta) { + Ok(d) => d, + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".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 { + 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 parameters".to_string()); + } + let dist = match Poisson::new(mean) { + Ok(d) => d, + Err(_) => { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()) + } + }; + let k = match safe_f64_to_u64(x, cell) { + Ok(val) => val, + Err(e) => return e, + }; + 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 { + 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 || !(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 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 = match safe_f64_to_u64(number_s, cell) { + Ok(val) => val, + Err(e) => return e, + }; + 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 { + 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 || !(0.0..=1.0).contains(&p) || !(0.0..=1.0).contains(&alpha) { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); + } + 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 result = dist.inverse_cdf(alpha) as f64; + CalcResult::Number(result) + } + + 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); + } + + 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 || !(0.0..=1.0).contains(&p) || s1 < 0.0 || s2 < s1 || s2 > trials { + return CalcResult::new_error(Error::NUM, cell, "Invalid parameters".to_string()); + } + + 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()) + } + }; + + // Use CDF-based calculation for better numerical stability + 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); + } + CalcResult::Number(result) + } } diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index a0a0d69d6..d00d85cd5 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -66,6 +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 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..493b098f8 --- /dev/null +++ b/base/src/test/test_statistical_dist.rs @@ -0,0 +1,101 @@ +#![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.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"), *"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"); +} + +#[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!")); +} 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