Skip to content
Closed
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
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 @@ -785,6 +785,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::Intercept | Function::Slope => vec![Signature::Vector; arg_count],
}
}

Expand Down Expand Up @@ -990,5 +991,6 @@ 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::Intercept | Function::Slope => scalar_arguments(args),
}
}
12 changes: 11 additions & 1 deletion base/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ pub enum Function {
Maxifs,
Minifs,
Geomean,
Intercept,
Slope,

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

impl Function {
pub fn into_iter() -> IntoIter<Function, 198> {
pub fn into_iter() -> IntoIter<Function, 200> {
[
Function::And,
Function::False,
Expand Down Expand Up @@ -357,6 +359,8 @@ impl Function {
Function::Maxifs,
Function::Minifs,
Function::Geomean,
Function::Intercept,
Function::Slope,
Function::Year,
Function::Day,
Function::Month,
Expand Down Expand Up @@ -625,6 +629,8 @@ impl Function {
"MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs),
"MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs),
"GEOMEAN" => Some(Function::Geomean),
"INTERCEPT" => Some(Function::Intercept),
"SLOPE" => Some(Function::Slope),
// Date and Time
"YEAR" => Some(Function::Year),
"DAY" => Some(Function::Day),
Expand Down Expand Up @@ -836,6 +842,8 @@ impl fmt::Display for Function {
Function::Maxifs => write!(f, "MAXIFS"),
Function::Minifs => write!(f, "MINIFS"),
Function::Geomean => write!(f, "GEOMEAN"),
Function::Intercept => write!(f, "INTERCEPT"),
Function::Slope => write!(f, "SLOPE"),
Function::Year => write!(f, "YEAR"),
Function::Day => write!(f, "DAY"),
Function::Month => write!(f, "MONTH"),
Expand Down Expand Up @@ -1076,6 +1084,8 @@ impl Model {
Function::Maxifs => self.fn_maxifs(args, cell),
Function::Minifs => self.fn_minifs(args, cell),
Function::Geomean => self.fn_geomean(args, cell),
Function::Intercept => self.fn_intercept(args, cell),
Function::Slope => self.fn_slope(args, cell),
// Date and Time
Function::Year => self.fn_year(args, cell),
Function::Day => self.fn_day(args, cell),
Expand Down
105 changes: 104 additions & 1 deletion base/src/functions/statistical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
model::Model,
};

use super::util::build_criteria;
use super::util::{build_criteria, collect_series};

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

// collect_series method moved to functions::util::collect_series

pub(crate) fn fn_slope(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let ys = match collect_series(self, &args[0], cell) {
Ok(v) => v,
Err(e) => return e,
};
let xs = match collect_series(self, &args[1], cell) {
Ok(v) => v,
Err(e) => return e,
};
if ys.len() != xs.len() {
return CalcResult::new_error(
Error::NA,
cell,
"Ranges have different lengths".to_string(),
);
}
let mut pairs = Vec::new();
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut n = 0.0;
for (y, x) in ys.iter().zip(xs.iter()) {
if let (Some(yy), Some(xx)) = (y, x) {
pairs.push((*yy, *xx));
sum_x += xx;
sum_y += yy;
n += 1.0;
}
}
if n == 0.0 {
return CalcResult::new_error(Error::DIV, cell, "Division by Zero".to_string());
}
let mean_x = sum_x / n;
let mean_y = sum_y / n;
let mut numerator = 0.0;
let mut denominator = 0.0;
for (yy, xx) in pairs {
let dx = xx - mean_x;
let dy = yy - mean_y;
numerator += dx * dy;
denominator += dx * dx;
}
if denominator == 0.0 {
return CalcResult::new_error(Error::DIV, cell, "Division by Zero".to_string());
}
CalcResult::Number(numerator / denominator)
}

pub(crate) fn fn_intercept(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let ys = match collect_series(self, &args[0], cell) {
Ok(v) => v,
Err(e) => return e,
};
let xs = match collect_series(self, &args[1], cell) {
Ok(v) => v,
Err(e) => return e,
};
if ys.len() != xs.len() {
return CalcResult::new_error(
Error::NA,
cell,
"Ranges have different lengths".to_string(),
);
}
let mut pairs = Vec::new();
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut n = 0.0;
for (y, x) in ys.iter().zip(xs.iter()) {
if let (Some(yy), Some(xx)) = (y, x) {
pairs.push((*yy, *xx));
sum_x += xx;
sum_y += yy;
n += 1.0;
}
}
if n == 0.0 {
return CalcResult::new_error(Error::DIV, cell, "Division by Zero".to_string());
}
let mean_x = sum_x / n;
let mean_y = sum_y / n;
let mut numerator = 0.0;
let mut denominator = 0.0;
for (yy, xx) in pairs {
let dx = xx - mean_x;
let dy = yy - mean_y;
numerator += dx * dy;
denominator += dx * dx;
}
if denominator == 0.0 {
return CalcResult::new_error(Error::DIV, cell, "Division by Zero".to_string());
}
let slope = numerator / denominator;
CalcResult::Number(mean_y - slope * mean_x)
}
}
110 changes: 110 additions & 0 deletions base/src/functions/util.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#[cfg(feature = "use_regex_lite")]
use regex_lite as regex;

use crate::expressions::parser::{ArrayNode, Node};
use crate::expressions::token::Error;
use crate::expressions::types::CellReferenceIndex;
use crate::model::Model;
use crate::{calc_result::CalcResult, expressions::token::is_english_error_string};

/// This test for exact match (modulo case).
Expand Down Expand Up @@ -398,3 +402,109 @@ pub(crate) fn build_criteria<'a>(value: &'a CalcResult) -> Box<dyn Fn(&CalcResul
CalcResult::EmptyCell | CalcResult::EmptyArg => Box::new(result_is_equal_to_empty),
}
}

/// Collect a numeric series preserving positional information.
///
/// Given a single argument (range, reference, literal, or array), returns a
/// vector with the same length as the flattened input. Each position contains
/// `Some(f64)` when the corresponding element is numeric and `None` when it is
/// non-numeric or empty. Errors are propagated immediately.
///
/// Behaviour mirrors Excel's rules used by paired-data statistical functions
/// (SLOPE, INTERCEPT, CORREL, etc.):
/// - Booleans/string literals are coerced to numbers, literals coming from
/// references are ignored.
/// - Non-numeric cells become `None`, keeping the alignment between two series.
/// - Ranges crossing sheets cause a `#VALUE!` error.
pub(crate) fn collect_series(
model: &mut Model,
node: &Node,
cell: CellReferenceIndex,
) -> Result<Vec<Option<f64>>, CalcResult> {
let is_reference = matches!(
node,
Node::ReferenceKind { .. } | Node::RangeKind { .. } | Node::OpRangeKind { .. }
);

match model.evaluate_node_in_context(node, cell) {
CalcResult::Number(v) => Ok(vec![Some(v)]),
CalcResult::Boolean(b) => {
if is_reference {
Ok(vec![None])
} else {
Ok(vec![Some(if b { 1.0 } else { 0.0 })])
}
}
CalcResult::String(s) => {
if is_reference {
Ok(vec![None])
} else if let Ok(v) = s.parse::<f64>() {
Ok(vec![Some(v)])
} else {
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 mut values = Vec::new();
for row in left.row..=right.row {
for column in left.column..=right.column {
let cell_result = model.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
});
match cell_result {
CalcResult::Number(n) => values.push(Some(n)),
error @ CalcResult::Error { .. } => {
return Err(error);
}
_ => values.push(None),
}
}
}
Ok(values)
}
CalcResult::Array(arr) => {
let mut values = Vec::new();
for row in arr {
for val in row {
match val {
ArrayNode::Number(n) => values.push(Some(n)),
ArrayNode::Boolean(b) => values.push(Some(if b { 1.0 } else { 0.0 })),
ArrayNode::String(s) => match s.parse::<f64>() {
Ok(v) => values.push(Some(v)),
Err(_) => {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Argument cannot be cast into number".to_string(),
))
}
},
ArrayNode::Error(e) => {
return Err(CalcResult::Error {
error: e,
origin: cell,
message: "Error in array".to_string(),
})
}
}
}
}
Ok(values)
}
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(vec![None]),
error @ CalcResult::Error { .. } => Err(error),
}
}
1 change: 1 addition & 0 deletions base/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ mod test_arrays;
mod test_escape_quotes;
mod test_extend;
mod test_fn_fv;
mod test_fn_slope_intercept;
mod test_fn_type;
mod test_frozen_rows_and_columns;
mod test_geomean;
Expand Down
Loading