Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
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 @@ -661,6 +661,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Sum => vec![Signature::Vector; arg_count],
Function::Sumif => args_signature_sumif(arg_count),
Function::Sumifs => vec![Signature::Vector; arg_count],
Function::Sumproduct => vec![Signature::Vector; arg_count],
Function::Tan => args_signature_scalars(arg_count, 1, 0),
Function::Tanh => args_signature_scalars(arg_count, 1, 0),
Function::ErrorType => args_signature_scalars(arg_count, 1, 0),
Expand Down Expand Up @@ -1055,6 +1056,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Sum => StaticResult::Scalar,
Function::Sumif => not_implemented(args),
Function::Sumifs => not_implemented(args),
Function::Sumproduct => scalar_arguments(args),
Function::Tan => scalar_arguments(args),
Function::Tanh => scalar_arguments(args),
Function::ErrorType => not_implemented(args),
Expand Down
5 changes: 5 additions & 0 deletions base/src/formatter/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,11 @@ fn parse_number(
} else {
1.0
};

if bytes[position] == group_separator {
return Err("Cannot parse number".to_string());
}

// numbers before the decimal point
while position < len {
let x = bytes[position];
Expand Down
106 changes: 106 additions & 0 deletions base/src/functions/mathematical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,112 @@ impl<'a> Model<'a> {
CalcResult::Number(result)
}

pub(crate) fn fn_sumproduct(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}

enum Arg {
Scalar(f64),
Array(Vec<Vec<f64>>),
}

let mut processed: Vec<Arg> = Vec::new();
let mut rows: usize = 1;
let mut cols: usize = 1;
let mut have_matrix = false;

for arg in args {
match self.get_number_or_array(arg, cell) {
Ok(NumberOrArray::Number(n)) => processed.push(Arg::Scalar(n)),
Ok(NumberOrArray::Array(a)) => {
let r = a.len();
let c = a.first().map(|row| row.len()).unwrap_or(0);

if r == 0 || c == 0 {
return CalcResult::new_error(
Error::VALUE,
cell,
"Empty arrays are not supported in SUMPRODUCT".to_string(),
);
}

let mut arr = Vec::with_capacity(r);
for row in a {
let mut row_vec = Vec::with_capacity(row.len());
for value in row {
match value {
ArrayNode::Number(n) => row_vec.push(n),
ArrayNode::Boolean(b) => row_vec.push(if b { 1.0 } else { 0.0 }),
ArrayNode::String(s) => match s.parse::<f64>() {
Ok(f) => row_vec.push(f),
Err(_) => row_vec.push(0.0),
},
ArrayNode::Error(e) => {
return CalcResult::Error {
error: e,
origin: cell,
message: "Error in array".to_string(),
};
}
}
}
arr.push(row_vec);
}

if !have_matrix {
rows = r;
cols = c;
have_matrix = true;
} else if r != rows || c != cols {
return CalcResult::new_error(
Error::VALUE,
cell,
"Array dimensions do not match".to_string(),
);
}
processed.push(Arg::Array(arr));
}
Err(e) => return e,
}
}

if !have_matrix {
let mut prod = 1.0;
for p in processed {
match p {
Arg::Scalar(n) => prod *= n,
Arg::Array(_) => unreachable!(),
}
}
return CalcResult::Number(prod);
}

let mut total = 0.0;
for i in 0..rows {
for j in 0..cols {
let mut prod = 1.0;
for p in processed.iter() {
match p {
Arg::Scalar(n) => prod *= *n,
Arg::Array(a) => {
if i >= a.len() || j >= a[i].len() {
return CalcResult::new_error(
Error::VALUE,
cell,
"Array index out of bounds".to_string(),
);
}
prod *= a[i][j];
}
}
}
total += prod;
}
}
CalcResult::Number(total)
}

/// SUMIF(criteria_range, criteria, [sum_range])
/// if sum_rage is missing then criteria_range will be used
pub(crate) fn fn_sumif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
Expand Down
11 changes: 10 additions & 1 deletion base/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ pub enum Function {
Sum,
Sumif,
Sumifs,
Sumproduct,
Sumx2my2,
Sumx2py2,
Sumxmy2,
Expand Down Expand Up @@ -428,6 +429,11 @@ macro_rules! impl_function_lookup {
impl Functions {
pub fn lookup(&self, name: &str) -> Option<Function> {
let key = name.to_uppercase();
// New functions without localization support
match key.as_str() {
"SUMPRODUCT" => return Some(Function::Sumproduct),
_ => {}
}
$(
if self.$field == key {
return Some(Function::$variant);
Expand Down Expand Up @@ -865,6 +871,7 @@ impl Function {
Function::Sum => functions.sum.clone(),
Function::Sumif => functions.sumif.clone(),
Function::Sumifs => functions.sumifs.clone(),
Function::Sumproduct => "SUMPRODUCT".to_string(),
Function::Sumx2my2 => functions.sumx2my2.clone(),
Function::Sumx2py2 => functions.sumx2py2.clone(),
Function::Sumxmy2 => functions.sumxmy2.clone(),
Expand Down Expand Up @@ -1168,7 +1175,7 @@ impl Function {
Function::Steyx => functions.steyx.clone(),
}
}
pub fn into_iter() -> IntoIter<Function, 345> {
pub fn into_iter() -> IntoIter<Function, 346> {
[
Function::And,
Function::False,
Expand Down Expand Up @@ -1245,6 +1252,7 @@ impl Function {
Function::Sum,
Function::Sumif,
Function::Sumifs,
Function::Sumproduct,
Function::Sumx2my2,
Function::Sumx2py2,
Function::Sumxmy2,
Expand Down Expand Up @@ -1704,6 +1712,7 @@ impl<'a> Model<'a> {
Function::Sum => self.fn_sum(args, cell),
Function::Sumif => self.fn_sumif(args, cell),
Function::Sumifs => self.fn_sumifs(args, cell),
Function::Sumproduct => self.fn_sumproduct(args, cell),
Function::Choose => self.fn_choose(args, cell),
Function::Column => self.fn_column(args, cell),
Function::Columns => self.fn_columns(args, cell),
Expand Down
39 changes: 23 additions & 16 deletions base/src/functions/statistical/chisq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
const MAX_DEGREES_OF_FREEDOM: f64 = 10_000_000_000.0;

impl<'a> Model<'a> {
// CHISQ.DIST(x, deg_freedom, cumulative)
Expand All @@ -13,12 +14,12 @@ impl<'a> Model<'a> {
return CalcResult::new_args_number_error(cell);
}

let x = match self.get_number_no_bools(&args[0], cell) {
let x = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};

let df = match self.get_number_no_bools(&args[1], cell) {
let df = match self.get_number(&args[1], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
Expand All @@ -35,11 +36,12 @@ impl<'a> Model<'a> {
"x must be >= 0 in CHISQ.DIST".to_string(),
);
}
if df < 1.0 {
// if degrees of freedom < 1 or > 10^10 → #NUM!
if !(1.0..=MAX_DEGREES_OF_FREEDOM).contains(&df) {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in CHISQ.DIST".to_string(),
"degrees of freedom must be in [1, 10^10] in CHISQ.DIST".to_string(),
);
}

Expand Down Expand Up @@ -77,12 +79,12 @@ impl<'a> Model<'a> {
return CalcResult::new_args_number_error(cell);
}

let x = match self.get_number_no_bools(&args[0], cell) {
let x = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};

let df_raw = match self.get_number_no_bools(&args[1], cell) {
let df_raw = match self.get_number(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
Expand All @@ -96,11 +98,13 @@ impl<'a> Model<'a> {
"x must be >= 0 in CHISQ.DIST.RT".to_string(),
);
}
if df < 1.0 {

// if degrees of freedom < 1 or > 10^10 → #NUM!
if !(1.0..=MAX_DEGREES_OF_FREEDOM).contains(&df) {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in CHISQ.DIST.RT".to_string(),
"degrees of freedom must be in [1, 10^10] in CHISQ.DIST.RT".to_string(),
);
}

Expand Down Expand Up @@ -136,12 +140,12 @@ impl<'a> Model<'a> {
return CalcResult::new_args_number_error(cell);
}

let p = match self.get_number_no_bools(&args[0], cell) {
let p = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};

let df = match self.get_number_no_bools(&args[1], cell) {
let df = match self.get_number(&args[1], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
Expand All @@ -154,11 +158,13 @@ impl<'a> Model<'a> {
"probability must be in [0,1] in CHISQ.INV".to_string(),
);
}
if df < 1.0 {

// if degrees of freedom < 1 or > 10^10 → #NUM!
if !(1.0..=MAX_DEGREES_OF_FREEDOM).contains(&df) {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in CHISQ.INV".to_string(),
"degrees of freedom must be in [1, 10^10] in CHISQ.INV".to_string(),
);
}

Expand Down Expand Up @@ -196,12 +202,12 @@ impl<'a> Model<'a> {
return CalcResult::new_args_number_error(cell);
}

let p = match self.get_number_no_bools(&args[0], cell) {
let p = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};

let df_raw = match self.get_number_no_bools(&args[1], cell) {
let df_raw = match self.get_number(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
Expand All @@ -216,11 +222,12 @@ impl<'a> Model<'a> {
"probability must be in [0,1] in CHISQ.INV.RT".to_string(),
);
}
if df < 1.0 {
// if degrees of freedom < 1 or > 10^10 → #NUM!
if !(1.0..=MAX_DEGREES_OF_FREEDOM).contains(&df) {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in CHISQ.INV.RT".to_string(),
"degrees of freedom must be in [1, 10^10] in CHISQ.INV.RT".to_string(),
);
}

Expand Down
2 changes: 2 additions & 0 deletions base/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ mod test_fn_or_xor;
mod test_fn_product;
mod test_fn_rept;
mod test_fn_sum;
mod test_fn_sumif;
mod test_fn_sumifs;
mod test_fn_sumproduct;
mod test_fn_textbefore;
mod test_fn_textjoin;
mod test_fn_time;
Expand Down
Loading