Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions base/src/calc_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ impl CalcResult {
pub fn is_error(&self) -> bool {
matches!(self, CalcResult::Error { .. })
}

pub fn into_number(self) -> Option<f64> {
match self {
Self::Number(s) => Some(s.to_owned()),
_ => None,
}
}
}

impl Ord for CalcResult {
Expand Down
2 changes: 2 additions & 0 deletions base/src/expressions/parser/static_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Formulatext => 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),
}
}

Expand Down Expand Up @@ -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),
Expand Down
44 changes: 44 additions & 0 deletions base/src/functions/mathematical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
7 changes: 6 additions & 1 deletion base/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pub enum Function {
Cosh,
Max,
Min,
PercentOf,
Pi,
Power,
Product,
Expand Down Expand Up @@ -250,7 +251,7 @@ pub enum Function {
}

impl Function {
pub fn into_iter() -> IntoIter<Function, 195> {
pub fn into_iter() -> IntoIter<Function, 196> {
[
Function::And,
Function::False,
Expand Down Expand Up @@ -283,6 +284,7 @@ impl Function {
Function::Power,
Function::Max,
Function::Min,
Function::PercentOf,
Function::Product,
Function::Rand,
Function::Randbetween,
Expand Down Expand Up @@ -715,6 +717,7 @@ impl Function {
"GESTEP" => Some(Function::Gestep),

"SUBTOTAL" => Some(Function::Subtotal),
"PERCENTOF" => Some(Function::PercentOf),
_ => None,
}
}
Expand Down Expand Up @@ -920,6 +923,7 @@ impl fmt::Display for Function {
Function::Gestep => write!(f, "GESTEP"),

Function::Subtotal => write!(f, "SUBTOTAL"),
Function::PercentOf => write!(f, "PERCENTOF"),
}
}
}
Expand Down Expand Up @@ -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),
Expand Down
24 changes: 24 additions & 0 deletions base/src/test/test_fn_percentof.rs
Original file line number Diff line number Diff line change
@@ -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")
}