diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 80f194360..582b4e57b 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -785,6 +785,10 @@ 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::Stdeva => vec![Signature::Vector; arg_count], + Function::Stdevpa => vec![Signature::Vector; arg_count], + Function::Vara => vec![Signature::Vector; arg_count], + Function::Varpa => vec![Signature::Vector; arg_count], } } @@ -990,5 +994,9 @@ 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::Stdeva => not_implemented(args), + Function::Stdevpa => not_implemented(args), + Function::Vara => not_implemented(args), + Function::Varpa => not_implemented(args), } } diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 21c8f72da..a9ce68426 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -145,6 +145,10 @@ pub enum Function { Maxifs, Minifs, Geomean, + Stdeva, + Stdevpa, + Vara, + Varpa, // Date and time Date, @@ -253,7 +257,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -357,6 +361,10 @@ impl Function { Function::Maxifs, Function::Minifs, Function::Geomean, + Function::Stdeva, + Function::Stdevpa, + Function::Vara, + Function::Varpa, Function::Year, Function::Day, Function::Month, @@ -625,6 +633,10 @@ impl Function { "MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs), "MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs), "GEOMEAN" => Some(Function::Geomean), + "STDEVA" => Some(Function::Stdeva), + "STDEVPA" => Some(Function::Stdevpa), + "VARA" => Some(Function::Vara), + "VARPA" => Some(Function::Varpa), // Date and Time "YEAR" => Some(Function::Year), "DAY" => Some(Function::Day), @@ -836,6 +848,10 @@ impl fmt::Display for Function { Function::Maxifs => write!(f, "MAXIFS"), Function::Minifs => write!(f, "MINIFS"), Function::Geomean => write!(f, "GEOMEAN"), + Function::Stdeva => write!(f, "STDEVA"), + Function::Stdevpa => write!(f, "STDEVPA"), + Function::Vara => write!(f, "VARA"), + Function::Varpa => write!(f, "VARPA"), Function::Year => write!(f, "YEAR"), Function::Day => write!(f, "DAY"), Function::Month => write!(f, "MONTH"), @@ -1076,6 +1092,10 @@ impl Model { Function::Maxifs => self.fn_maxifs(args, cell), Function::Minifs => self.fn_minifs(args, cell), Function::Geomean => self.fn_geomean(args, cell), + Function::Stdeva => self.fn_stdeva(args, cell), + Function::Stdevpa => self.fn_stdevpa(args, cell), + Function::Vara => self.fn_vara(args, cell), + Function::Varpa => self.fn_varpa(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..c6267f8d1 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -730,4 +730,185 @@ impl Model { } CalcResult::Number(product.powf(1.0 / count)) } + + fn get_a_values( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + ) -> Result, CalcResult> { + let mut values = Vec::new(); + for arg in args { + match self.evaluate_node_in_context(arg, cell) { + CalcResult::Range { left, right } => { + if left.sheet != right.sheet { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + "Ranges are in different sheets".to_string(), + )); + } + for row in left.row..=right.row { + for column in left.column..=right.column { + match self.evaluate_cell(CellReferenceIndex { + sheet: left.sheet, + row, + column, + }) { + CalcResult::Number(v) => values.push(v), + CalcResult::Boolean(b) => { + values.push(if b { 1.0 } else { 0.0 }); + } + CalcResult::String(_) => values.push(0.0), + error @ CalcResult::Error { .. } => return Err(error), + CalcResult::Range { .. } => { + return Err(CalcResult::new_error( + Error::ERROR, + cell, + "Unexpected Range".to_string(), + )) + } + CalcResult::EmptyCell | CalcResult::EmptyArg => {} + CalcResult::Array(_) => { + return Err(CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + }) + } + } + } + } + } + CalcResult::Number(v) => values.push(v), + CalcResult::Boolean(b) => values.push(if b { 1.0 } else { 0.0 }), + CalcResult::String(s) => { + if let Node::ReferenceKind { .. } = arg { + values.push(0.0); + } else if let Ok(t) = s.parse::() { + values.push(t); + } else { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + "Argument cannot be cast into number".to_string(), + )); + } + } + error @ CalcResult::Error { .. } => return Err(error), + CalcResult::EmptyCell | CalcResult::EmptyArg => {} + CalcResult::Array(_) => { + return Err(CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + }) + } + } + } + Ok(values) + } + + pub(crate) fn fn_stdeva(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + let values = match self.get_a_values(args, cell) { + Ok(v) => v, + Err(e) => return e, + }; + let l = values.len(); + if l < 2 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Division by 0".to_string(), + }; + } + let sum: f64 = values.iter().sum(); + let mean = sum / l as f64; + let mut var = 0.0; + for v in &values { + var += (v - mean).powi(2); + } + var /= l as f64 - 1.0; + CalcResult::Number(var.sqrt()) + } + + pub(crate) fn fn_stdevpa(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + let values = match self.get_a_values(args, cell) { + Ok(v) => v, + Err(e) => return e, + }; + let l = values.len(); + if l == 0 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Division by 0".to_string(), + }; + } + let sum: f64 = values.iter().sum(); + let mean = sum / l as f64; + let mut var = 0.0; + for v in &values { + var += (v - mean).powi(2); + } + var /= l as f64; + CalcResult::Number(var.sqrt()) + } + + pub(crate) fn fn_vara(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + let values = match self.get_a_values(args, cell) { + Ok(v) => v, + Err(e) => return e, + }; + let l = values.len(); + if l < 2 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Division by 0".to_string(), + }; + } + let sum: f64 = values.iter().sum(); + let mean = sum / l as f64; + let mut var = 0.0; + for v in &values { + var += (v - mean).powi(2); + } + var /= l as f64 - 1.0; + CalcResult::Number(var) + } + + pub(crate) fn fn_varpa(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + let values = match self.get_a_values(args, cell) { + Ok(v) => v, + Err(e) => return e, + }; + let l = values.len(); + if l == 0 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Division by 0".to_string(), + }; + } + let sum: f64 = values.iter().sum(); + let mean = sum / l as f64; + let mut var = 0.0; + for v in &values { + var += (v - mean).powi(2); + } + var /= l as f64; + CalcResult::Number(var) + } } diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index a0a0d69d6..099b9d256 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -55,6 +55,7 @@ mod test_arrays; mod test_escape_quotes; mod test_extend; mod test_fn_fv; +mod test_fn_stdev_var; mod test_fn_type; mod test_frozen_rows_and_columns; mod test_geomean; diff --git a/base/src/test/test_fn_stdev_var.rs b/base/src/test/test_fn_stdev_var.rs new file mode 100644 index 000000000..cd3669b78 --- /dev/null +++ b/base/src/test/test_fn_stdev_var.rs @@ -0,0 +1,246 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_stdev_var_no_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=STDEVA()"); + model._set("A2", "=STDEVPA()"); + model._set("A3", "=VARA()"); + model._set("A4", "=VARPA()"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); + assert_eq!(model._get_text("A4"), *"#ERROR!"); +} + +#[test] +fn test_fn_stdev_var_single_value() { + let mut model = new_empty_model(); + model._set("B1", "5"); + + // Sample functions (STDEVA, VARA) should error with single value + model._set("A1", "=STDEVA(B1)"); + model._set("A2", "=VARA(B1)"); + + // Population functions (STDEVPA, VARPA) should work with single value + model._set("A3", "=STDEVPA(B1)"); + model._set("A4", "=VARPA(B1)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#DIV/0!"); + assert_eq!(model._get_text("A2"), *"#DIV/0!"); + assert_eq!(model._get_text("A3"), *"0"); // Single value has zero deviation + assert_eq!(model._get_text("A4"), *"0"); // Single value has zero variance +} + +#[test] +fn test_fn_stdev_var_identical_values() { + let mut model = new_empty_model(); + model._set("B1", "3"); + model._set("B2", "3"); + model._set("B3", "3"); + model._set("B4", "3"); + + model._set("A1", "=STDEVA(B1:B4)"); + model._set("A2", "=STDEVPA(B1:B4)"); + model._set("A3", "=VARA(B1:B4)"); + model._set("A4", "=VARPA(B1:B4)"); + + model.evaluate(); + + // All identical values should have zero variance and standard deviation + assert_eq!(model._get_text("A1"), *"0"); + assert_eq!(model._get_text("A2"), *"0"); + assert_eq!(model._get_text("A3"), *"0"); + assert_eq!(model._get_text("A4"), *"0"); +} + +#[test] +fn test_fn_stdev_var_negative_values() { + let mut model = new_empty_model(); + model._set("B1", "-2"); + model._set("B2", "-1"); + model._set("B3", "0"); + model._set("B4", "1"); + model._set("B5", "2"); + + model._set("A1", "=STDEVA(B1:B5)"); + model._set("A2", "=STDEVPA(B1:B5)"); + model._set("A3", "=VARA(B1:B5)"); + model._set("A4", "=VARPA(B1:B5)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"1.58113883"); + assert_eq!(model._get_text("A2"), *"1.414213562"); + assert_eq!(model._get_text("A3"), *"2.5"); + assert_eq!(model._get_text("A4"), *"2"); +} + +#[test] +fn test_fn_stdev_var_data_types() { + let mut model = new_empty_model(); + model._set("B1", "10"); // Number + model._set("B2", "20"); // Number + model._set("B3", "true"); // Boolean TRUE -> 1 + model._set("B4", "false"); // Boolean FALSE -> 0 + model._set("B5", "'Hello"); // Text -> 0 + model._set("B6", "'123"); // Text number -> 0 + + model._set("A1", "=STDEVA(B1:B7)"); + model._set("A2", "=STDEVPA(B1:B7)"); + model._set("A3", "=VARA(B1:B7)"); + model._set("A4", "=VARPA(B1:B7)"); + + model.evaluate(); + assert_eq!(model._get_text("A1"), *"8.256310718"); + assert_eq!(model._get_text("A2"), *"7.536946036"); + assert_eq!(model._get_text("A3"), *"68.166666667"); + assert_eq!(model._get_text("A4"), *"56.805555556"); +} + +#[test] +fn test_fn_stdev_var_mixed_arguments() { + let mut model = new_empty_model(); + model._set("B1", "1"); + model._set("B2", "4"); + model._set("B3", "7"); + + // Test with mixed range and direct arguments + model._set("A1", "=STDEVA(B1:B2, B3, 10)"); + model._set("A2", "=STDEVPA(B1:B2, B3, 10)"); + model._set("A3", "=VARA(B1:B2, B3, 10)"); + model._set("A4", "=VARPA(B1:B2, B3, 10)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"3.872983346"); + assert_eq!(model._get_text("A2"), *"3.354101966"); + assert_eq!(model._get_text("A3"), *"15"); + assert_eq!(model._get_text("A4"), *"11.25"); +} + +#[test] +fn test_fn_stdev_var_error_propagation() { + let mut model = new_empty_model(); + model._set("B1", "1"); + model._set("B2", "=1/0"); // #DIV/0! error + model._set("B3", "3"); + + model._set("A1", "=STDEVA(B1:B3)"); + model._set("A2", "=STDEVPA(B1:B3)"); + model._set("A3", "=VARA(B1:B3)"); + model._set("A4", "=VARPA(B1:B3)"); + + model.evaluate(); + + // All should propagate the #DIV/0! error + assert_eq!(model._get_text("A1"), *"#DIV/0!"); + assert_eq!(model._get_text("A2"), *"#DIV/0!"); + assert_eq!(model._get_text("A3"), *"#DIV/0!"); + assert_eq!(model._get_text("A4"), *"#DIV/0!"); +} + +#[test] +fn test_fn_stdev_var_empty_range() { + let mut model = new_empty_model(); + // B1:B3 contains only empty cells and text (treated as 0 but empty cells ignored) + model._set("B2", "'text"); // Text -> 0, but this is the only value + + model._set("A1", "=STDEVA(B1:B3)"); + model._set("A2", "=STDEVPA(B1:B3)"); + model._set("A3", "=VARA(B1:B3)"); + model._set("A4", "=VARPA(B1:B3)"); + + model.evaluate(); + + // Only one value (0 from text), so sample functions error, population functions return 0 + assert_eq!(model._get_text("A1"), *"#DIV/0!"); + assert_eq!(model._get_text("A2"), *"0"); + assert_eq!(model._get_text("A3"), *"#DIV/0!"); + assert_eq!(model._get_text("A4"), *"0"); +} + +#[test] +fn test_fn_stdev_var_large_dataset() { + let mut model = new_empty_model(); + + // Create a larger dataset with known statistical properties + for i in 1..=10 { + model._set(&format!("B{i}"), &format!("{i}")); + } + + model._set("A1", "=STDEVA(B1:B10)"); + model._set("A2", "=STDEVPA(B1:B10)"); + model._set("A3", "=VARA(B1:B10)"); + model._set("A4", "=VARPA(B1:B10)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"3.027650354"); + assert_eq!(model._get_text("A2"), *"2.872281323"); + assert_eq!(model._get_text("A3"), *"9.166666667"); + assert_eq!(model._get_text("A4"), *"8.25"); +} + +#[test] +fn test_fn_stdev_var_boolean_only() { + let mut model = new_empty_model(); + model._set("B1", "true"); // 1 + model._set("B2", "false"); // 0 + model._set("B3", "true"); // 1 + model._set("B4", "false"); // 0 + + model._set("A1", "=STDEVA(B1:B4)"); + model._set("A2", "=STDEVPA(B1:B4)"); + model._set("A3", "=VARA(B1:B4)"); + model._set("A4", "=VARPA(B1:B4)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0.577350269"); + assert_eq!(model._get_text("A2"), *"0.5"); + assert_eq!(model._get_text("A3"), *"0.333333333"); + assert_eq!(model._get_text("A4"), *"0.25"); +} + +#[test] +fn test_fn_stdev_var_precision() { + let mut model = new_empty_model(); + model._set("B1", "1.5"); + model._set("B2", "2.5"); + + model._set("A1", "=STDEVA(B1:B2)"); + model._set("A2", "=STDEVPA(B1:B2)"); + model._set("A3", "=VARA(B1:B2)"); + model._set("A4", "=VARPA(B1:B2)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0.707106781"); + assert_eq!(model._get_text("A2"), *"0.5"); + assert_eq!(model._get_text("A3"), *"0.5"); + assert_eq!(model._get_text("A4"), *"0.25"); +} + +#[test] +fn test_fn_stdev_var_direct_argument_error_propagation() { + let mut model = new_empty_model(); + + // Test that specific errors in direct arguments are properly propagated + // This is different from the range error test - this tests direct error arguments + // Bug fix: Previously converted specific errors to generic #ERROR! + model._set("A1", "=STDEVA(1, 1/0, 3)"); // #DIV/0! in direct argument + model._set("A2", "=VARA(2, VALUE(\"text\"), 4)"); // #VALUE! in direct argument + + model.evaluate(); + + // Should propagate specific errors, not generic #ERROR! + assert_eq!(model._get_text("A1"), *"#DIV/0!"); + assert_eq!(model._get_text("A2"), *"#VALUE!"); +} diff --git a/docs/src/functions/statistical.md b/docs/src/functions/statistical.md index 6842212c3..81379d1f1 100644 --- a/docs/src/functions/statistical.md +++ b/docs/src/functions/statistical.md @@ -102,8 +102,8 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | STANDARDIZE | | – | | STDEV.P | | – | | STDEV.S | | – | -| STDEVA | | – | -| STDEVPA | | – | +| STDEVA | | – | +| STDEVPA | | – | | STEYX | | – | | T.DIST | | – | | T.DIST.2T | | – | @@ -115,7 +115,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | TRIMMEAN | | – | | VAR.P | | – | | VAR.S | | – | -| VARA | | – | -| VARPA | | – | +| VARA | | – | +| VARPA | | – | | WEIBULL.DIST | | – | | Z.TEST | | – | diff --git a/docs/src/functions/statistical/stdeva.md b/docs/src/functions/statistical/stdeva.md index a00072fe2..c24b85d41 100644 --- a/docs/src/functions/statistical/stdeva.md +++ b/docs/src/functions/statistical/stdeva.md @@ -7,6 +7,5 @@ lang: en-US # STDEVA ::: 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/stdevpa.md b/docs/src/functions/statistical/stdevpa.md index 3e3f49282..56f104866 100644 --- a/docs/src/functions/statistical/stdevpa.md +++ b/docs/src/functions/statistical/stdevpa.md @@ -7,6 +7,5 @@ lang: en-US # STDEVPA ::: 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/vara.md b/docs/src/functions/statistical/vara.md index fa683eb12..1b369f64a 100644 --- a/docs/src/functions/statistical/vara.md +++ b/docs/src/functions/statistical/vara.md @@ -7,6 +7,5 @@ lang: en-US # VARA ::: 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/varpa.md b/docs/src/functions/statistical/varpa.md index dd9b06b38..f92c9b00a 100644 --- a/docs/src/functions/statistical/varpa.md +++ b/docs/src/functions/statistical/varpa.md @@ -7,6 +7,5 @@ lang: en-US # VARPA ::: 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