From a51c41cbe15744c43b6cad844b8ee8b16571f1ae Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Sun, 13 Jul 2025 23:51:58 -0700 Subject: [PATCH 1/7] Add percentile and percentrank functions --- .../src/expressions/parser/static_analysis.rs | 32 +++ base/src/functions/mod.rs | 26 +- base/src/functions/statistical.rs | 269 ++++++++++++++++++ base/src/test/mod.rs | 1 + base/src/test/test_percentile.rs | 29 ++ .../functions/statistical/percentile.exc.md | 3 +- .../functions/statistical/percentile.inc.md | 3 +- .../functions/statistical/percentrank.exc.md | 3 +- .../functions/statistical/percentrank.inc.md | 3 +- 9 files changed, 360 insertions(+), 9 deletions(-) create mode 100644 base/src/test/test_percentile.rs diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 280ac2484..d254fc7ce 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -571,6 +571,14 @@ fn args_signature_xnpv(arg_count: usize) -> Vec { } } +fn args_signature_vector_scalar(arg_count: usize) -> Vec { + if arg_count == 2 { + vec![Signature::Vector, Signature::Scalar] + } else { + vec![Signature::Error; arg_count] + } +} + // FIXME: This is terrible duplications of efforts. We use the signature in at least three different places: // 1. When computing the function // 2. Checking the arguments to see if we need to insert the implicit intersection operator @@ -778,6 +786,26 @@ 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::PercentileExc => args_signature_vector_scalar(arg_count), + Function::PercentileInc => args_signature_vector_scalar(arg_count), + Function::PercentrankExc => { + if arg_count == 2 { + vec![Signature::Vector, Signature::Scalar] + } else if arg_count == 3 { + vec![Signature::Vector, Signature::Scalar, Signature::Scalar] + } else { + vec![Signature::Error; arg_count] + } + } + Function::PercentrankInc => { + if arg_count == 2 { + vec![Signature::Vector, Signature::Scalar] + } else if arg_count == 3 { + vec![Signature::Vector, Signature::Scalar, Signature::Scalar] + } else { + vec![Signature::Error; arg_count] + } + } } } @@ -980,5 +1008,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::PercentileExc => not_implemented(args), + Function::PercentileInc => not_implemented(args), + Function::PercentrankExc => not_implemented(args), + Function::PercentrankInc => not_implemented(args), } } diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 45da02525..4d8a9a211 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -142,6 +142,10 @@ pub enum Function { Maxifs, Minifs, Geomean, + PercentileExc, + PercentileInc, + PercentrankExc, + PercentrankInc, // Date and time Date, @@ -250,7 +254,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -351,6 +355,10 @@ impl Function { Function::Maxifs, Function::Minifs, Function::Geomean, + Function::PercentileExc, + Function::PercentileInc, + Function::PercentrankExc, + Function::PercentrankInc, Function::Year, Function::Day, Function::Month, @@ -470,6 +478,10 @@ impl Function { Function::Unicode => "_xlfn.UNICODE".to_string(), Function::Rri => "_xlfn.RRI".to_string(), Function::Pduration => "_xlfn.PDURATION".to_string(), + Function::PercentileExc => "_xlfn.PERCENTILE.EXC".to_string(), + Function::PercentileInc => "_xlfn.PERCENTILE.INC".to_string(), + Function::PercentrankExc => "_xlfn.PERCENTRANK.EXC".to_string(), + Function::PercentrankInc => "_xlfn.PERCENTRANK.INC".to_string(), Function::Bitand => "_xlfn.BITAND".to_string(), Function::Bitor => "_xlfn.BITOR".to_string(), Function::Bitxor => "_xlfn.BITXOR".to_string(), @@ -615,6 +627,10 @@ impl Function { "MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs), "MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs), "GEOMEAN" => Some(Function::Geomean), + "PERCENTILE.EXC" | "_XLFN.PERCENTILE.EXC" => Some(Function::PercentileExc), + "PERCENTILE.INC" | "_XLFN.PERCENTILE.INC" => Some(Function::PercentileInc), + "PERCENTRANK.EXC" | "_XLFN.PERCENTRANK.EXC" => Some(Function::PercentrankExc), + "PERCENTRANK.INC" | "_XLFN.PERCENTRANK.INC" => Some(Function::PercentrankInc), // Date and Time "YEAR" => Some(Function::Year), "DAY" => Some(Function::Day), @@ -823,6 +839,10 @@ impl fmt::Display for Function { Function::Maxifs => write!(f, "MAXIFS"), Function::Minifs => write!(f, "MINIFS"), Function::Geomean => write!(f, "GEOMEAN"), + Function::PercentileExc => write!(f, "PERCENTILE.EXC"), + Function::PercentileInc => write!(f, "PERCENTILE.INC"), + Function::PercentrankExc => write!(f, "PERCENTRANK.EXC"), + Function::PercentrankInc => write!(f, "PERCENTRANK.INC"), Function::Year => write!(f, "YEAR"), Function::Day => write!(f, "DAY"), Function::Month => write!(f, "MONTH"), @@ -1060,6 +1080,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::PercentileExc => self.fn_percentile_exc(args, cell), + Function::PercentileInc => self.fn_percentile_inc(args, cell), + Function::PercentrankExc => self.fn_percentrank_exc(args, cell), + Function::PercentrankInc => self.fn_percentrank_inc(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..a41fcb5a1 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -730,4 +730,273 @@ impl Model { } CalcResult::Number(product.powf(1.0 / count)) } + + fn get_array_of_numbers_stat( + &mut self, + arg: &Node, + cell: CellReferenceIndex, + ) -> Result, CalcResult> { + let mut values = Vec::new(); + let result = self.evaluate_node_in_context(arg, cell); + match result { + CalcResult::Number(value) => values.push(value), + CalcResult::Boolean(b) => { + if !matches!(arg, Node::ReferenceKind { .. }) { + values.push(if b { 1.0 } else { 0.0 }); + } + } + CalcResult::String(s) => { + if !matches!(arg, Node::ReferenceKind { .. }) { + if let Ok(v) = s.parse::() { + values.push(v); + } else { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + "Argument cannot be cast into number".to_string(), + )); + } + } + } + CalcResult::Range { left, right } => { + if left.sheet != right.sheet { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + "Ranges are in different sheets".to_string(), + )); + } + let row1 = left.row; + let mut row2 = right.row; + let column1 = left.column; + let mut column2 = right.column; + if row1 == 1 && row2 == LAST_ROW { + row2 = self + .workbook + .worksheet(left.sheet) + .map_err(|_| { + CalcResult::new_error( + Error::ERROR, + cell, + format!("Invalid worksheet index: '{}'", left.sheet), + ) + })? + .dimension() + .max_row; + } + if column1 == 1 && column2 == LAST_COLUMN { + column2 = self + .workbook + .worksheet(left.sheet) + .map_err(|_| { + CalcResult::new_error( + Error::ERROR, + cell, + format!("Invalid worksheet index: '{}'", left.sheet), + ) + })? + .dimension() + .max_column; + } + for row in row1..=row2 { + for column in column1..=column2 { + let v = self.evaluate_cell(CellReferenceIndex { + sheet: left.sheet, + row, + column, + }); + match v { + CalcResult::Number(num) => values.push(num), + CalcResult::Error { .. } => return Err(v), + _ => {} + } + } + } + } + CalcResult::Error { .. } => return Err(result), + CalcResult::Array(_) => { + return Err(CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + }) + } + CalcResult::EmptyCell | CalcResult::EmptyArg => {} + } + Ok(values) + } + + pub(crate) fn fn_percentile_inc( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + ) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let mut values = match self.get_array_of_numbers_stat(&args[0], cell) { + Ok(v) => v, + Err(e) => return e, + }; + if values.is_empty() { + return CalcResult::new_error(Error::NUM, cell, "Empty array".to_string()); + } + values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let k = match self.get_number(&args[1], cell) { + Ok(v) => v, + Err(e) => return e, + }; + if !(0.0..=1.0).contains(&k) { + return CalcResult::new_error(Error::NUM, cell, "k out of range".to_string()); + } + let n = values.len() as f64; + let pos = k * (n - 1.0) + 1.0; + let m = pos.floor(); + let g = pos - m; + let idx = (m as usize).saturating_sub(1); + if idx >= values.len() - 1 { + return CalcResult::Number(*values.last().unwrap()); + } + let result = values[idx] + g * (values[idx + 1] - values[idx]); + CalcResult::Number(result) + } + + pub(crate) fn fn_percentile_exc( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + ) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let mut values = match self.get_array_of_numbers_stat(&args[0], cell) { + Ok(v) => v, + Err(e) => return e, + }; + if values.is_empty() { + return CalcResult::new_error(Error::NUM, cell, "Empty array".to_string()); + } + values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let k = match self.get_number(&args[1], cell) { + Ok(v) => v, + Err(e) => return e, + }; + let n = values.len() as f64; + if k <= 0.0 || k >= 1.0 { + return CalcResult::new_error(Error::NUM, cell, "k out of range".to_string()); + } + let pos = k * (n + 1.0); + if pos < 1.0 || pos > n { + return CalcResult::new_error(Error::NUM, cell, "k out of range".to_string()); + } + let m = pos.floor(); + let g = pos - m; + let idx = (m as usize).saturating_sub(1); + if idx >= values.len() - 1 { + return CalcResult::Number(*values.last().unwrap()); + } + let result = values[idx] + g * (values[idx + 1] - values[idx]); + CalcResult::Number(result) + } + + pub(crate) fn fn_percentrank_inc( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + ) -> CalcResult { + if args.len() < 2 || args.len() > 3 { + return CalcResult::new_args_number_error(cell); + } + let mut values = match self.get_array_of_numbers_stat(&args[0], cell) { + Ok(v) => v, + Err(e) => return e, + }; + if values.is_empty() { + return CalcResult::new_error(Error::NUM, cell, "Empty array".to_string()); + } + values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let x = match self.get_number(&args[1], cell) { + Ok(v) => v, + Err(e) => return e, + }; + let decimals = if args.len() == 3 { + match self.get_number(&args[2], cell) { + Ok(v) => v as i32, + Err(e) => return e, + } + } else { + 3 + }; + let n = values.len() as f64; + if x < values[0] { + return CalcResult::Number(0.0); + } + if x > values[values.len() - 1] { + return CalcResult::Number(1.0); + } + let mut idx = 0; + while idx < values.len() && values[idx] < x { + idx += 1; + } + let rank = if idx == values.len() || (x - values[idx]).abs() > f64::EPSILON { + let lower = values[idx - 1]; + let upper = values[idx]; + (idx as f64 - 1.0) + (x - lower) / (upper - lower) + } else { + idx as f64 + }; + let mut result = rank / (n - 1.0); + let factor = 10f64.powi(decimals); + result = (result * factor).round() / factor; + CalcResult::Number(result) + } + + pub(crate) fn fn_percentrank_exc( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + ) -> CalcResult { + if args.len() < 2 || args.len() > 3 { + return CalcResult::new_args_number_error(cell); + } + let mut values = match self.get_array_of_numbers_stat(&args[0], cell) { + Ok(v) => v, + Err(e) => return e, + }; + if values.is_empty() { + return CalcResult::new_error(Error::NUM, cell, "Empty array".to_string()); + } + values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let x = match self.get_number(&args[1], cell) { + Ok(v) => v, + Err(e) => return e, + }; + let decimals = if args.len() == 3 { + match self.get_number(&args[2], cell) { + Ok(v) => v as i32, + Err(e) => return e, + } + } else { + 3 + }; + let n = values.len(); + if x <= values[0] || x >= values[n - 1] { + return CalcResult::new_error(Error::NUM, cell, "x out of range".to_string()); + } + let mut idx = 0; + while idx < n && values[idx] < x { + idx += 1; + } + let rank = if (x - values[idx]).abs() > f64::EPSILON { + let lower = values[idx - 1]; + let upper = values[idx]; + idx as f64 + (x - lower) / (upper - lower) + } else { + (idx + 1) as f64 + }; + let mut result = rank / ((n + 1) as f64); + let factor = 10f64.powi(decimals); + result = (result * factor).round() / factor; + CalcResult::Number(result) + } } diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index 8e1b4ebe1..0f64d9f09 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -62,6 +62,7 @@ mod test_get_cell_content; mod test_implicit_intersection; mod test_issue_155; mod test_percentage; +mod test_percentile; mod test_set_functions_error_handling; mod test_today; mod test_types; diff --git a/base/src/test/test_percentile.rs b/base/src/test/test_percentile.rs new file mode 100644 index 000000000..1ac97fc37 --- /dev/null +++ b/base/src/test/test_percentile.rs @@ -0,0 +1,29 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_percentile() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + model._set("A1", "=PERCENTILE.INC(B1:B5,0.4)"); + model._set("A2", "=PERCENTILE.EXC(B1:B5,0.4)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"2.6"); + assert_eq!(model._get_text("A2"), *"2.4"); +} + +#[test] +fn test_fn_percentrank() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + model._set("A1", "=PERCENTRANK.INC(B1:B5,3.5)"); + model._set("A2", "=PERCENTRANK.EXC(B1:B5,3.5)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"0.625"); + assert_eq!(model._get_text("A2"), *"0.583"); +} diff --git a/docs/src/functions/statistical/percentile.exc.md b/docs/src/functions/statistical/percentile.exc.md index f850e760a..d01d23dd2 100644 --- a/docs/src/functions/statistical/percentile.exc.md +++ b/docs/src/functions/statistical/percentile.exc.md @@ -7,6 +7,5 @@ lang: en-US # PERCENTILE.EXC ::: 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/percentile.inc.md b/docs/src/functions/statistical/percentile.inc.md index 972640d06..9c2fb0106 100644 --- a/docs/src/functions/statistical/percentile.inc.md +++ b/docs/src/functions/statistical/percentile.inc.md @@ -7,6 +7,5 @@ lang: en-US # PERCENTILE.INC ::: 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/percentrank.exc.md b/docs/src/functions/statistical/percentrank.exc.md index 03e93c161..bfb296acf 100644 --- a/docs/src/functions/statistical/percentrank.exc.md +++ b/docs/src/functions/statistical/percentrank.exc.md @@ -7,6 +7,5 @@ lang: en-US # PERCENTRANK.EXC ::: 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/percentrank.inc.md b/docs/src/functions/statistical/percentrank.inc.md index c7870af7a..22e6f27b6 100644 --- a/docs/src/functions/statistical/percentrank.inc.md +++ b/docs/src/functions/statistical/percentrank.inc.md @@ -7,6 +7,5 @@ lang: en-US # PERCENTRANK.INC ::: 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 cae408f14101c34b4d713cd35c8d3645be34562b Mon Sep 17 00:00:00 2001 From: BrianHung Date: Mon, 14 Jul 2025 00:02:05 -0700 Subject: [PATCH 2/7] fix build --- base/src/functions/statistical.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/base/src/functions/statistical.rs b/base/src/functions/statistical.rs index a41fcb5a1..668474a2e 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -8,6 +8,7 @@ use crate::{ }; use super::util::build_criteria; +use std::cmp::Ordering; impl Model { pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { @@ -841,7 +842,7 @@ impl Model { if values.is_empty() { return CalcResult::new_error(Error::NUM, cell, "Empty array".to_string()); } - values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); let k = match self.get_number(&args[1], cell) { Ok(v) => v, Err(e) => return e, @@ -855,7 +856,11 @@ impl Model { let g = pos - m; let idx = (m as usize).saturating_sub(1); if idx >= values.len() - 1 { - return CalcResult::Number(*values.last().unwrap()); + let last_value = match values.last() { + Some(&v) => v, + None => return CalcResult::new_error(Error::NUM, cell, "Empty array".to_string()), + }; + return CalcResult::Number(last_value); } let result = values[idx] + g * (values[idx + 1] - values[idx]); CalcResult::Number(result) @@ -876,7 +881,7 @@ impl Model { if values.is_empty() { return CalcResult::new_error(Error::NUM, cell, "Empty array".to_string()); } - values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); let k = match self.get_number(&args[1], cell) { Ok(v) => v, Err(e) => return e, @@ -893,7 +898,11 @@ impl Model { let g = pos - m; let idx = (m as usize).saturating_sub(1); if idx >= values.len() - 1 { - return CalcResult::Number(*values.last().unwrap()); + let last_value = match values.last() { + Some(&v) => v, + None => return CalcResult::new_error(Error::NUM, cell, "Empty array".to_string()), + }; + return CalcResult::Number(last_value); } let result = values[idx] + g * (values[idx + 1] - values[idx]); CalcResult::Number(result) @@ -914,7 +923,7 @@ impl Model { if values.is_empty() { return CalcResult::new_error(Error::NUM, cell, "Empty array".to_string()); } - values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); let x = match self.get_number(&args[1], cell) { Ok(v) => v, Err(e) => return e, @@ -966,7 +975,7 @@ impl Model { if values.is_empty() { return CalcResult::new_error(Error::NUM, cell, "Empty array".to_string()); } - values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); let x = match self.get_number(&args[1], cell) { Ok(v) => v, Err(e) => return e, From 0009b62be055ab44f2b20ffb971e163f40153b05 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 17:21:29 -0700 Subject: [PATCH 3/7] fix out of bounds, divide by zero --- base/src/functions/statistical.rs | 35 +++++++++++++++++++++++++--- base/src/test/test_percentile.rs | 38 +++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/base/src/functions/statistical.rs b/base/src/functions/statistical.rs index 668474a2e..68d32e9f8 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -937,6 +937,22 @@ impl Model { 3 }; let n = values.len() as f64; + + // Handle single element array case + if n == 1.0 { + if (x - values[0]).abs() <= f64::EPSILON { + let factor = 10f64.powi(decimals); + let result = (0.5 * factor).round() / factor; + return CalcResult::Number(result); + } else { + return CalcResult::new_error( + Error::NA, + cell, + "Value not found in single element array".to_string(), + ); + } + } + if x < values[0] { return CalcResult::Number(0.0); } @@ -947,13 +963,26 @@ impl Model { while idx < values.len() && values[idx] < x { idx += 1; } - let rank = if idx == values.len() || (x - values[idx]).abs() > f64::EPSILON { + + // Handle case where idx reaches end of array (should not happen due to bounds check above) + if idx >= values.len() { + return CalcResult::Number(1.0); + } + + let rank = if (x - values[idx]).abs() <= f64::EPSILON { + // Exact match found + idx as f64 + } else { + // Interpolation needed - ensure we don't go out of bounds + if idx == 0 { + // x is between the minimum and the first element, should not happen due to bounds check + return CalcResult::Number(0.0); + } let lower = values[idx - 1]; let upper = values[idx]; (idx as f64 - 1.0) + (x - lower) / (upper - lower) - } else { - idx as f64 }; + let mut result = rank / (n - 1.0); let factor = 10f64.powi(decimals); result = (result * factor).round() / factor; diff --git a/base/src/test/test_percentile.rs b/base/src/test/test_percentile.rs index 1ac97fc37..71e2f0d44 100644 --- a/base/src/test/test_percentile.rs +++ b/base/src/test/test_percentile.rs @@ -27,3 +27,41 @@ fn test_fn_percentrank() { assert_eq!(model._get_text("A1"), *"0.625"); assert_eq!(model._get_text("A2"), *"0.583"); } + +#[test] +fn test_fn_percentrank_inc_single_element() { + let mut model = new_empty_model(); + // Test single element array - should not cause division by zero + model._set("B1", "5.0"); + model._set("A1", "=PERCENTRANK.INC(B1:B1,5.0)"); + model._set("A2", "=PERCENTRANK.INC(B1:B1,3.0)"); + model.evaluate(); + + // For single element array with exact match, should return 0.5 + assert_eq!(model._get_text("A1"), *"0.5"); + // For single element array with no match, should return #N/A error + assert!(model._get_text("A2").contains("#N/A")); +} + +#[test] +fn test_fn_percentrank_inc_boundary_values() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + + // Test values outside the range + model._set("A1", "=PERCENTRANK.INC(B1:B5,0.5)"); // Below minimum + model._set("A2", "=PERCENTRANK.INC(B1:B5,6.0)"); // Above maximum + + // Test exact matches at boundaries + model._set("A3", "=PERCENTRANK.INC(B1:B5,1.0)"); // Exact minimum + model._set("A4", "=PERCENTRANK.INC(B1:B5,5.0)"); // Exact maximum + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0"); // Below min should return 0 + assert_eq!(model._get_text("A2"), *"1"); // Above max should return 1 + assert_eq!(model._get_text("A3"), *"0"); // Exact min should return 0 + assert_eq!(model._get_text("A4"), *"1"); // Exact max should return 1 +} From d3b8d0792045d793bba9812a0fb4cff31a892a91 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 17:37:18 -0700 Subject: [PATCH 4/7] increase test coverage --- src/test/test_percentile.rs | 336 +++++++++++++++++++++++++++++++++++ src/test/test_percentrank.rs | 325 +++++++++++++++++++++++++++++++++ 2 files changed, 661 insertions(+) create mode 100644 src/test/test_percentile.rs create mode 100644 src/test/test_percentrank.rs diff --git a/src/test/test_percentile.rs b/src/test/test_percentile.rs new file mode 100644 index 000000000..6f3a565f4 --- /dev/null +++ b/src/test/test_percentile.rs @@ -0,0 +1,336 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_percentile_inc_basic() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + model._set("A1", "=PERCENTILE.INC(B1:B5,0.4)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"2.6"); +} + +#[test] +fn test_fn_percentile_inc_boundary_k_values() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + + model._set("A1", "=PERCENTILE.INC(B1:B5,0)"); + model._set("A2", "=PERCENTILE.INC(B1:B5,1)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"1"); // Min value + assert_eq!(model._get_text("A2"), *"5"); // Max value +} + +#[test] +fn test_fn_percentile_inc_invalid_k_values() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + + model._set("A1", "=PERCENTILE.INC(B1:B5,-0.1)"); + model._set("A2", "=PERCENTILE.INC(B1:B5,1.1)"); + model._set("A3", "=PERCENTILE.INC(B1:B5,1000.0)"); + model.evaluate(); + + assert!(model._get_text("A1").contains("#NUM!")); + assert!(model._get_text("A2").contains("#NUM!")); + assert!(model._get_text("A3").contains("#NUM!")); +} + +#[test] +fn test_fn_percentile_inc_empty_array() { + let mut model = new_empty_model(); + model._set("A1", "=PERCENTILE.INC(B1:B1,0.5)"); + model.evaluate(); + + assert!(model._get_text("A1").contains("#NUM!")); +} + +#[test] +fn test_fn_percentile_inc_single_element() { + let mut model = new_empty_model(); + model._set("B1", "42"); + model._set("A1", "=PERCENTILE.INC(B1:B1,0.5)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"42"); +} + +#[test] +fn test_fn_percentile_inc_with_duplicates() { + let mut model = new_empty_model(); + // Array with duplicates: [1, 2, 2, 3, 3, 3] + model._set("B1", "1"); + model._set("B2", "2"); + model._set("B3", "2"); + model._set("B4", "3"); + model._set("B5", "3"); + model._set("B6", "3"); + + model._set("A1", "=PERCENTILE.INC(B1:B6,0.5)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"2.5"); +} + +#[test] +fn test_fn_percentile_inc_with_negative_values() { + let mut model = new_empty_model(); + // Array with negative values: [-5, -2, 0, 2, 5] + model._set("B1", "-5"); + model._set("B2", "-2"); + model._set("B3", "0"); + model._set("B4", "2"); + model._set("B5", "5"); + + model._set("A1", "=PERCENTILE.INC(B1:B5,0.5)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0"); +} + +#[test] +fn test_fn_percentile_inc_interpolation() { + let mut model = new_empty_model(); + // Test with array [10, 20, 30, 40] + model._set("B1", "10"); + model._set("B2", "20"); + model._set("B3", "30"); + model._set("B4", "40"); + + model._set("A1", "=PERCENTILE.INC(B1:B4,0.25)"); + model._set("A2", "=PERCENTILE.INC(B1:B4,0.75)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"17.5"); + assert_eq!(model._get_text("A2"), *"32.5"); +} + +#[test] +fn test_fn_percentile_inc_wrong_argument_count() { + let mut model = new_empty_model(); + + model._set("A1", "=PERCENTILE.INC(B1:B5)"); // Missing k + model._set("A2", "=PERCENTILE.INC(B1:B5,0.5,0.1)"); // Too many args + model._set("A3", "=PERCENTILE.INC()"); // No args + model.evaluate(); + + assert!(model._get_text("A1").contains("#ERROR!")); + assert!(model._get_text("A2").contains("#ERROR!")); + assert!(model._get_text("A3").contains("#ERROR!")); +} + +// ============================================================================ +// PERCENTILE.EXC BASIC FUNCTIONALITY TESTS +// ============================================================================ + +#[test] +fn test_fn_percentile_exc_basic() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + model._set("A1", "=PERCENTILE.EXC(B1:B5,0.4)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"2.4"); +} + +#[test] +fn test_fn_percentile_exc_boundary_k_values() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + + // Test k=0 and k=1 for PERCENTILE.EXC (should be errors) + model._set("A1", "=PERCENTILE.EXC(B1:B5,0)"); + model._set("A2", "=PERCENTILE.EXC(B1:B5,1)"); + model.evaluate(); + + assert!(model._get_text("A1").contains("#NUM!")); + assert!(model._get_text("A2").contains("#NUM!")); +} + +#[test] +fn test_fn_percentile_exc_invalid_k_values() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + + model._set("A1", "=PERCENTILE.EXC(B1:B5,-0.1)"); + model._set("A2", "=PERCENTILE.EXC(B1:B5,1.1)"); + model._set("A3", "=PERCENTILE.EXC(B1:B5,1000.0)"); + model.evaluate(); + + assert!(model._get_text("A1").contains("#NUM!")); + assert!(model._get_text("A2").contains("#NUM!")); + assert!(model._get_text("A3").contains("#NUM!")); +} + +#[test] +fn test_fn_percentile_exc_empty_array() { + let mut model = new_empty_model(); + model._set("A1", "=PERCENTILE.EXC(B1:B1,0.5)"); + model.evaluate(); + + assert!(model._get_text("A1").contains("#NUM!")); +} + +#[test] +fn test_fn_percentile_exc_single_element() { + let mut model = new_empty_model(); + model._set("B1", "42"); + model._set("A1", "=PERCENTILE.EXC(B1:B1,0.5)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"42"); +} + +#[test] +fn test_fn_percentile_exc_interpolation() { + let mut model = new_empty_model(); + // Test with array [10, 20, 30, 40] + model._set("B1", "10"); + model._set("B2", "20"); + model._set("B3", "30"); + model._set("B4", "40"); + + model._set("A1", "=PERCENTILE.EXC(B1:B4,0.25)"); + model._set("A2", "=PERCENTILE.EXC(B1:B4,0.75)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"12.5"); + assert_eq!(model._get_text("A2"), *"37.5"); +} + +#[test] +fn test_fn_percentile_exc_with_duplicates() { + let mut model = new_empty_model(); + // Array with duplicates: [1, 2, 2, 3, 3, 3] + model._set("B1", "1"); + model._set("B2", "2"); + model._set("B3", "2"); + model._set("B4", "3"); + model._set("B5", "3"); + model._set("B6", "3"); + + model._set("A1", "=PERCENTILE.EXC(B1:B6,0.5)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"2.5"); +} + +#[test] +fn test_fn_percentile_exc_with_negative_values() { + let mut model = new_empty_model(); + // Array with negative values: [-5, -2, 0, 2, 5] + model._set("B1", "-5"); + model._set("B2", "-2"); + model._set("B3", "0"); + model._set("B4", "2"); + model._set("B5", "5"); + + model._set("A1", "=PERCENTILE.EXC(B1:B5,0.5)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0"); +} + +// ============================================================================ +// MIXED DATA TYPE HANDLING TESTS +// ============================================================================ + +#[test] +fn test_fn_percentile_with_text_data() { + let mut model = new_empty_model(); + model._set("B1", "1"); + model._set("B2", "text"); + model._set("B3", "3"); + model._set("B4", "4"); + model._set("B5", "5"); + + model._set("A1", "=PERCENTILE.INC(B1:B5,0.5)"); + model._set("A2", "=PERCENTILE.EXC(B1:B5,0.5)"); + model.evaluate(); + + // Should ignore text and work with numeric values only + assert_eq!(model._get_text("A1"), *"3.5"); // Median of [1,3,4,5] + assert_eq!(model._get_text("A2"), *"3.5"); +} + +#[test] +fn test_fn_percentile_with_boolean_data() { + let mut model = new_empty_model(); + model._set("B1", "1"); + model._set("B2", "TRUE"); + model._set("B3", "3"); + model._set("B4", "FALSE"); + model._set("B5", "5"); + + model._set("A1", "=PERCENTILE.INC(B1:B5,0.5)"); + model.evaluate(); + + // Should ignore boolean values in ranges + assert_eq!(model._get_text("A1"), *"3"); // Median of [1,3,5] +} + +// ============================================================================ +// ERROR HANDLING AND EDGE CASE TESTS +// ============================================================================ + +#[test] +fn test_fn_percentile_invalid_range() { + let mut model = new_empty_model(); + + model._set("A1", "=PERCENTILE.INC(ZZ999:ZZ1000,0.5)"); + model._set("A2", "=PERCENTILE.EXC(ZZ999:ZZ1000,0.5)"); + model.evaluate(); + + assert!(model._get_text("A1").contains("#")); + assert!(model._get_text("A2").contains("#")); +} + +#[test] +fn test_fn_percentile_very_large_k_values() { + let mut model = new_empty_model(); + for i in 0..3 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + + model._set("A1", "=PERCENTILE.INC(B1:B3,999999)"); + model._set("A2", "=PERCENTILE.EXC(B1:B3,-999999)"); + model.evaluate(); + + assert!(model._get_text("A1").contains("#NUM!")); + assert!(model._get_text("A2").contains("#NUM!")); +} + +// ============================================================================ +// PERFORMANCE AND LARGE DATASET TESTS +// ============================================================================ + +#[test] +fn test_fn_percentile_large_dataset_correctness() { + let mut model = new_empty_model(); + + // Create a larger dataset (100 values) + for i in 1..=100 { + model._set(&format!("B{}", i), &i.to_string()); + } + + model._set("A1", "=PERCENTILE.INC(B1:B100,0.95)"); + model._set("A2", "=PERCENTILE.EXC(B1:B100,0.95)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"95.05"); + assert_eq!(model._get_text("A2"), *"95.95"); +} \ No newline at end of file diff --git a/src/test/test_percentrank.rs b/src/test/test_percentrank.rs new file mode 100644 index 000000000..1ce167e48 --- /dev/null +++ b/src/test/test_percentrank.rs @@ -0,0 +1,325 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +// ============================================================================ +// PERCENTRANK.INC BASIC FUNCTIONALITY TESTS +// ============================================================================ + +#[test] +fn test_fn_percentrank_inc_basic() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + model._set("A1", "=PERCENTRANK.INC(B1:B5,3.5)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"0.625"); +} + +#[test] +fn test_fn_percentrank_inc_boundary_values() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + + model._set("A1", "=PERCENTRANK.INC(B1:B5,0.5)"); // Below minimum + model._set("A2", "=PERCENTRANK.INC(B1:B5,6.0)"); // Above maximum + model._set("A3", "=PERCENTRANK.INC(B1:B5,1.0)"); // Exact minimum + model._set("A4", "=PERCENTRANK.INC(B1:B5,5.0)"); // Exact maximum + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0"); // Below min should return 0 + assert_eq!(model._get_text("A2"), *"1"); // Above max should return 1 + assert_eq!(model._get_text("A3"), *"0"); // Exact min should return 0 + assert_eq!(model._get_text("A4"), *"1"); // Exact max should return 1 +} + +#[test] +fn test_fn_percentrank_inc_single_element() { + let mut model = new_empty_model(); + model._set("B1", "5.0"); + model._set("A1", "=PERCENTRANK.INC(B1:B1,5.0)"); + model._set("A2", "=PERCENTRANK.INC(B1:B1,3.0)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0.5"); + assert!(model._get_text("A2").contains("#N/A")); +} + +#[test] +fn test_fn_percentrank_inc_empty_array() { + let mut model = new_empty_model(); + model._set("A1", "=PERCENTRANK.INC(B1:B1,5)"); + model.evaluate(); + + assert!(model._get_text("A1").contains("#NUM!")); +} + +#[test] +fn test_fn_percentrank_inc_with_duplicates() { + let mut model = new_empty_model(); + // Array with duplicates: [1, 2, 2, 3, 3, 3] + model._set("B1", "1"); + model._set("B2", "2"); + model._set("B3", "2"); + model._set("B4", "3"); + model._set("B5", "3"); + model._set("B6", "3"); + + model._set("A1", "=PERCENTRANK.INC(B1:B6,2)"); + model._set("A2", "=PERCENTRANK.INC(B1:B6,2.5)"); // Interpolation + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0.2"); + assert_eq!(model._get_text("A2"), *"0.5"); +} + +#[test] +fn test_fn_percentrank_inc_with_negative_values() { + let mut model = new_empty_model(); + // Array with negative values: [-5, -2, 0, 2, 5] + model._set("B1", "-5"); + model._set("B2", "-2"); + model._set("B3", "0"); + model._set("B4", "2"); + model._set("B5", "5"); + + model._set("A1", "=PERCENTRANK.INC(B1:B5,-2)"); + model._set("A2", "=PERCENTRANK.INC(B1:B5,0)"); + model._set("A3", "=PERCENTRANK.INC(B1:B5,-3.5)"); // Interpolation + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0.25"); + assert_eq!(model._get_text("A2"), *"0.5"); + assert_eq!(model._get_text("A3"), *"0.125"); +} + +#[test] +fn test_fn_percentrank_inc_exact_vs_interpolated() { + let mut model = new_empty_model(); + // Array [10, 20, 30, 40, 50] + for i in 1..=5 { + model._set(&format!("B{}", i), &(i * 10).to_string()); + } + + model._set("A1", "=PERCENTRANK.INC(B1:B5,30)"); // Exact match + model._set("A2", "=PERCENTRANK.INC(B1:B5,25)"); // Interpolated + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0.5"); + assert_eq!(model._get_text("A2"), *"0.375"); +} + +#[test] +fn test_fn_percentrank_inc_decimals_basic() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + + model._set("A1", "=PERCENTRANK.INC(B1:B5,3.333,1)"); // 1 decimal place + model._set("A2", "=PERCENTRANK.INC(B1:B5,3.333,2)"); // 2 decimal places + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0.6"); + assert_eq!(model._get_text("A2"), *"0.58"); +} + +#[test] +fn test_fn_percentrank_inc_decimals_extreme() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + + model._set("A1", "=PERCENTRANK.INC(B1:B5,3.333,0)"); // 0 decimals + model._set("A2", "=PERCENTRANK.INC(B1:B5,3.333,5)"); // 5 decimals + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"1"); + assert_eq!(model._get_text("A2"), *"0.58325"); // Actual implementation value +} + +#[test] +fn test_fn_percentrank_inc_wrong_argument_count() { + let mut model = new_empty_model(); + for i in 0..3 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + + model._set("A1", "=PERCENTRANK.INC(B1:B3)"); // Missing x + model._set("A2", "=PERCENTRANK.INC(B1:B3,2,3,4)"); // Too many args + model._set("A3", "=PERCENTRANK.INC()"); // No args + model.evaluate(); + + assert!(model._get_text("A1").contains("#ERROR!")); + assert!(model._get_text("A2").contains("#ERROR!")); + assert!(model._get_text("A3").contains("#ERROR!")); +} + +// ============================================================================ +// PERCENTRANK.EXC BASIC FUNCTIONALITY TESTS +// ============================================================================ + +#[test] +fn test_fn_percentrank_exc_basic() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + model._set("A1", "=PERCENTRANK.EXC(B1:B5,3.5)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"0.583"); +} + +#[test] +fn test_fn_percentrank_exc_boundary_values() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + + // Test boundary values for EXC (should be errors at extremes) + model._set("A1", "=PERCENTRANK.EXC(B1:B5,1)"); // Exact minimum + model._set("A2", "=PERCENTRANK.EXC(B1:B5,5)"); // Exact maximum + model._set("A3", "=PERCENTRANK.EXC(B1:B5,0.5)"); // Below minimum + model._set("A4", "=PERCENTRANK.EXC(B1:B5,6)"); // Above maximum + model.evaluate(); + + assert!(model._get_text("A1").contains("#NUM!")); + assert!(model._get_text("A2").contains("#NUM!")); + assert!(model._get_text("A3").contains("#NUM!")); + assert!(model._get_text("A4").contains("#NUM!")); +} + +#[test] +fn test_fn_percentrank_exc_empty_array() { + let mut model = new_empty_model(); + model._set("A1", "=PERCENTRANK.EXC(B1:B1,5)"); + model.evaluate(); + + assert!(model._get_text("A1").contains("#NUM!")); +} + +#[test] +fn test_fn_percentrank_exc_exact_vs_interpolated() { + let mut model = new_empty_model(); + // Array [10, 20, 30, 40, 50] + for i in 1..=5 { + model._set(&format!("B{}", i), &(i * 10).to_string()); + } + + model._set("A1", "=PERCENTRANK.EXC(B1:B5,30)"); // Exact match + model._set("A2", "=PERCENTRANK.EXC(B1:B5,25)"); // Interpolated + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0.5"); + assert_eq!(model._get_text("A2"), *"0.417"); +} + +#[test] +fn test_fn_percentrank_exc_decimals() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + + model._set("A1", "=PERCENTRANK.EXC(B1:B5,3.333,1)"); // 1 decimal + model._set("A2", "=PERCENTRANK.EXC(B1:B5,3.333,3)"); // 3 decimals + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0.6"); + assert_eq!(model._get_text("A2"), *"0.556"); +} + +// ============================================================================ +// MIXED DATA TYPE HANDLING TESTS +// ============================================================================ + +#[test] +fn test_fn_percentrank_with_text_data() { + let mut model = new_empty_model(); + model._set("B1", "1"); + model._set("B2", "text"); + model._set("B3", "3"); + model._set("B4", "4"); + model._set("B5", "5"); + + model._set("A1", "=PERCENTRANK.INC(B1:B5,3)"); + model.evaluate(); + + // Should ignore text and work with numeric values only [1,3,4,5] + assert_eq!(model._get_text("A1"), *"0.333"); +} + +#[test] +fn test_fn_percentrank_with_boolean_data() { + let mut model = new_empty_model(); + model._set("B1", "1"); + model._set("B2", "TRUE"); + model._set("B3", "3"); + model._set("B4", "FALSE"); + model._set("B5", "5"); + + model._set("A1", "=PERCENTRANK.INC(B1:B5,3)"); + model.evaluate(); + + // Should ignore boolean values in ranges [1,3,5] + assert_eq!(model._get_text("A1"), *"0.5"); +} + +// ============================================================================ +// ERROR HANDLING AND EDGE CASE TESTS +// ============================================================================ + +#[test] +fn test_fn_percentrank_invalid_range() { + let mut model = new_empty_model(); + + model._set("A1", "=PERCENTRANK.INC(ZZ999:ZZ1000,5)"); + model._set("A2", "=PERCENTRANK.EXC(ZZ999:ZZ1000,5)"); + model.evaluate(); + + assert!(model._get_text("A1").contains("#")); + assert!(model._get_text("A2").contains("#")); +} + +#[test] +fn test_fn_percentrank_decimal_precision_edge_cases() { + let mut model = new_empty_model(); + for i in 0..5 { + model._set(&format!("B{}", i + 1), &(i + 1).to_string()); + } + + // Test with high precision + model._set("A1", "=PERCENTRANK.INC(B1:B5,3.333333,8)"); + // Test with zero precision + model._set("A2", "=PERCENTRANK.INC(B1:B5,3.1,0)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0.58333325"); // Actual implementation value + assert_eq!(model._get_text("A2"), *"1"); +} + +// ============================================================================ +// PERFORMANCE AND LARGE DATASET TESTS +// ============================================================================ + +#[test] +fn test_fn_percentrank_large_dataset_correctness() { + let mut model = new_empty_model(); + + // Create a larger dataset (100 values) + for i in 1..=100 { + model._set(&format!("B{}", i), &i.to_string()); + } + + model._set("A1", "=PERCENTRANK.INC(B1:B100,95)"); + model._set("A2", "=PERCENTRANK.EXC(B1:B100,95)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0.949"); + assert_eq!(model._get_text("A2"), *"0.941"); +} \ No newline at end of file From 5659005aafbdbcca035fdb6b60290db0b09e366b Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 17:51:22 -0700 Subject: [PATCH 5/7] move test files into right dir --- base/src/test/mod.rs | 1 + {src => base/src}/test/test_percentrank.rs | 68 ++--- src/test/test_percentile.rs | 336 --------------------- 3 files changed, 35 insertions(+), 370 deletions(-) rename {src => base/src}/test/test_percentrank.rs (97%) delete mode 100644 src/test/test_percentile.rs diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index f260ad378..dce83e57f 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -66,6 +66,7 @@ mod test_log; mod test_log10; mod test_percentage; mod test_percentile; +mod test_percentrank; mod test_set_functions_error_handling; mod test_today; mod test_types; diff --git a/src/test/test_percentrank.rs b/base/src/test/test_percentrank.rs similarity index 97% rename from src/test/test_percentrank.rs rename to base/src/test/test_percentrank.rs index 1ce167e48..8e811ee28 100644 --- a/src/test/test_percentrank.rs +++ b/base/src/test/test_percentrank.rs @@ -53,7 +53,7 @@ fn test_fn_percentrank_inc_empty_array() { let mut model = new_empty_model(); model._set("A1", "=PERCENTRANK.INC(B1:B1,5)"); model.evaluate(); - + assert!(model._get_text("A1").contains("#NUM!")); } @@ -67,11 +67,11 @@ fn test_fn_percentrank_inc_with_duplicates() { model._set("B4", "3"); model._set("B5", "3"); model._set("B6", "3"); - + model._set("A1", "=PERCENTRANK.INC(B1:B6,2)"); model._set("A2", "=PERCENTRANK.INC(B1:B6,2.5)"); // Interpolation model.evaluate(); - + assert_eq!(model._get_text("A1"), *"0.2"); assert_eq!(model._get_text("A2"), *"0.5"); } @@ -85,12 +85,12 @@ fn test_fn_percentrank_inc_with_negative_values() { model._set("B3", "0"); model._set("B4", "2"); model._set("B5", "5"); - + model._set("A1", "=PERCENTRANK.INC(B1:B5,-2)"); model._set("A2", "=PERCENTRANK.INC(B1:B5,0)"); model._set("A3", "=PERCENTRANK.INC(B1:B5,-3.5)"); // Interpolation model.evaluate(); - + assert_eq!(model._get_text("A1"), *"0.25"); assert_eq!(model._get_text("A2"), *"0.5"); assert_eq!(model._get_text("A3"), *"0.125"); @@ -103,11 +103,11 @@ fn test_fn_percentrank_inc_exact_vs_interpolated() { for i in 1..=5 { model._set(&format!("B{}", i), &(i * 10).to_string()); } - + model._set("A1", "=PERCENTRANK.INC(B1:B5,30)"); // Exact match model._set("A2", "=PERCENTRANK.INC(B1:B5,25)"); // Interpolated model.evaluate(); - + assert_eq!(model._get_text("A1"), *"0.5"); assert_eq!(model._get_text("A2"), *"0.375"); } @@ -118,11 +118,11 @@ fn test_fn_percentrank_inc_decimals_basic() { for i in 0..5 { model._set(&format!("B{}", i + 1), &(i + 1).to_string()); } - + model._set("A1", "=PERCENTRANK.INC(B1:B5,3.333,1)"); // 1 decimal place model._set("A2", "=PERCENTRANK.INC(B1:B5,3.333,2)"); // 2 decimal places model.evaluate(); - + assert_eq!(model._get_text("A1"), *"0.6"); assert_eq!(model._get_text("A2"), *"0.58"); } @@ -133,11 +133,11 @@ fn test_fn_percentrank_inc_decimals_extreme() { for i in 0..5 { model._set(&format!("B{}", i + 1), &(i + 1).to_string()); } - + model._set("A1", "=PERCENTRANK.INC(B1:B5,3.333,0)"); // 0 decimals model._set("A2", "=PERCENTRANK.INC(B1:B5,3.333,5)"); // 5 decimals model.evaluate(); - + assert_eq!(model._get_text("A1"), *"1"); assert_eq!(model._get_text("A2"), *"0.58325"); // Actual implementation value } @@ -148,12 +148,12 @@ fn test_fn_percentrank_inc_wrong_argument_count() { for i in 0..3 { model._set(&format!("B{}", i + 1), &(i + 1).to_string()); } - + model._set("A1", "=PERCENTRANK.INC(B1:B3)"); // Missing x model._set("A2", "=PERCENTRANK.INC(B1:B3,2,3,4)"); // Too many args model._set("A3", "=PERCENTRANK.INC()"); // No args model.evaluate(); - + assert!(model._get_text("A1").contains("#ERROR!")); assert!(model._get_text("A2").contains("#ERROR!")); assert!(model._get_text("A3").contains("#ERROR!")); @@ -180,14 +180,14 @@ fn test_fn_percentrank_exc_boundary_values() { for i in 0..5 { model._set(&format!("B{}", i + 1), &(i + 1).to_string()); } - + // Test boundary values for EXC (should be errors at extremes) model._set("A1", "=PERCENTRANK.EXC(B1:B5,1)"); // Exact minimum model._set("A2", "=PERCENTRANK.EXC(B1:B5,5)"); // Exact maximum model._set("A3", "=PERCENTRANK.EXC(B1:B5,0.5)"); // Below minimum model._set("A4", "=PERCENTRANK.EXC(B1:B5,6)"); // Above maximum model.evaluate(); - + assert!(model._get_text("A1").contains("#NUM!")); assert!(model._get_text("A2").contains("#NUM!")); assert!(model._get_text("A3").contains("#NUM!")); @@ -199,7 +199,7 @@ fn test_fn_percentrank_exc_empty_array() { let mut model = new_empty_model(); model._set("A1", "=PERCENTRANK.EXC(B1:B1,5)"); model.evaluate(); - + assert!(model._get_text("A1").contains("#NUM!")); } @@ -210,11 +210,11 @@ fn test_fn_percentrank_exc_exact_vs_interpolated() { for i in 1..=5 { model._set(&format!("B{}", i), &(i * 10).to_string()); } - + model._set("A1", "=PERCENTRANK.EXC(B1:B5,30)"); // Exact match model._set("A2", "=PERCENTRANK.EXC(B1:B5,25)"); // Interpolated model.evaluate(); - + assert_eq!(model._get_text("A1"), *"0.5"); assert_eq!(model._get_text("A2"), *"0.417"); } @@ -225,11 +225,11 @@ fn test_fn_percentrank_exc_decimals() { for i in 0..5 { model._set(&format!("B{}", i + 1), &(i + 1).to_string()); } - + model._set("A1", "=PERCENTRANK.EXC(B1:B5,3.333,1)"); // 1 decimal model._set("A2", "=PERCENTRANK.EXC(B1:B5,3.333,3)"); // 3 decimals model.evaluate(); - + assert_eq!(model._get_text("A1"), *"0.6"); assert_eq!(model._get_text("A2"), *"0.556"); } @@ -246,10 +246,10 @@ fn test_fn_percentrank_with_text_data() { model._set("B3", "3"); model._set("B4", "4"); model._set("B5", "5"); - + model._set("A1", "=PERCENTRANK.INC(B1:B5,3)"); model.evaluate(); - + // Should ignore text and work with numeric values only [1,3,4,5] assert_eq!(model._get_text("A1"), *"0.333"); } @@ -260,12 +260,12 @@ fn test_fn_percentrank_with_boolean_data() { model._set("B1", "1"); model._set("B2", "TRUE"); model._set("B3", "3"); - model._set("B4", "FALSE"); + model._set("B4", "FALSE"); model._set("B5", "5"); - + model._set("A1", "=PERCENTRANK.INC(B1:B5,3)"); model.evaluate(); - + // Should ignore boolean values in ranges [1,3,5] assert_eq!(model._get_text("A1"), *"0.5"); } @@ -277,11 +277,11 @@ fn test_fn_percentrank_with_boolean_data() { #[test] fn test_fn_percentrank_invalid_range() { let mut model = new_empty_model(); - + model._set("A1", "=PERCENTRANK.INC(ZZ999:ZZ1000,5)"); model._set("A2", "=PERCENTRANK.EXC(ZZ999:ZZ1000,5)"); model.evaluate(); - + assert!(model._get_text("A1").contains("#")); assert!(model._get_text("A2").contains("#")); } @@ -292,13 +292,13 @@ fn test_fn_percentrank_decimal_precision_edge_cases() { for i in 0..5 { model._set(&format!("B{}", i + 1), &(i + 1).to_string()); } - + // Test with high precision model._set("A1", "=PERCENTRANK.INC(B1:B5,3.333333,8)"); - // Test with zero precision + // Test with zero precision model._set("A2", "=PERCENTRANK.INC(B1:B5,3.1,0)"); model.evaluate(); - + assert_eq!(model._get_text("A1"), *"0.58333325"); // Actual implementation value assert_eq!(model._get_text("A2"), *"1"); } @@ -310,16 +310,16 @@ fn test_fn_percentrank_decimal_precision_edge_cases() { #[test] fn test_fn_percentrank_large_dataset_correctness() { let mut model = new_empty_model(); - + // Create a larger dataset (100 values) for i in 1..=100 { model._set(&format!("B{}", i), &i.to_string()); } - + model._set("A1", "=PERCENTRANK.INC(B1:B100,95)"); model._set("A2", "=PERCENTRANK.EXC(B1:B100,95)"); model.evaluate(); - + assert_eq!(model._get_text("A1"), *"0.949"); assert_eq!(model._get_text("A2"), *"0.941"); -} \ No newline at end of file +} diff --git a/src/test/test_percentile.rs b/src/test/test_percentile.rs deleted file mode 100644 index 6f3a565f4..000000000 --- a/src/test/test_percentile.rs +++ /dev/null @@ -1,336 +0,0 @@ -#![allow(clippy::unwrap_used)] - -use crate::test::util::new_empty_model; - -#[test] -fn test_fn_percentile_inc_basic() { - let mut model = new_empty_model(); - for i in 0..5 { - model._set(&format!("B{}", i + 1), &(i + 1).to_string()); - } - model._set("A1", "=PERCENTILE.INC(B1:B5,0.4)"); - model.evaluate(); - assert_eq!(model._get_text("A1"), *"2.6"); -} - -#[test] -fn test_fn_percentile_inc_boundary_k_values() { - let mut model = new_empty_model(); - for i in 0..5 { - model._set(&format!("B{}", i + 1), &(i + 1).to_string()); - } - - model._set("A1", "=PERCENTILE.INC(B1:B5,0)"); - model._set("A2", "=PERCENTILE.INC(B1:B5,1)"); - model.evaluate(); - - assert_eq!(model._get_text("A1"), *"1"); // Min value - assert_eq!(model._get_text("A2"), *"5"); // Max value -} - -#[test] -fn test_fn_percentile_inc_invalid_k_values() { - let mut model = new_empty_model(); - for i in 0..5 { - model._set(&format!("B{}", i + 1), &(i + 1).to_string()); - } - - model._set("A1", "=PERCENTILE.INC(B1:B5,-0.1)"); - model._set("A2", "=PERCENTILE.INC(B1:B5,1.1)"); - model._set("A3", "=PERCENTILE.INC(B1:B5,1000.0)"); - model.evaluate(); - - assert!(model._get_text("A1").contains("#NUM!")); - assert!(model._get_text("A2").contains("#NUM!")); - assert!(model._get_text("A3").contains("#NUM!")); -} - -#[test] -fn test_fn_percentile_inc_empty_array() { - let mut model = new_empty_model(); - model._set("A1", "=PERCENTILE.INC(B1:B1,0.5)"); - model.evaluate(); - - assert!(model._get_text("A1").contains("#NUM!")); -} - -#[test] -fn test_fn_percentile_inc_single_element() { - let mut model = new_empty_model(); - model._set("B1", "42"); - model._set("A1", "=PERCENTILE.INC(B1:B1,0.5)"); - model.evaluate(); - - assert_eq!(model._get_text("A1"), *"42"); -} - -#[test] -fn test_fn_percentile_inc_with_duplicates() { - let mut model = new_empty_model(); - // Array with duplicates: [1, 2, 2, 3, 3, 3] - model._set("B1", "1"); - model._set("B2", "2"); - model._set("B3", "2"); - model._set("B4", "3"); - model._set("B5", "3"); - model._set("B6", "3"); - - model._set("A1", "=PERCENTILE.INC(B1:B6,0.5)"); - model.evaluate(); - - assert_eq!(model._get_text("A1"), *"2.5"); -} - -#[test] -fn test_fn_percentile_inc_with_negative_values() { - let mut model = new_empty_model(); - // Array with negative values: [-5, -2, 0, 2, 5] - model._set("B1", "-5"); - model._set("B2", "-2"); - model._set("B3", "0"); - model._set("B4", "2"); - model._set("B5", "5"); - - model._set("A1", "=PERCENTILE.INC(B1:B5,0.5)"); - model.evaluate(); - - assert_eq!(model._get_text("A1"), *"0"); -} - -#[test] -fn test_fn_percentile_inc_interpolation() { - let mut model = new_empty_model(); - // Test with array [10, 20, 30, 40] - model._set("B1", "10"); - model._set("B2", "20"); - model._set("B3", "30"); - model._set("B4", "40"); - - model._set("A1", "=PERCENTILE.INC(B1:B4,0.25)"); - model._set("A2", "=PERCENTILE.INC(B1:B4,0.75)"); - model.evaluate(); - - assert_eq!(model._get_text("A1"), *"17.5"); - assert_eq!(model._get_text("A2"), *"32.5"); -} - -#[test] -fn test_fn_percentile_inc_wrong_argument_count() { - let mut model = new_empty_model(); - - model._set("A1", "=PERCENTILE.INC(B1:B5)"); // Missing k - model._set("A2", "=PERCENTILE.INC(B1:B5,0.5,0.1)"); // Too many args - model._set("A3", "=PERCENTILE.INC()"); // No args - model.evaluate(); - - assert!(model._get_text("A1").contains("#ERROR!")); - assert!(model._get_text("A2").contains("#ERROR!")); - assert!(model._get_text("A3").contains("#ERROR!")); -} - -// ============================================================================ -// PERCENTILE.EXC BASIC FUNCTIONALITY TESTS -// ============================================================================ - -#[test] -fn test_fn_percentile_exc_basic() { - let mut model = new_empty_model(); - for i in 0..5 { - model._set(&format!("B{}", i + 1), &(i + 1).to_string()); - } - model._set("A1", "=PERCENTILE.EXC(B1:B5,0.4)"); - model.evaluate(); - assert_eq!(model._get_text("A1"), *"2.4"); -} - -#[test] -fn test_fn_percentile_exc_boundary_k_values() { - let mut model = new_empty_model(); - for i in 0..5 { - model._set(&format!("B{}", i + 1), &(i + 1).to_string()); - } - - // Test k=0 and k=1 for PERCENTILE.EXC (should be errors) - model._set("A1", "=PERCENTILE.EXC(B1:B5,0)"); - model._set("A2", "=PERCENTILE.EXC(B1:B5,1)"); - model.evaluate(); - - assert!(model._get_text("A1").contains("#NUM!")); - assert!(model._get_text("A2").contains("#NUM!")); -} - -#[test] -fn test_fn_percentile_exc_invalid_k_values() { - let mut model = new_empty_model(); - for i in 0..5 { - model._set(&format!("B{}", i + 1), &(i + 1).to_string()); - } - - model._set("A1", "=PERCENTILE.EXC(B1:B5,-0.1)"); - model._set("A2", "=PERCENTILE.EXC(B1:B5,1.1)"); - model._set("A3", "=PERCENTILE.EXC(B1:B5,1000.0)"); - model.evaluate(); - - assert!(model._get_text("A1").contains("#NUM!")); - assert!(model._get_text("A2").contains("#NUM!")); - assert!(model._get_text("A3").contains("#NUM!")); -} - -#[test] -fn test_fn_percentile_exc_empty_array() { - let mut model = new_empty_model(); - model._set("A1", "=PERCENTILE.EXC(B1:B1,0.5)"); - model.evaluate(); - - assert!(model._get_text("A1").contains("#NUM!")); -} - -#[test] -fn test_fn_percentile_exc_single_element() { - let mut model = new_empty_model(); - model._set("B1", "42"); - model._set("A1", "=PERCENTILE.EXC(B1:B1,0.5)"); - model.evaluate(); - - assert_eq!(model._get_text("A1"), *"42"); -} - -#[test] -fn test_fn_percentile_exc_interpolation() { - let mut model = new_empty_model(); - // Test with array [10, 20, 30, 40] - model._set("B1", "10"); - model._set("B2", "20"); - model._set("B3", "30"); - model._set("B4", "40"); - - model._set("A1", "=PERCENTILE.EXC(B1:B4,0.25)"); - model._set("A2", "=PERCENTILE.EXC(B1:B4,0.75)"); - model.evaluate(); - - assert_eq!(model._get_text("A1"), *"12.5"); - assert_eq!(model._get_text("A2"), *"37.5"); -} - -#[test] -fn test_fn_percentile_exc_with_duplicates() { - let mut model = new_empty_model(); - // Array with duplicates: [1, 2, 2, 3, 3, 3] - model._set("B1", "1"); - model._set("B2", "2"); - model._set("B3", "2"); - model._set("B4", "3"); - model._set("B5", "3"); - model._set("B6", "3"); - - model._set("A1", "=PERCENTILE.EXC(B1:B6,0.5)"); - model.evaluate(); - - assert_eq!(model._get_text("A1"), *"2.5"); -} - -#[test] -fn test_fn_percentile_exc_with_negative_values() { - let mut model = new_empty_model(); - // Array with negative values: [-5, -2, 0, 2, 5] - model._set("B1", "-5"); - model._set("B2", "-2"); - model._set("B3", "0"); - model._set("B4", "2"); - model._set("B5", "5"); - - model._set("A1", "=PERCENTILE.EXC(B1:B5,0.5)"); - model.evaluate(); - - assert_eq!(model._get_text("A1"), *"0"); -} - -// ============================================================================ -// MIXED DATA TYPE HANDLING TESTS -// ============================================================================ - -#[test] -fn test_fn_percentile_with_text_data() { - let mut model = new_empty_model(); - model._set("B1", "1"); - model._set("B2", "text"); - model._set("B3", "3"); - model._set("B4", "4"); - model._set("B5", "5"); - - model._set("A1", "=PERCENTILE.INC(B1:B5,0.5)"); - model._set("A2", "=PERCENTILE.EXC(B1:B5,0.5)"); - model.evaluate(); - - // Should ignore text and work with numeric values only - assert_eq!(model._get_text("A1"), *"3.5"); // Median of [1,3,4,5] - assert_eq!(model._get_text("A2"), *"3.5"); -} - -#[test] -fn test_fn_percentile_with_boolean_data() { - let mut model = new_empty_model(); - model._set("B1", "1"); - model._set("B2", "TRUE"); - model._set("B3", "3"); - model._set("B4", "FALSE"); - model._set("B5", "5"); - - model._set("A1", "=PERCENTILE.INC(B1:B5,0.5)"); - model.evaluate(); - - // Should ignore boolean values in ranges - assert_eq!(model._get_text("A1"), *"3"); // Median of [1,3,5] -} - -// ============================================================================ -// ERROR HANDLING AND EDGE CASE TESTS -// ============================================================================ - -#[test] -fn test_fn_percentile_invalid_range() { - let mut model = new_empty_model(); - - model._set("A1", "=PERCENTILE.INC(ZZ999:ZZ1000,0.5)"); - model._set("A2", "=PERCENTILE.EXC(ZZ999:ZZ1000,0.5)"); - model.evaluate(); - - assert!(model._get_text("A1").contains("#")); - assert!(model._get_text("A2").contains("#")); -} - -#[test] -fn test_fn_percentile_very_large_k_values() { - let mut model = new_empty_model(); - for i in 0..3 { - model._set(&format!("B{}", i + 1), &(i + 1).to_string()); - } - - model._set("A1", "=PERCENTILE.INC(B1:B3,999999)"); - model._set("A2", "=PERCENTILE.EXC(B1:B3,-999999)"); - model.evaluate(); - - assert!(model._get_text("A1").contains("#NUM!")); - assert!(model._get_text("A2").contains("#NUM!")); -} - -// ============================================================================ -// PERFORMANCE AND LARGE DATASET TESTS -// ============================================================================ - -#[test] -fn test_fn_percentile_large_dataset_correctness() { - let mut model = new_empty_model(); - - // Create a larger dataset (100 values) - for i in 1..=100 { - model._set(&format!("B{}", i), &i.to_string()); - } - - model._set("A1", "=PERCENTILE.INC(B1:B100,0.95)"); - model._set("A2", "=PERCENTILE.EXC(B1:B100,0.95)"); - model.evaluate(); - - assert_eq!(model._get_text("A1"), *"95.05"); - assert_eq!(model._get_text("A2"), *"95.95"); -} \ No newline at end of file From 3ad9929ad82684a0b458c944e8916a78bf6a641e Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 17:56:37 -0700 Subject: [PATCH 6/7] fix build --- base/src/test/test_percentrank.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/base/src/test/test_percentrank.rs b/base/src/test/test_percentrank.rs index 8e811ee28..62ced0dc9 100644 --- a/base/src/test/test_percentrank.rs +++ b/base/src/test/test_percentrank.rs @@ -101,7 +101,7 @@ fn test_fn_percentrank_inc_exact_vs_interpolated() { let mut model = new_empty_model(); // Array [10, 20, 30, 40, 50] for i in 1..=5 { - model._set(&format!("B{}", i), &(i * 10).to_string()); + model._set(&format!("B{i}"), &(i * 10).to_string()); } model._set("A1", "=PERCENTRANK.INC(B1:B5,30)"); // Exact match @@ -208,7 +208,7 @@ fn test_fn_percentrank_exc_exact_vs_interpolated() { let mut model = new_empty_model(); // Array [10, 20, 30, 40, 50] for i in 1..=5 { - model._set(&format!("B{}", i), &(i * 10).to_string()); + model._set(&format!("B{i}"), &(i * 10).to_string()); } model._set("A1", "=PERCENTRANK.EXC(B1:B5,30)"); // Exact match @@ -313,7 +313,7 @@ fn test_fn_percentrank_large_dataset_correctness() { // Create a larger dataset (100 values) for i in 1..=100 { - model._set(&format!("B{}", i), &i.to_string()); + model._set(&format!("B{i}"), &i.to_string()); } model._set("A1", "=PERCENTRANK.INC(B1:B100,95)"); From b16e338f854e0124c8c1fe64ccde11980de1475b Mon Sep 17 00:00:00 2001 From: BrianHung Date: Fri, 6 Feb 2026 01:26:51 -0800 Subject: [PATCH 7/7] Fix clippy unwraps in percentile functions Avoid Option unwraps by indexing validated non-empty arrays in percentile and percentrank helpers. --- base/src/functions/statistical/percentile.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/base/src/functions/statistical/percentile.rs b/base/src/functions/statistical/percentile.rs index 7974b41ca..f76425da0 100644 --- a/base/src/functions/statistical/percentile.rs +++ b/base/src/functions/statistical/percentile.rs @@ -83,7 +83,11 @@ impl<'a> Model<'a> { }; if !(0.0..=1.0).contains(&k) { - return CalcResult::new_error(Error::NUM, cell, "k must be between 0 and 1".to_string()); + return CalcResult::new_error( + Error::NUM, + cell, + "k must be between 0 and 1".to_string(), + ); } let n = values.len() as f64; @@ -93,7 +97,7 @@ impl<'a> Model<'a> { let idx = (m as usize).saturating_sub(1); if idx >= values.len() - 1 { - return CalcResult::Number(*values.last().unwrap()); + return CalcResult::Number(values[values.len() - 1]); } let result = values[idx] + g * (values[idx + 1] - values[idx]); @@ -139,7 +143,11 @@ impl<'a> Model<'a> { let pos = k * (n + 1.0); if pos < 1.0 || pos > n { - return CalcResult::new_error(Error::NUM, cell, "k out of range for data size".to_string()); + return CalcResult::new_error( + Error::NUM, + cell, + "k out of range for data size".to_string(), + ); } let m = pos.floor(); @@ -147,7 +155,7 @@ impl<'a> Model<'a> { let idx = (m as usize).saturating_sub(1); if idx >= values.len() - 1 { - return CalcResult::Number(*values.last().unwrap()); + return CalcResult::Number(values[values.len() - 1]); } let result = values[idx] + g * (values[idx + 1] - values[idx]); @@ -208,7 +216,7 @@ impl<'a> Model<'a> { if x < values[0] { return CalcResult::Number(0.0); } - if x > *values.last().unwrap() { + if x > values[values.len() - 1] { return CalcResult::Number(1.0); }