Skip to content
Open
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
8 changes: 6 additions & 2 deletions base/src/expressions/parser/static_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,9 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Randbetween => args_signature_scalars(arg_count, 2, 0),
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::Geomean | Function::Harmean | Function::Avedev | Function::Devsq => {
vec![Signature::Vector; arg_count]
}
}
}

Expand Down Expand Up @@ -989,6 +991,8 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Randbetween => scalar_arguments(args),
Function::Eomonth => scalar_arguments(args),
Function::Formulatext => not_implemented(args),
Function::Geomean => not_implemented(args),
Function::Geomean | Function::Harmean | Function::Avedev | Function::Devsq => {
not_implemented(args)
}
}
}
17 changes: 16 additions & 1 deletion base/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ pub enum Function {
Maxifs,
Minifs,
Geomean,
Harmean,
Avedev,
Devsq,

// Date and time
Date,
Expand Down Expand Up @@ -253,7 +256,7 @@ pub enum Function {
}

impl Function {
pub fn into_iter() -> IntoIter<Function, 198> {
pub fn into_iter() -> IntoIter<Function, 201> {
[
Function::And,
Function::False,
Expand Down Expand Up @@ -357,6 +360,9 @@ impl Function {
Function::Maxifs,
Function::Minifs,
Function::Geomean,
Function::Harmean,
Function::Avedev,
Function::Devsq,
Function::Year,
Function::Day,
Function::Month,
Expand Down Expand Up @@ -625,6 +631,9 @@ impl Function {
"MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs),
"MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs),
"GEOMEAN" => Some(Function::Geomean),
"HARMEAN" => Some(Function::Harmean),
"AVEDEV" => Some(Function::Avedev),
"DEVSQ" => Some(Function::Devsq),
// Date and Time
"YEAR" => Some(Function::Year),
"DAY" => Some(Function::Day),
Expand Down Expand Up @@ -836,6 +845,9 @@ impl fmt::Display for Function {
Function::Maxifs => write!(f, "MAXIFS"),
Function::Minifs => write!(f, "MINIFS"),
Function::Geomean => write!(f, "GEOMEAN"),
Function::Harmean => write!(f, "HARMEAN"),
Function::Avedev => write!(f, "AVEDEV"),
Function::Devsq => write!(f, "DEVSQ"),
Function::Year => write!(f, "YEAR"),
Function::Day => write!(f, "DAY"),
Function::Month => write!(f, "MONTH"),
Expand Down Expand Up @@ -1076,6 +1088,9 @@ impl Model {
Function::Maxifs => self.fn_maxifs(args, cell),
Function::Minifs => self.fn_minifs(args, cell),
Function::Geomean => self.fn_geomean(args, cell),
Function::Harmean => self.fn_harmean(args, cell),
Function::Avedev => self.fn_avedev(args, cell),
Function::Devsq => self.fn_devsq(args, cell),
// Date and Time
Function::Year => self.fn_year(args, cell),
Function::Day => self.fn_day(args, cell),
Expand Down
173 changes: 173 additions & 0 deletions base/src/functions/statistical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,108 @@ use crate::{

use super::util::build_criteria;

/// Helper function to extract numeric values from arguments, with optional positive-only validation
fn extract_numeric_values(
model: &mut Model,
args: &[Node],
cell: CellReferenceIndex,
positive_only: bool,
) -> Result<Vec<f64>, CalcResult> {
let mut values = Vec::new();

for arg in args {
match model.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
if positive_only && value <= 0.0 {
return Err(CalcResult::new_error(
Error::NUM,
cell,
"Values must be positive".to_string(),
));
}
values.push(value);
}
CalcResult::Boolean(b) => {
if let Node::ReferenceKind { .. } = arg {
// Skip booleans in cell references
} else {
let value = if b { 1.0 } else { 0.0 };
if positive_only && value <= 0.0 {
return Err(CalcResult::new_error(
Error::NUM,
cell,
"Values must be positive".to_string(),
));
}
values.push(value);
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return Err(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 model.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
if positive_only && value <= 0.0 {
return Err(CalcResult::new_error(
Error::NUM,
cell,
"Values must be positive".to_string(),
));
}
values.push(value);
}
error @ CalcResult::Error { .. } => return Err(error),
CalcResult::Range { .. } => {
return Err(CalcResult::new_error(
Error::ERROR,
cell,
"Unexpected Range".to_string(),
));
}
_ => {}
}
}
}
}
error @ CalcResult::Error { .. } => return Err(error),
CalcResult::String(s) => {
if let Node::ReferenceKind { .. } = arg {
// Skip strings in cell references
} else if let Ok(value) = s.parse::<f64>() {
if positive_only && value <= 0.0 {
return Err(CalcResult::new_error(
Error::NUM,
cell,
"Values must be positive".to_string(),
));
}
values.push(value);
} else {
return Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Argument cannot be cast into number".to_string(),
});
}
}
_ => {}
}
}

Ok(values)
}

impl Model {
pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
Expand Down Expand Up @@ -730,4 +832,75 @@ impl Model {
}
CalcResult::Number(product.powf(1.0 / count))
}

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

let values = match extract_numeric_values(self, args, cell, true) {
Ok(v) => v,
Err(error) => return error,
};

if values.is_empty() {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Division by Zero".to_string(),
};
}

let count = values.len() as f64;
let sum_recip: f64 = values.iter().map(|v| 1.0 / v).sum();
CalcResult::Number(count / sum_recip)
}

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

let values = match extract_numeric_values(self, args, cell, false) {
Ok(v) => v,
Err(error) => return error,
};

if values.is_empty() {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Division by Zero".to_string(),
};
}

let count = values.len() as f64;
let mean: f64 = values.iter().sum::<f64>() / count;
let total: f64 = values.iter().map(|v| (v - mean).abs()).sum();
CalcResult::Number(total / count)
}

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

let values = match extract_numeric_values(self, args, cell, false) {
Ok(v) => v,
Err(error) => return error,
};

if values.is_empty() {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Division by Zero".to_string(),
};
}

let count = values.len() as f64;
let mean: f64 = values.iter().sum::<f64>() / count;
let devsq: f64 = values.iter().map(|v| (v - mean).powi(2)).sum();
CalcResult::Number(devsq)
}
}
3 changes: 3 additions & 0 deletions base/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ mod test_number_format;
mod test_arrays;
mod test_escape_quotes;
mod test_extend;
mod test_fn_avedev;
mod test_fn_devsq;
mod test_fn_fv;
mod test_fn_harmean;
mod test_fn_type;
mod test_frozen_rows_and_columns;
mod test_geomean;
Expand Down
53 changes: 53 additions & 0 deletions base/src/test/test_fn_avedev.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#![allow(clippy::unwrap_used)]

use crate::test::util::new_empty_model;

#[test]
fn test_fn_avedev_arguments() {
let mut model = new_empty_model();
model._set("A1", "=AVEDEV()");
model.evaluate();

assert_eq!(model._get_text("A1"), *"#ERROR!");
}

#[test]
fn test_fn_avedev_minimal() {
let mut model = new_empty_model();
model._set("B1", "1");
model._set("B2", "2");
model._set("B3", "3");
model._set("B4", "'2");
model._set("B6", "true");
model._set("A1", "=AVEDEV(B1:B6)");
model.evaluate();

assert_eq!(model._get_text("A1"), *"0.666666667");
}

#[test]
fn test_fn_avedev_mathematical_validation() {
let mut model = new_empty_model();
// Test with values 2, 4, 6, 8
// Mean = 5, deviations: |2-5|=3, |4-5|=1, |6-5|=1, |8-5|=3
// Average deviation = (3+1+1+3)/4 = 2
model._set("B1", "2");
model._set("B2", "4");
model._set("B3", "6");
model._set("B4", "8");
model._set("A1", "=AVEDEV(B1:B4)");
model.evaluate();

assert_eq!(model._get_text("A1"), *"2");
}

#[test]
fn test_fn_avedev_single_value() {
let mut model = new_empty_model();
model._set("B1", "10");
model._set("A1", "=AVEDEV(B1)");
model.evaluate();

// Single value has zero deviation from its own mean
assert_eq!(model._get_text("A1"), *"0");
}
55 changes: 55 additions & 0 deletions base/src/test/test_fn_devsq.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#![allow(clippy::unwrap_used)]

use crate::test::util::new_empty_model;

#[test]
fn test_fn_devsq_arguments() {
let mut model = new_empty_model();
model._set("A1", "=DEVSQ()");
model.evaluate();

assert_eq!(model._get_text("A1"), *"#ERROR!");
}

#[test]
fn test_fn_devsq_minimal() {
let mut model = new_empty_model();
// Data from mathematical example: 4,5,8,7,11,4,3 -> result 48
model._set("B1", "4");
model._set("B2", "5");
model._set("B3", "8");
model._set("B4", "7");
model._set("B5", "11");
model._set("B6", "4");
model._set("B7", "3");
model._set("A1", "=DEVSQ(B1:B7)");
model.evaluate();

assert_eq!(model._get_text("A1"), *"48");
}

#[test]
fn test_fn_devsq_simple_validation() {
let mut model = new_empty_model();
// Test with values 1, 3, 5
// Mean = 3, deviations: (1-3)²=4, (3-3)²=0, (5-3)²=4
// Sum of squared deviations = 4+0+4 = 8
model._set("B1", "1");
model._set("B2", "3");
model._set("B3", "5");
model._set("A1", "=DEVSQ(B1:B3)");
model.evaluate();

assert_eq!(model._get_text("A1"), *"8");
}

#[test]
fn test_fn_devsq_single_value() {
let mut model = new_empty_model();
model._set("B1", "10");
model._set("A1", "=DEVSQ(B1)");
model.evaluate();

// Single value has zero squared deviation from its own mean
assert_eq!(model._get_text("A1"), *"0");
}
Loading