From d2a5c480d5c60670056fb705a699927a9502274a Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Mon, 14 Jul 2025 17:57:04 -0700 Subject: [PATCH 1/5] Add SKEW and SKEW.P functions --- .../src/expressions/parser/static_analysis.rs | 2 + base/src/functions/mod.rs | 12 +- base/src/functions/statistical.rs | 158 ++++++++++++++++++ base/src/test/mod.rs | 1 + base/src/test/test_skew.rs | 30 ++++ docs/src/functions/statistical.md | 4 +- docs/src/functions/statistical/skew.md | 3 +- docs/src/functions/statistical/skew.p.md | 3 +- 8 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 base/src/test/test_skew.rs diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 80f194360..2157b58fc 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -785,6 +785,7 @@ 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::Skew | Function::SkewP => vec![Signature::Vector; arg_count], } } @@ -990,5 +991,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::Skew | Function::SkewP => not_implemented(args), } } diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 21c8f72da..ad26d914c 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -145,6 +145,8 @@ pub enum Function { Maxifs, Minifs, Geomean, + Skew, + SkewP, // Date and time Date, @@ -253,7 +255,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -357,6 +359,8 @@ impl Function { Function::Maxifs, Function::Minifs, Function::Geomean, + Function::Skew, + Function::SkewP, Function::Year, Function::Day, Function::Month, @@ -625,6 +629,8 @@ impl Function { "MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs), "MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs), "GEOMEAN" => Some(Function::Geomean), + "SKEW" => Some(Function::Skew), + "SKEW.P" | "_XLFN.SKEW.P" => Some(Function::SkewP), // Date and Time "YEAR" => Some(Function::Year), "DAY" => Some(Function::Day), @@ -836,6 +842,8 @@ impl fmt::Display for Function { Function::Maxifs => write!(f, "MAXIFS"), Function::Minifs => write!(f, "MINIFS"), Function::Geomean => write!(f, "GEOMEAN"), + Function::Skew => write!(f, "SKEW"), + Function::SkewP => write!(f, "SKEW.P"), Function::Year => write!(f, "YEAR"), Function::Day => write!(f, "DAY"), Function::Month => write!(f, "MONTH"), @@ -1076,6 +1084,8 @@ impl Model { Function::Maxifs => self.fn_maxifs(args, cell), Function::Minifs => self.fn_minifs(args, cell), Function::Geomean => self.fn_geomean(args, cell), + Function::Skew => self.fn_skew(args, cell), + Function::SkewP => self.fn_skew_p(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..0e4299183 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -730,4 +730,162 @@ impl Model { } CalcResult::Number(product.powf(1.0 / count)) } + + pub(crate) fn fn_skew(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + let mut values = Vec::new(); + for arg in args { + match self.evaluate_node_in_context(arg, cell) { + CalcResult::Number(value) => values.push(value), + CalcResult::Boolean(b) => { + if !matches!(arg, Node::ReferenceKind { .. }) { + values.push(if b { 1.0 } else { 0.0 }); + } + } + CalcResult::Range { left, right } => { + if left.sheet != right.sheet { + return 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(_) | CalcResult::EmptyCell | CalcResult::EmptyArg => {} + CalcResult::Range { .. } => { + return CalcResult::new_error( + Error::ERROR, + cell, + "Unexpected Range".to_string(), + ); + } + error @ CalcResult::Error { .. } => return error, + _ => {} + } + } + } + } + error @ CalcResult::Error { .. } => return error, + CalcResult::String(s) => { + if !matches!(arg, Node::ReferenceKind { .. }) { + if let Ok(t) = s.parse::() { + values.push(t); + } else { + return CalcResult::new_error( + Error::VALUE, + cell, + "Argument cannot be cast into number".to_string(), + ); + } + } + } + _ => {} + } + } + + let n = values.len(); + if n < 3 { + return CalcResult::new_error(Error::DIV, cell, "Division by Zero".to_string()); + } + + let mean = values.iter().sum::() / n as f64; + let mut var = 0.0; + for &v in &values { + var += (v - mean).powi(2); + } + let std = (var / (n as f64 - 1.0)).sqrt(); + if std == 0.0 { + return CalcResult::new_error(Error::DIV, cell, "division by 0".to_string()); + } + let mut sum3 = 0.0; + for &v in &values { + sum3 += ((v - mean) / std).powi(3); + } + let result = n as f64 / ((n as f64 - 1.0) * (n as f64 - 2.0)) * sum3; + CalcResult::Number(result) + } + + pub(crate) fn fn_skew_p(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + let mut values = Vec::new(); + for arg in args { + match self.evaluate_node_in_context(arg, cell) { + CalcResult::Number(value) => values.push(value), + CalcResult::Boolean(b) => { + if !matches!(arg, Node::ReferenceKind { .. }) { + values.push(if b { 1.0 } else { 0.0 }); + } + } + CalcResult::Range { left, right } => { + if left.sheet != right.sheet { + return 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(_) | CalcResult::EmptyCell | CalcResult::EmptyArg => {} + CalcResult::Range { .. } => { + return CalcResult::new_error( + Error::ERROR, + cell, + "Unexpected Range".to_string(), + ); + } + error @ CalcResult::Error { .. } => return error, + _ => {} + } + } + } + } + error @ CalcResult::Error { .. } => return error, + CalcResult::String(s) => { + if !matches!(arg, Node::ReferenceKind { .. }) { + if let Ok(t) = s.parse::() { + values.push(t); + } else { + return CalcResult::new_error( + Error::VALUE, + cell, + "Argument cannot be cast into number".to_string(), + ); + } + } + } + _ => {} + } + } + + let n = values.len(); + if n == 0 { + return CalcResult::new_error(Error::DIV, cell, "Division by Zero".to_string()); + } + + let mean = values.iter().sum::() / n as f64; + let mut var = 0.0; + for &v in &values { + var += (v - mean).powi(2); + } + let std = (var / n as f64).sqrt(); + if std == 0.0 { + return CalcResult::new_error(Error::DIV, cell, "division by 0".to_string()); + } + let mut sum3 = 0.0; + for &v in &values { + sum3 += ((v - mean) / std).powi(3); + } + let result = sum3 / n as f64; + CalcResult::Number(result) + } } diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index a0a0d69d6..d9ff50c11 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -58,6 +58,7 @@ mod test_fn_fv; mod test_fn_type; mod test_frozen_rows_and_columns; mod test_geomean; +mod test_skew; mod test_get_cell_content; mod test_implicit_intersection; mod test_issue_155; diff --git a/base/src/test/test_skew.rs b/base/src/test/test_skew.rs new file mode 100644 index 000000000..55a51fa35 --- /dev/null +++ b/base/src/test/test_skew.rs @@ -0,0 +1,30 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_skew_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=SKEW()"); + model._set("A2", "=SKEW.P()"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); +} + +#[test] +fn test_fn_skew_minimal() { + let mut model = new_empty_model(); + model._set("B1", "1"); + model._set("B2", "2"); + model._set("B3", "3"); + model._set("B4", "'2"); + // B5 is empty + model._set("B6", "true"); + model._set("A1", "=SKEW(B1:B6)"); + model._set("A2", "=SKEW.P(B1:B6)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"0"); + assert_eq!(model._get_text("A2"), *"0"); +} diff --git a/docs/src/functions/statistical.md b/docs/src/functions/statistical.md index 6842212c3..25b063290 100644 --- a/docs/src/functions/statistical.md +++ b/docs/src/functions/statistical.md @@ -95,8 +95,8 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | RANK.AVG | | – | | RANK.EQ | | – | | RSQ | | – | -| SKEW | | – | -| SKEW.P | | – | +| SKEW | | – | +| SKEW.P | | – | | SLOPE | | – | | SMALL | | – | | STANDARDIZE | | – | diff --git a/docs/src/functions/statistical/skew.md b/docs/src/functions/statistical/skew.md index d0d0cc50b..929d064d5 100644 --- a/docs/src/functions/statistical/skew.md +++ b/docs/src/functions/statistical/skew.md @@ -7,6 +7,5 @@ lang: en-US # SKEW ::: 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/skew.p.md b/docs/src/functions/statistical/skew.p.md index 59c2cff25..68935bd4e 100644 --- a/docs/src/functions/statistical/skew.p.md +++ b/docs/src/functions/statistical/skew.p.md @@ -7,6 +7,5 @@ lang: en-US # SKEW.P ::: 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 d997cfae043c5449833ce33c0989587c8b15a50f Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Mon, 14 Jul 2025 18:05:12 -0700 Subject: [PATCH 2/5] fmt --- base/src/functions/statistical.rs | 20 ++++++++++++++++---- base/src/test/mod.rs | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/base/src/functions/statistical.rs b/base/src/functions/statistical.rs index 0e4299183..c52a39606 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -754,9 +754,15 @@ impl Model { } for row in left.row..=right.row { for column in left.column..=right.column { - match self.evaluate_cell(CellReferenceIndex { sheet: left.sheet, row, column }) { + match self.evaluate_cell(CellReferenceIndex { + sheet: left.sheet, + row, + column, + }) { CalcResult::Number(v) => values.push(v), - CalcResult::Boolean(_) | CalcResult::EmptyCell | CalcResult::EmptyArg => {} + CalcResult::Boolean(_) + | CalcResult::EmptyCell + | CalcResult::EmptyArg => {} CalcResult::Range { .. } => { return CalcResult::new_error( Error::ERROR, @@ -833,9 +839,15 @@ impl Model { } for row in left.row..=right.row { for column in left.column..=right.column { - match self.evaluate_cell(CellReferenceIndex { sheet: left.sheet, row, column }) { + match self.evaluate_cell(CellReferenceIndex { + sheet: left.sheet, + row, + column, + }) { CalcResult::Number(v) => values.push(v), - CalcResult::Boolean(_) | CalcResult::EmptyCell | CalcResult::EmptyArg => {} + CalcResult::Boolean(_) + | CalcResult::EmptyCell + | CalcResult::EmptyArg => {} CalcResult::Range { .. } => { return CalcResult::new_error( Error::ERROR, diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index d9ff50c11..ded474056 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -58,7 +58,6 @@ mod test_fn_fv; mod test_fn_type; mod test_frozen_rows_and_columns; mod test_geomean; -mod test_skew; mod test_get_cell_content; mod test_implicit_intersection; mod test_issue_155; @@ -67,6 +66,7 @@ mod test_log; mod test_log10; mod test_percentage; mod test_set_functions_error_handling; +mod test_skew; mod test_today; mod test_types; mod user_model; From bf478a7e476b738cf7a152695c97e3af71d4ad26 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 15:53:33 -0700 Subject: [PATCH 3/5] increase test coverage --- base/src/test/test_skew.rs | 232 +++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) diff --git a/base/src/test/test_skew.rs b/base/src/test/test_skew.rs index 55a51fa35..f564638ce 100644 --- a/base/src/test/test_skew.rs +++ b/base/src/test/test_skew.rs @@ -28,3 +28,235 @@ fn test_fn_skew_minimal() { assert_eq!(model._get_text("A1"), *"0"); assert_eq!(model._get_text("A2"), *"0"); } + +// Boundary condition tests +#[test] +fn test_skew_boundary_conditions() { + let mut model = new_empty_model(); + + // SKEW requires at least 3 numeric values + model._set("A1", "=SKEW(1)"); + model._set("A2", "=SKEW(1, 2)"); + model._set("A3", "=SKEW(1, 2, 3)"); // Should work + + // SKEW.P requires at least 1 numeric value + model._set("B1", "=SKEW.P(1)"); // Should work + model._set("B2", "=SKEW.P()"); // Should error + + 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"); // Perfect symmetry = 0 skew + assert_eq!(model._get_text("B1"), *"#DIV/0!"); // Single value has undefined skew + assert_eq!(model._get_text("B2"), *"#ERROR!"); +} + +// Edge cases with identical values +#[test] +fn test_skew_identical_values() { + let mut model = new_empty_model(); + + // All identical values should cause division by zero (std = 0) + model._set("A1", "=SKEW(5, 5, 5)"); + model._set("A2", "=SKEW.P(5, 5, 5, 5)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#DIV/0!"); + assert_eq!(model._get_text("A2"), *"#DIV/0!"); +} + +// Test with negative values and mixed signs +#[test] +fn test_skew_negative_values() { + let mut model = new_empty_model(); + + // Negative values + model._set("A1", "=SKEW(-3, -2, -1)"); + model._set("A2", "=SKEW.P(-3, -2, -1)"); + + // Mixed positive/negative (right-skewed) + model._set("B1", "=SKEW(-1, 0, 10)"); + model._set("B2", "=SKEW.P(-1, 0, 10)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0"); // Symmetric + assert_eq!(model._get_text("A2"), *"0"); // Symmetric + + // Should be positive (right-skewed due to outlier 10) + let b1_val: f64 = model._get_text("B1").parse().unwrap(); + let b2_val: f64 = model._get_text("B2").parse().unwrap(); + assert!(b1_val > 0.0); + assert!(b2_val > 0.0); +} + +// Test mixed data types handling +#[test] +fn test_skew_mixed_data_types() { + let mut model = new_empty_model(); + + // Mix of numbers, text, booleans, empty cells + model._set("A1", "1"); + model._set("A2", "true"); // Boolean in reference -> ignored + model._set("A3", "'text"); // Text in reference -> ignored + model._set("A4", "2"); + // A5 is empty -> ignored + model._set("A6", "3"); + + // Direct boolean and text arguments (coerced to numbers) + model._set("B1", "=SKEW(1, 2, 3, TRUE, \"4\")"); // TRUE=1, "4"=4 → (1,2,3,1,4) + model._set("B2", "=SKEW.P(A1:A6)"); // Range refs: only 1,2,3 used (booleans/text ignored) + + model.evaluate(); + + // Direct args: SKEW(1,2,3,1,4) should work (not an error) + assert_ne!(model._get_text("B1"), *"#ERROR!"); + // Range refs: SKEW.P(1,2,3) should be 0 (symmetric) + assert_eq!(model._get_text("B2"), *"0"); +} + +// Test error propagation +#[test] +fn test_skew_error_propagation() { + let mut model = new_empty_model(); + + model._set("A1", "=1/0"); // DIV error + model._set("A2", "2"); + model._set("A3", "3"); + + model._set("B1", "=SKEW(A1:A3)"); + model._set("B2", "=SKEW.P(A1, A2, A3)"); + + model.evaluate(); + + // Errors should propagate + assert_eq!(model._get_text("B1"), *"#DIV/0!"); + assert_eq!(model._get_text("B2"), *"#DIV/0!"); +} + +// Test with known mathematical results +#[test] +fn test_skew_known_values() { + let mut model = new_empty_model(); + + // Right-skewed distribution: 1, 2, 2, 3, 8 (outlier pulls right) + model._set("A1", "=SKEW(1, 2, 2, 3, 8)"); + model._set("A2", "=SKEW.P(1, 2, 2, 3, 8)"); + + // Left-skewed distribution: 1, 6, 7, 7, 8 (outlier pulls left) + model._set("B1", "=SKEW(1, 6, 7, 7, 8)"); + model._set("B2", "=SKEW.P(1, 6, 7, 7, 8)"); + + // Perfectly symmetric distribution + model._set("C1", "=SKEW(1, 2, 3, 4, 5)"); + model._set("C2", "=SKEW.P(1, 2, 3, 4, 5)"); + + model.evaluate(); + + // Right-skewed should be positive (> 0) + let a1_val: f64 = model._get_text("A1").parse().unwrap(); + let a2_val: f64 = model._get_text("A2").parse().unwrap(); + assert!(a1_val > 0.0); + assert!(a2_val > 0.0); + + // Left-skewed should be negative (< 0) + let b1_val: f64 = model._get_text("B1").parse().unwrap(); + let b2_val: f64 = model._get_text("B2").parse().unwrap(); + assert!(b1_val < 0.0); + assert!(b2_val < 0.0); + + // Symmetric should be exactly 0 + assert_eq!(model._get_text("C1"), *"0"); + assert_eq!(model._get_text("C2"), *"0"); +} + +// Test large dataset handling +#[test] +fn test_skew_large_dataset() { + let mut model = new_empty_model(); + + // Set up a larger dataset (normal distribution should have skew ≈ 0) + for i in 1..=20 { + model._set(&format!("A{}", i), &i.to_string()); + } + + model._set("B1", "=SKEW(A1:A20)"); + model._set("B2", "=SKEW.P(A1:A20)"); + + model.evaluate(); + + // Large symmetric dataset should have skew close to 0 + let b1_val: f64 = model._get_text("B1").parse().unwrap(); + let b2_val: f64 = model._get_text("B2").parse().unwrap(); + assert!(b1_val.abs() < 0.5); // Should be close to 0 + assert!(b2_val.abs() < 0.5); // Should be close to 0 +} + +// Test precision with small differences +#[test] +fn test_skew_precision() { + let mut model = new_empty_model(); + + // Test with very small numbers + model._set("A1", "=SKEW(0.001, 0.002, 0.003)"); + model._set("A2", "=SKEW.P(0.001, 0.002, 0.003)"); + + // Test with very large numbers + model._set("B1", "=SKEW(1000000, 2000000, 3000000)"); + model._set("B2", "=SKEW.P(1000000, 2000000, 3000000)"); + + model.evaluate(); + + // Both should be 0 (perfect symmetry) + assert_eq!(model._get_text("A1"), *"0"); + assert_eq!(model._get_text("A2"), *"0"); + assert_eq!(model._get_text("B1"), *"0"); + assert_eq!(model._get_text("B2"), *"0"); +} + +// Test ranges with no numeric values +#[test] +fn test_skew_empty_and_text_only() { + let mut model = new_empty_model(); + + // Range with only empty cells + model._set("A1", "=SKEW(B1:B5)"); // Empty range + model._set("A2", "=SKEW.P(B1:B5)"); // Empty range + + // Range with only text + model._set("C1", "'text"); + model._set("C2", "'more"); + model._set("C3", "'words"); + model._set("A3", "=SKEW(C1:C3)"); + model._set("A4", "=SKEW.P(C1:C3)"); + + model.evaluate(); + + // All should error due to no numeric values + 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 SKEW vs SKEW.P differences +#[test] +fn test_skew_vs_skew_p_differences() { + let mut model = new_empty_model(); + + // Same dataset, different formulas + model._set("A1", "=SKEW(1, 2, 3, 4, 10)"); // Sample skewness + model._set("A2", "=SKEW.P(1, 2, 3, 4, 10)"); // Population skewness + + model.evaluate(); + + // Both should be positive (right-skewed), but different values + let skew_sample: f64 = model._get_text("A1").parse().unwrap(); + let skew_pop: f64 = model._get_text("A2").parse().unwrap(); + + assert!(skew_sample > 0.0); + assert!(skew_pop > 0.0); + assert_ne!(skew_sample, skew_pop); // Should be different values +} From 5ea9329289a5d566575fc4a5e5db48ce87ea2d01 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 15:57:00 -0700 Subject: [PATCH 4/5] fmt --- base/src/test/test_skew.rs | 88 +++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/base/src/test/test_skew.rs b/base/src/test/test_skew.rs index f564638ce..a2318f9ab 100644 --- a/base/src/test/test_skew.rs +++ b/base/src/test/test_skew.rs @@ -33,18 +33,18 @@ fn test_fn_skew_minimal() { #[test] fn test_skew_boundary_conditions() { let mut model = new_empty_model(); - + // SKEW requires at least 3 numeric values model._set("A1", "=SKEW(1)"); model._set("A2", "=SKEW(1, 2)"); model._set("A3", "=SKEW(1, 2, 3)"); // Should work - + // SKEW.P requires at least 1 numeric value model._set("B1", "=SKEW.P(1)"); // Should work model._set("B2", "=SKEW.P()"); // Should error - + 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"); // Perfect symmetry = 0 skew @@ -56,13 +56,13 @@ fn test_skew_boundary_conditions() { #[test] fn test_skew_identical_values() { let mut model = new_empty_model(); - + // All identical values should cause division by zero (std = 0) model._set("A1", "=SKEW(5, 5, 5)"); model._set("A2", "=SKEW.P(5, 5, 5, 5)"); - + model.evaluate(); - + assert_eq!(model._get_text("A1"), *"#DIV/0!"); assert_eq!(model._get_text("A2"), *"#DIV/0!"); } @@ -71,20 +71,20 @@ fn test_skew_identical_values() { #[test] fn test_skew_negative_values() { let mut model = new_empty_model(); - + // Negative values model._set("A1", "=SKEW(-3, -2, -1)"); model._set("A2", "=SKEW.P(-3, -2, -1)"); - + // Mixed positive/negative (right-skewed) model._set("B1", "=SKEW(-1, 0, 10)"); model._set("B2", "=SKEW.P(-1, 0, 10)"); - + model.evaluate(); - + assert_eq!(model._get_text("A1"), *"0"); // Symmetric assert_eq!(model._get_text("A2"), *"0"); // Symmetric - + // Should be positive (right-skewed due to outlier 10) let b1_val: f64 = model._get_text("B1").parse().unwrap(); let b2_val: f64 = model._get_text("B2").parse().unwrap(); @@ -96,7 +96,7 @@ fn test_skew_negative_values() { #[test] fn test_skew_mixed_data_types() { let mut model = new_empty_model(); - + // Mix of numbers, text, booleans, empty cells model._set("A1", "1"); model._set("A2", "true"); // Boolean in reference -> ignored @@ -104,13 +104,13 @@ fn test_skew_mixed_data_types() { model._set("A4", "2"); // A5 is empty -> ignored model._set("A6", "3"); - + // Direct boolean and text arguments (coerced to numbers) model._set("B1", "=SKEW(1, 2, 3, TRUE, \"4\")"); // TRUE=1, "4"=4 → (1,2,3,1,4) model._set("B2", "=SKEW.P(A1:A6)"); // Range refs: only 1,2,3 used (booleans/text ignored) - + model.evaluate(); - + // Direct args: SKEW(1,2,3,1,4) should work (not an error) assert_ne!(model._get_text("B1"), *"#ERROR!"); // Range refs: SKEW.P(1,2,3) should be 0 (symmetric) @@ -121,16 +121,16 @@ fn test_skew_mixed_data_types() { #[test] fn test_skew_error_propagation() { let mut model = new_empty_model(); - + model._set("A1", "=1/0"); // DIV error model._set("A2", "2"); model._set("A3", "3"); - + model._set("B1", "=SKEW(A1:A3)"); model._set("B2", "=SKEW.P(A1, A2, A3)"); - + model.evaluate(); - + // Errors should propagate assert_eq!(model._get_text("B1"), *"#DIV/0!"); assert_eq!(model._get_text("B2"), *"#DIV/0!"); @@ -140,33 +140,33 @@ fn test_skew_error_propagation() { #[test] fn test_skew_known_values() { let mut model = new_empty_model(); - + // Right-skewed distribution: 1, 2, 2, 3, 8 (outlier pulls right) model._set("A1", "=SKEW(1, 2, 2, 3, 8)"); model._set("A2", "=SKEW.P(1, 2, 2, 3, 8)"); - + // Left-skewed distribution: 1, 6, 7, 7, 8 (outlier pulls left) model._set("B1", "=SKEW(1, 6, 7, 7, 8)"); model._set("B2", "=SKEW.P(1, 6, 7, 7, 8)"); - + // Perfectly symmetric distribution model._set("C1", "=SKEW(1, 2, 3, 4, 5)"); model._set("C2", "=SKEW.P(1, 2, 3, 4, 5)"); - + model.evaluate(); - + // Right-skewed should be positive (> 0) let a1_val: f64 = model._get_text("A1").parse().unwrap(); let a2_val: f64 = model._get_text("A2").parse().unwrap(); assert!(a1_val > 0.0); assert!(a2_val > 0.0); - + // Left-skewed should be negative (< 0) let b1_val: f64 = model._get_text("B1").parse().unwrap(); let b2_val: f64 = model._get_text("B2").parse().unwrap(); assert!(b1_val < 0.0); assert!(b2_val < 0.0); - + // Symmetric should be exactly 0 assert_eq!(model._get_text("C1"), *"0"); assert_eq!(model._get_text("C2"), *"0"); @@ -176,17 +176,17 @@ fn test_skew_known_values() { #[test] fn test_skew_large_dataset() { let mut model = new_empty_model(); - + // Set up a larger dataset (normal distribution should have skew ≈ 0) for i in 1..=20 { model._set(&format!("A{}", i), &i.to_string()); } - + model._set("B1", "=SKEW(A1:A20)"); model._set("B2", "=SKEW.P(A1:A20)"); - + model.evaluate(); - + // Large symmetric dataset should have skew close to 0 let b1_val: f64 = model._get_text("B1").parse().unwrap(); let b2_val: f64 = model._get_text("B2").parse().unwrap(); @@ -198,17 +198,17 @@ fn test_skew_large_dataset() { #[test] fn test_skew_precision() { let mut model = new_empty_model(); - + // Test with very small numbers model._set("A1", "=SKEW(0.001, 0.002, 0.003)"); model._set("A2", "=SKEW.P(0.001, 0.002, 0.003)"); - + // Test with very large numbers model._set("B1", "=SKEW(1000000, 2000000, 3000000)"); model._set("B2", "=SKEW.P(1000000, 2000000, 3000000)"); - + model.evaluate(); - + // Both should be 0 (perfect symmetry) assert_eq!(model._get_text("A1"), *"0"); assert_eq!(model._get_text("A2"), *"0"); @@ -220,20 +220,20 @@ fn test_skew_precision() { #[test] fn test_skew_empty_and_text_only() { let mut model = new_empty_model(); - + // Range with only empty cells model._set("A1", "=SKEW(B1:B5)"); // Empty range model._set("A2", "=SKEW.P(B1:B5)"); // Empty range - + // Range with only text model._set("C1", "'text"); model._set("C2", "'more"); model._set("C3", "'words"); model._set("A3", "=SKEW(C1:C3)"); model._set("A4", "=SKEW.P(C1:C3)"); - + model.evaluate(); - + // All should error due to no numeric values assert_eq!(model._get_text("A1"), *"#DIV/0!"); assert_eq!(model._get_text("A2"), *"#DIV/0!"); @@ -241,21 +241,21 @@ fn test_skew_empty_and_text_only() { assert_eq!(model._get_text("A4"), *"#DIV/0!"); } -// Test SKEW vs SKEW.P differences +// Test SKEW vs SKEW.P differences #[test] fn test_skew_vs_skew_p_differences() { let mut model = new_empty_model(); - + // Same dataset, different formulas model._set("A1", "=SKEW(1, 2, 3, 4, 10)"); // Sample skewness model._set("A2", "=SKEW.P(1, 2, 3, 4, 10)"); // Population skewness - + model.evaluate(); - + // Both should be positive (right-skewed), but different values let skew_sample: f64 = model._get_text("A1").parse().unwrap(); let skew_pop: f64 = model._get_text("A2").parse().unwrap(); - + assert!(skew_sample > 0.0); assert!(skew_pop > 0.0); assert_ne!(skew_sample, skew_pop); // Should be different values From 8ddc70a56b7b7065c719651988036d5150f7bc96 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 16:02:26 -0700 Subject: [PATCH 5/5] fix test --- base/src/test/test_skew.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/test/test_skew.rs b/base/src/test/test_skew.rs index a2318f9ab..eadb95fdb 100644 --- a/base/src/test/test_skew.rs +++ b/base/src/test/test_skew.rs @@ -179,7 +179,7 @@ fn test_skew_large_dataset() { // Set up a larger dataset (normal distribution should have skew ≈ 0) for i in 1..=20 { - model._set(&format!("A{}", i), &i.to_string()); + model._set(&format!("A{i}"), &i.to_string()); } model._set("B1", "=SKEW(A1:A20)");