diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 80f194360..ae96662fe 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -785,6 +785,8 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec args_signature_scalars(arg_count, 1, 0), Function::Unicode => args_signature_scalars(arg_count, 1, 0), Function::Geomean => vec![Signature::Vector; arg_count], + Function::Gauss | Function::Phi => args_signature_scalars(arg_count, 1, 0), + Function::Standardize => args_signature_scalars(arg_count, 3, 0), } } @@ -990,5 +992,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Eomonth => scalar_arguments(args), Function::Formulatext => not_implemented(args), Function::Geomean => not_implemented(args), + Function::Gauss | Function::Phi | Function::Standardize => scalar_arguments(args), } } diff --git a/base/src/functions/engineering/mod.rs b/base/src/functions/engineering/mod.rs index 1f549cfaa..ad027c9be 100644 --- a/base/src/functions/engineering/mod.rs +++ b/base/src/functions/engineering/mod.rs @@ -4,4 +4,4 @@ mod complex; mod convert; mod misc; mod number_basis; -mod transcendental; +pub(crate) mod transcendental; diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 21c8f72da..35321abd7 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -145,6 +145,9 @@ pub enum Function { Maxifs, Minifs, Geomean, + Gauss, + Phi, + Standardize, // Date and time Date, @@ -253,7 +256,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -357,6 +360,9 @@ impl Function { Function::Maxifs, Function::Minifs, Function::Geomean, + Function::Gauss, + Function::Phi, + Function::Standardize, Function::Year, Function::Day, Function::Month, @@ -625,6 +631,9 @@ impl Function { "MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs), "MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs), "GEOMEAN" => Some(Function::Geomean), + "GAUSS" => Some(Function::Gauss), + "PHI" => Some(Function::Phi), + "STANDARDIZE" => Some(Function::Standardize), // Date and Time "YEAR" => Some(Function::Year), "DAY" => Some(Function::Day), @@ -836,6 +845,9 @@ impl fmt::Display for Function { Function::Maxifs => write!(f, "MAXIFS"), Function::Minifs => write!(f, "MINIFS"), Function::Geomean => write!(f, "GEOMEAN"), + Function::Gauss => write!(f, "GAUSS"), + Function::Phi => write!(f, "PHI"), + Function::Standardize => write!(f, "STANDARDIZE"), Function::Year => write!(f, "YEAR"), Function::Day => write!(f, "DAY"), Function::Month => write!(f, "MONTH"), @@ -1076,6 +1088,9 @@ impl Model { Function::Maxifs => self.fn_maxifs(args, cell), Function::Minifs => self.fn_minifs(args, cell), Function::Geomean => self.fn_geomean(args, cell), + Function::Gauss => self.fn_gauss(args, cell), + Function::Phi => self.fn_phi(args, cell), + Function::Standardize => self.fn_standardize(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..9ac4bfad6 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -7,7 +7,9 @@ use crate::{ model::Model, }; +use super::engineering::transcendental::erf; use super::util::build_criteria; +use std::f64::consts::PI; impl Model { pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { @@ -730,4 +732,72 @@ impl Model { } CalcResult::Number(product.powf(1.0 / count)) } + + pub(crate) fn fn_gauss(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 1 { + return CalcResult::new_args_number_error(cell); + } + let z = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let result = 0.5 * erf(z / std::f64::consts::SQRT_2); + if result.is_nan() || result.is_infinite() { + return CalcResult::new_error( + Error::NUM, + cell, + "Invalid parameter for GAUSS function".to_string(), + ); + } + CalcResult::Number(result) + } + + pub(crate) fn fn_phi(&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(s) => return s, + }; + let result = (1.0 / (2.0 * PI).sqrt()) * f64::exp(-0.5 * x * x); + if result.is_nan() || result.is_infinite() { + return CalcResult::new_error( + Error::NUM, + cell, + "Invalid parameter for PHI function".to_string(), + ); + } + CalcResult::Number(result) + } + + pub(crate) fn fn_standardize(&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(s) => return s, + }; + let mean = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let std_dev = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + if std_dev <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "standard_dev must be > 0".to_string()); + } + let result = (x - mean) / std_dev; + if result.is_nan() || result.is_infinite() { + return CalcResult::new_error( + Error::NUM, + cell, + "Invalid parameter for STANDARDIZE function".to_string(), + ); + } + CalcResult::Number(result) + } } diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index a0a0d69d6..ebc1d0dea 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -64,6 +64,7 @@ mod test_issue_155; mod test_ln; mod test_log; mod test_log10; +mod test_normal_distribution; mod test_percentage; mod test_set_functions_error_handling; mod test_today; diff --git a/base/src/test/test_normal_distribution.rs b/base/src/test/test_normal_distribution.rs new file mode 100644 index 000000000..61a941964 --- /dev/null +++ b/base/src/test/test_normal_distribution.rs @@ -0,0 +1,119 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_gauss() { + let mut model = new_empty_model(); + model._set("A1", "=GAUSS(2)"); + model._set("A2", "=GAUSS(0)"); + model._set("A3", "=GAUSS(-1)"); + model.evaluate(); + let a1: f64 = model._get_text("A1").parse().unwrap(); + let a2: f64 = model._get_text("A2").parse().unwrap(); + let a3: f64 = model._get_text("A3").parse().unwrap(); + assert!((a1 - 0.477249868).abs() < 1e-9); + assert!(a2.abs() < 1e-12); + assert!((a3 - (-0.341344746)).abs() < 1e-9); +} + +#[test] +fn test_phi() { + let mut model = new_empty_model(); + model._set("A1", "=PHI(0)"); + model._set("A2", "=PHI(1)"); + model._set("A3", "=PHI(-1)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"0.39894228"); + assert_eq!(model._get_text("A2"), *"0.241970725"); + assert_eq!(model._get_text("A3"), *"0.241970725"); // PHI is symmetric around 0 +} + +#[test] +fn test_standardize() { + let mut model = new_empty_model(); + model._set("A1", "=STANDARDIZE(75,70,5)"); + model._set("A2", "=STANDARDIZE(65,70,5)"); + model._set("A3", "=STANDARDIZE(70,70,5)"); + model._set("A4", "=STANDARDIZE(80,70,10)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"1"); + assert_eq!(model._get_text("A2"), *"-1"); + assert_eq!(model._get_text("A3"), *"0"); + assert_eq!(model._get_text("A4"), *"1"); +} + +#[test] +fn test_gauss_phi_standardize_errors() { + let mut model = new_empty_model(); + + // Test wrong argument counts + model._set("B1", "=GAUSS()"); // Too few arguments + model._set("B2", "=GAUSS(1,2)"); // Too many arguments + model._set("B3", "=PHI()"); // Too few arguments + model._set("B4", "=PHI(1,2)"); // Too many arguments + model._set("B5", "=STANDARDIZE(1,2)"); // Too few arguments + model._set("B6", "=STANDARDIZE(1,2,3,4)"); // Too many arguments + + // Test invalid standard deviation + model._set("B7", "=STANDARDIZE(1,2,0)"); // Zero std dev + model._set("B8", "=STANDARDIZE(1,2,-1)"); // Negative std dev + + model.evaluate(); + + // All should return errors + assert_eq!(model._get_text("B1"), *"#ERROR!"); + assert_eq!(model._get_text("B2"), *"#ERROR!"); + assert_eq!(model._get_text("B3"), *"#ERROR!"); + assert_eq!(model._get_text("B4"), *"#ERROR!"); + assert_eq!(model._get_text("B5"), *"#ERROR!"); + assert_eq!(model._get_text("B6"), *"#ERROR!"); + assert_eq!(model._get_text("B7"), *"#NUM!"); // Should be NUM error for invalid std dev + assert_eq!(model._get_text("B8"), *"#NUM!"); // Should be NUM error for invalid std dev +} + +#[test] +fn test_extreme_values() { + let mut model = new_empty_model(); + + // Test with very large values that shouldn't cause NaN/infinity + model._set("C1", "=GAUSS(100)"); // Very large positive value + model._set("C2", "=GAUSS(-100)"); // Very large negative value + model._set("C3", "=PHI(10)"); // Should be very small but valid + model._set("C4", "=PHI(-10)"); // Should be very small but valid + + model.evaluate(); + + // These should all produce valid numbers, not errors + assert_ne!(model._get_text("C1"), *"#NUM!"); + assert_ne!(model._get_text("C2"), *"#NUM!"); + assert_ne!(model._get_text("C3"), *"#NUM!"); + assert_ne!(model._get_text("C4"), *"#NUM!"); + + // Verify GAUSS approaches limits correctly + let c1: f64 = model._get_text("C1").parse().unwrap(); + let c2: f64 = model._get_text("C2").parse().unwrap(); + assert!(c1 > 0.49); // Should approach 0.5 for large positive values + assert!(c2 < -0.49); // Should approach -0.5 for large negative values +} + +#[test] +fn test_type_errors() { + let mut model = new_empty_model(); + + // Test with text inputs + model._set("D1", "=GAUSS(\"text\")"); + model._set("D2", "=PHI(\"text\")"); + model._set("D3", "=STANDARDIZE(\"text\",70,5)"); + model._set("D4", "=STANDARDIZE(75,\"text\",5)"); + model._set("D5", "=STANDARDIZE(75,70,\"text\")"); + + model.evaluate(); + + // All should return VALUE errors + assert_eq!(model._get_text("D1"), *"#VALUE!"); + assert_eq!(model._get_text("D2"), *"#VALUE!"); + assert_eq!(model._get_text("D3"), *"#VALUE!"); + assert_eq!(model._get_text("D4"), *"#VALUE!"); + assert_eq!(model._get_text("D5"), *"#VALUE!"); +} diff --git a/docs/src/functions/statistical.md b/docs/src/functions/statistical.md index 6842212c3..cc14d684a 100644 --- a/docs/src/functions/statistical.md +++ b/docs/src/functions/statistical.md @@ -57,7 +57,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | GAMMA.INV | | – | | GAMMALN | | – | | GAMMALN.PRECISE | | – | -| GAUSS | | – | +| GAUSS | | – | | GEOMEAN | | – | | GROWTH | | – | | HARMEAN | | – | @@ -87,7 +87,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | PERCENTRANK.INC | | – | | PERMUT | | – | | PERMUTATIONA | | – | -| PHI | | – | +| PHI | | – | | POISSON.DIST | | – | | PROB | | – | | QUARTILE.EXC | | – | @@ -99,7 +99,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | SKEW.P | | – | | SLOPE | | – | | SMALL | | – | -| STANDARDIZE | | – | +| STANDARDIZE | | – | | STDEV.P | | – | | STDEV.S | | – | | STDEVA | | – | diff --git a/docs/src/functions/statistical/gauss.md b/docs/src/functions/statistical/gauss.md index f6445a985..64bfd654c 100644 --- a/docs/src/functions/statistical/gauss.md +++ b/docs/src/functions/statistical/gauss.md @@ -7,6 +7,5 @@ lang: en-US # GAUSS ::: warning -🚧 This function is not yet available in IronCalc. -[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) +🚧 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). ::: \ No newline at end of file diff --git a/docs/src/functions/statistical/phi.md b/docs/src/functions/statistical/phi.md index 15e410ccd..5d88ecce9 100644 --- a/docs/src/functions/statistical/phi.md +++ b/docs/src/functions/statistical/phi.md @@ -7,6 +7,5 @@ lang: en-US # PHI ::: warning -🚧 This function is not yet available in IronCalc. -[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) +🚧 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). ::: \ No newline at end of file diff --git a/docs/src/functions/statistical/standardize.md b/docs/src/functions/statistical/standardize.md index b1e5b1dad..56607fa87 100644 --- a/docs/src/functions/statistical/standardize.md +++ b/docs/src/functions/statistical/standardize.md @@ -7,6 +7,5 @@ lang: en-US # STANDARDIZE ::: warning -🚧 This function is not yet available in IronCalc. -[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) +🚧 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). ::: \ No newline at end of file