diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a07f371..4c129653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Add python bindings for all platforms - Add is split into the product and widget - Add Python documentation [#260] +- New function PERCENTOF ([]()) ### Fixed diff --git a/base/src/calc_result.rs b/base/src/calc_result.rs index 1691a820..ca5ca970 100644 --- a/base/src/calc_result.rs +++ b/base/src/calc_result.rs @@ -45,6 +45,13 @@ impl CalcResult { pub fn is_error(&self) -> bool { matches!(self, CalcResult::Error { .. }) } + + pub fn into_number(self) -> Option { + match self { + Self::Number(s) => Some(s.to_owned()), + _ => None, + } + } } impl Ord for CalcResult { diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 280ac248..2f696818 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -778,6 +778,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::PercentOf => args_signature_scalars(arg_count, 2, 0), } } @@ -810,6 +811,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Cosh => scalar_arguments(args), Function::Max => StaticResult::Scalar, Function::Min => StaticResult::Scalar, + Function::PercentOf => StaticResult::Scalar, Function::Pi => StaticResult::Scalar, Function::Power => scalar_arguments(args), Function::Product => not_implemented(args), diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index 82f4b8b4..211b29b3 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -501,4 +501,48 @@ impl Model { } CalcResult::Number((x + random() * (y - x)).floor()) } + + pub(crate) fn fn_percentof(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + // Function is `=PERCENTOF(array1, array2)` == `=PERCENTOF((=SUM(subset) / =SUM(total)) * 100)` + // Implementation is the same as in Excel docs found here: + // https://support.microsoft.com/en-us/office/percentof-function-7c66da0a-ac30-45d0-bfc7-834a8bd7c962 + // `Note: PERCENTOF is logically equivalent to =SUM(data_subset)/SUM(data_all)` + // They rely on formatting for converting into a percentage e.g. 0.1 = SUM(10) / SUM(100) before formatting + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + + let subset_array: CalcResult = self.fn_sum(&[args[0].clone()], cell); + let total_array: CalcResult = self.fn_sum(&[args[1].clone()], cell); + + let subset_total = subset_array.into_number(); + + let subset_value = match subset_total { + Some(number) => number, + None => { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "SUM for Subset Total cannot be calculated, results in a null value!" + .to_string(), + } + } + }; + + let total_total = total_array.into_number(); + + let total_value = match total_total { + Some(number) => number, + None => { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "SUM for Total cannot be calculated, results in a null value!" + .to_string(), + } + } + }; + + CalcResult::Number(subset_value / total_value) + } } diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index fa255607..a5a0875d 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -56,6 +56,7 @@ pub enum Function { Cosh, Max, Min, + PercentOf, Pi, Power, Product, @@ -250,7 +251,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -283,6 +284,7 @@ impl Function { Function::Power, Function::Max, Function::Min, + Function::PercentOf, Function::Product, Function::Rand, Function::Randbetween, @@ -715,6 +717,7 @@ impl Function { "GESTEP" => Some(Function::Gestep), "SUBTOTAL" => Some(Function::Subtotal), + "PERCENTOF" => Some(Function::PercentOf), _ => None, } } @@ -920,6 +923,7 @@ impl fmt::Display for Function { Function::Gestep => write!(f, "GESTEP"), Function::Subtotal => write!(f, "SUBTOTAL"), + Function::PercentOf => write!(f, "PERCENTOF"), } } } @@ -987,6 +991,7 @@ impl Model { Function::Max => self.fn_max(args, cell), Function::Min => self.fn_min(args, cell), + Function::PercentOf => self.fn_percentof(args, cell), Function::Product => self.fn_product(args, cell), Function::Rand => self.fn_rand(args, cell), Function::Randbetween => self.fn_randbetween(args, cell), diff --git a/base/src/test/test_fn_percentof.rs b/base/src/test/test_fn_percentof.rs new file mode 100644 index 00000000..bd386ef5 --- /dev/null +++ b/base/src/test/test_fn_percentof.rs @@ -0,0 +1,24 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_percentof_arguments() { + let mut model = new_empty_model(); + // Incorrect number of arguments + model._set("A1", "=PERCENTOF()"); + model._set("A2", "=PERCENTOF(10)"); + + // Correct use of function + model._set("A3", "=PERCENTOF(10,100)"); + model._set("A4", "=PERCENTOF(500,1000"); + + model.evaluate(); + // Error (Incorrect number of arguments) + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + + // Success + assert_eq!(model._get_text("A3"), *"0.1"); + assert_eq!(model._get_text("A4"), *"0.5") +}