From c6a21b9c27c64503e8b436c74788f3b5eff04998 Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Mon, 14 Jul 2025 18:04:50 -0700 Subject: [PATCH 1/7] Add STDEVA, STDEVPA, VARA and VARPA functions --- .../src/expressions/parser/static_analysis.rs | 8 + base/src/functions/mod.rs | 22 +- base/src/functions/statistical.rs | 193 ++++++++++++++++++ base/src/test/mod.rs | 1 + base/src/test/test_fn_stdev_var.rs | 37 ++++ docs/src/functions/statistical.md | 8 +- docs/src/functions/statistical/stdeva.md | 3 +- docs/src/functions/statistical/stdevpa.md | 3 +- docs/src/functions/statistical/vara.md | 3 +- docs/src/functions/statistical/varpa.md | 3 +- 10 files changed, 268 insertions(+), 13 deletions(-) create mode 100644 base/src/test/test_fn_stdev_var.rs 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..2653bf070 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -730,4 +730,197 @@ 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), + CalcResult::Error { .. } => { + return Err(CalcResult::Error { + error: Error::ERROR, + origin: cell, + message: "Unexpected Range".to_string(), + }) + } + 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(), + )); + } + } + CalcResult::Error { .. } => { + return Err(CalcResult::new_error( + Error::ERROR, + cell, + "Error".to_string(), + )) + } + 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..8d83b8b18 --- /dev/null +++ b/base/src/test/test_fn_stdev_var.rs @@ -0,0 +1,37 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_stdev_var_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_minimal() { + let mut model = new_empty_model(); + model._set("B1", "1"); + model._set("B2", "2"); + model._set("B3", "3"); + model._set("B4", "'2"); + model._set("B6", "true"); + model._set("A1", "=STDEVA(B1:B6)"); + model._set("A2", "=STDEVPA(B1:B6)"); + model._set("A3", "=VARA(B1:B6)"); + model._set("A4", "=VARPA(B1:B6)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"1.140175425"); + assert_eq!(model._get_text("A2"), *"1.019803903"); + assert_eq!(model._get_text("A3"), *"1.3"); + assert_eq!(model._get_text("A4"), *"1.04"); +} 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 From 8433a36be6c90ebc4b820f889a7deb2608f656d7 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 15:32:55 -0700 Subject: [PATCH 2/7] fix error handling in get_a_values --- base/src/functions/statistical.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/base/src/functions/statistical.rs b/base/src/functions/statistical.rs index 2653bf070..0955afde4 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -759,13 +759,7 @@ impl Model { values.push(if b { 1.0 } else { 0.0 }); } CalcResult::String(_) => values.push(0.0), - CalcResult::Error { .. } => { - return Err(CalcResult::Error { - error: Error::ERROR, - origin: cell, - message: "Unexpected Range".to_string(), - }) - } + error @ CalcResult::Error { .. } => return Err(error), CalcResult::Range { .. } => { return Err(CalcResult::new_error( Error::ERROR, From 8f89ccda8e51c5e96bb013b4cfca9393fd96c5fd Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 15:42:11 -0700 Subject: [PATCH 3/7] increase test coverage --- base/src/test/test_fn_stdev_var.rs | 221 +++++++++++++++++++++++++++-- 1 file changed, 207 insertions(+), 14 deletions(-) diff --git a/base/src/test/test_fn_stdev_var.rs b/base/src/test/test_fn_stdev_var.rs index 8d83b8b18..e378de79e 100644 --- a/base/src/test/test_fn_stdev_var.rs +++ b/base/src/test/test_fn_stdev_var.rs @@ -3,7 +3,7 @@ use crate::test::util::new_empty_model; #[test] -fn test_fn_stdev_var_arguments() { +fn test_fn_stdev_var_no_arguments() { let mut model = new_empty_model(); model._set("A1", "=STDEVA()"); model._set("A2", "=STDEVPA()"); @@ -18,20 +18,213 @@ fn test_fn_stdev_var_arguments() { } #[test] -fn test_fn_stdev_var_minimal() { +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 + // B7 is empty -> ignored + + 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"), *"77.2"); + assert_eq!(model._get_text("A4"), *"61.76"); +} + +#[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", "2"); + model._set("B2", "=1/0"); // #DIV/0! error model._set("B3", "3"); - model._set("B4", "'2"); - model._set("B6", "true"); - model._set("A1", "=STDEVA(B1:B6)"); - model._set("A2", "=STDEVPA(B1:B6)"); - model._set("A3", "=VARA(B1:B6)"); - model._set("A4", "=VARPA(B1:B6)"); - model.evaluate(); - assert_eq!(model._get_text("A1"), *"1.140175425"); - assert_eq!(model._get_text("A2"), *"1.019803903"); - assert_eq!(model._get_text("A3"), *"1.3"); - assert_eq!(model._get_text("A4"), *"1.04"); + + 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"); } From ff6097eef8d06f4bd4be47ca37d12bca593fe6f3 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 15:54:47 -0700 Subject: [PATCH 4/7] fmt test --- base/src/test/test_fn_stdev_var.rs | 86 +++++++++++++++--------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/base/src/test/test_fn_stdev_var.rs b/base/src/test/test_fn_stdev_var.rs index e378de79e..93afd3194 100644 --- a/base/src/test/test_fn_stdev_var.rs +++ b/base/src/test/test_fn_stdev_var.rs @@ -21,17 +21,17 @@ fn test_fn_stdev_var_no_arguments() { 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 @@ -45,14 +45,14 @@ fn test_fn_stdev_var_identical_values() { 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"); @@ -68,14 +68,14 @@ fn test_fn_stdev_var_negative_values() { 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"); @@ -85,19 +85,19 @@ fn test_fn_stdev_var_negative_values() { #[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 - // B7 is empty -> ignored - + 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 + // B7 is empty -> ignored + 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"); @@ -111,15 +111,15 @@ fn test_fn_stdev_var_mixed_arguments() { 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"); @@ -130,16 +130,16 @@ fn test_fn_stdev_var_mixed_arguments() { 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("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!"); @@ -152,14 +152,14 @@ 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"); @@ -170,19 +170,19 @@ fn test_fn_stdev_var_empty_range() { #[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"); @@ -192,18 +192,18 @@ fn test_fn_stdev_var_large_dataset() { #[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("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"); @@ -215,14 +215,14 @@ 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"); From 3f05b7ad5e54eb25a983e23d10924a0e9790803c Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 16:00:57 -0700 Subject: [PATCH 5/7] fix test --- base/src/test/test_fn_stdev_var.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/test/test_fn_stdev_var.rs b/base/src/test/test_fn_stdev_var.rs index 93afd3194..42cd2c2d0 100644 --- a/base/src/test/test_fn_stdev_var.rs +++ b/base/src/test/test_fn_stdev_var.rs @@ -173,7 +173,7 @@ fn test_fn_stdev_var_large_dataset() { // Create a larger dataset with known statistical properties for i in 1..=10 { - model._set(&format!("B{}", i), &format!("{}", i)); + model._set(&format!("B{i}"), &format!("{i}")); } model._set("A1", "=STDEVA(B1:B10)"); From 48a063c4c32fed20353ebb45a2310938efbe24f8 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 16:14:02 -0700 Subject: [PATCH 6/7] fix test --- base/src/test/test_fn_stdev_var.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/base/src/test/test_fn_stdev_var.rs b/base/src/test/test_fn_stdev_var.rs index 42cd2c2d0..71a324641 100644 --- a/base/src/test/test_fn_stdev_var.rs +++ b/base/src/test/test_fn_stdev_var.rs @@ -91,7 +91,6 @@ fn test_fn_stdev_var_data_types() { model._set("B4", "false"); // Boolean FALSE -> 0 model._set("B5", "'Hello"); // Text -> 0 model._set("B6", "'123"); // Text number -> 0 - // B7 is empty -> ignored model._set("A1", "=STDEVA(B1:B7)"); model._set("A2", "=STDEVPA(B1:B7)"); @@ -101,8 +100,8 @@ fn test_fn_stdev_var_data_types() { model.evaluate(); assert_eq!(model._get_text("A1"), *"8.256310718"); assert_eq!(model._get_text("A2"), *"7.536946036"); - assert_eq!(model._get_text("A3"), *"77.2"); - assert_eq!(model._get_text("A4"), *"61.76"); + assert_eq!(model._get_text("A3"), *"68.166666667"); + assert_eq!(model._get_text("A4"), *"56.805555556"); } #[test] From b084655c1e76c1857d2724743a9e4713f0e548d4 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 16:34:17 -0700 Subject: [PATCH 7/7] fix error handling in get_a_values and add test case --- base/src/functions/statistical.rs | 8 +------- base/src/test/test_fn_stdev_var.rs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/base/src/functions/statistical.rs b/base/src/functions/statistical.rs index 0955afde4..c6267f8d1 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -794,13 +794,7 @@ impl Model { )); } } - CalcResult::Error { .. } => { - return Err(CalcResult::new_error( - Error::ERROR, - cell, - "Error".to_string(), - )) - } + error @ CalcResult::Error { .. } => return Err(error), CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::Array(_) => { return Err(CalcResult::Error { diff --git a/base/src/test/test_fn_stdev_var.rs b/base/src/test/test_fn_stdev_var.rs index 71a324641..cd3669b78 100644 --- a/base/src/test/test_fn_stdev_var.rs +++ b/base/src/test/test_fn_stdev_var.rs @@ -227,3 +227,20 @@ fn test_fn_stdev_var_precision() { 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!"); +}