From c4142d4bf8f562109cd32d4032fc804398c190d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher?= Date: Thu, 27 Nov 2025 20:14:55 +0100 Subject: [PATCH] UPDATE: Adds 12 more statistical functions: * GAUSS * HARMEAN * KURT * MAXA * MEDIAN * MINA * RANK.EQ * RANK.AVG * SKEW * SKEW.P * SMALL * LARGE --- base/src/expressions/parser/mod.rs | 28 + .../src/expressions/parser/static_analysis.rs | 24 + base/src/functions/mod.rs | 88 ++- .../statistical/count_and_average.rs | 701 +++++++++++++++++- base/src/functions/statistical/gauss.rs | 39 + base/src/functions/statistical/mod.rs | 2 + base/src/functions/statistical/rank_eq_avg.rs | 202 +++++ base/src/test/statistical/mod.rs | 1 + base/src/test/statistical/test_fn_gauss.rs | 35 + xlsx/tests/calc_tests/MINA_MAXA.xlsx | Bin 0 -> 9233 bytes xlsx/tests/calc_tests/RANK_EQ_RANK_AVG.xlsx | Bin 0 -> 9281 bytes xlsx/tests/calc_tests/SMALL_LARGE.xlsx | Bin 0 -> 9417 bytes .../MEADIAN_KURT_SKEW_HARMEAN.xlsx | Bin 0 -> 9251 bytes 13 files changed, 1061 insertions(+), 59 deletions(-) create mode 100644 base/src/functions/statistical/gauss.rs create mode 100644 base/src/functions/statistical/rank_eq_avg.rs create mode 100644 base/src/test/statistical/test_fn_gauss.rs create mode 100644 xlsx/tests/calc_tests/MINA_MAXA.xlsx create mode 100644 xlsx/tests/calc_tests/RANK_EQ_RANK_AVG.xlsx create mode 100644 xlsx/tests/calc_tests/SMALL_LARGE.xlsx create mode 100644 xlsx/tests/statistical/MEADIAN_KURT_SKEW_HARMEAN.xlsx diff --git a/base/src/expressions/parser/mod.rs b/base/src/expressions/parser/mod.rs index af9e33a..2afa993 100644 --- a/base/src/expressions/parser/mod.rs +++ b/base/src/expressions/parser/mod.rs @@ -471,6 +471,20 @@ impl Parser { Node::NumberKind(s) => ArrayNode::Number(s), Node::StringKind(s) => ArrayNode::String(s), Node::ErrorKind(kind) => ArrayNode::Error(kind), + Node::UnaryKind { + kind: OpUnary::Minus, + right, + } => { + if let Node::NumberKind(n) = *right { + ArrayNode::Number(-n) + } else { + return Err(Node::ParseErrorKind { + formula: self.lexer.get_formula(), + message: "Invalid value in array".to_string(), + position: self.lexer.get_position() as usize, + }); + } + } error @ Node::ParseErrorKind { .. } => return Err(error), _ => { return Err(Node::ParseErrorKind { @@ -490,6 +504,20 @@ impl Parser { Node::NumberKind(s) => ArrayNode::Number(s), Node::StringKind(s) => ArrayNode::String(s), Node::ErrorKind(kind) => ArrayNode::Error(kind), + Node::UnaryKind { + kind: OpUnary::Minus, + right, + } => { + if let Node::NumberKind(n) = *right { + ArrayNode::Number(-n) + } else { + return Err(Node::ParseErrorKind { + formula: self.lexer.get_formula(), + message: "Invalid value in array".to_string(), + position: self.lexer.get_position() as usize, + }); + } + } error @ Node::ParseErrorKind { .. } => return Err(error), _ => { return Err(Node::ParseErrorKind { diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index e3e5ef1..04edf56 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -995,6 +995,18 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec vec![Signature::Vector; 2], Function::Slope => vec![Signature::Vector; 2], Function::Steyx => vec![Signature::Vector; 2], + Function::Gauss => args_signature_scalars(arg_count, 1, 0), + Function::Harmean => vec![Signature::Vector; arg_count], + Function::Kurt => vec![Signature::Vector; arg_count], + Function::Large => vec![Signature::Vector, Signature::Scalar], + Function::MaxA => vec![Signature::Vector; arg_count], + Function::Median => vec![Signature::Vector; arg_count], + Function::MinA => vec![Signature::Vector; arg_count], + Function::RankAvg => vec![Signature::Scalar, Signature::Vector, Signature::Scalar], + Function::RankEq => vec![Signature::Scalar, Signature::Vector, Signature::Scalar], + Function::Skew => vec![Signature::Vector; arg_count], + Function::SkewP => vec![Signature::Vector; arg_count], + Function::Small => vec![Signature::Vector, Signature::Scalar], } } @@ -1334,5 +1346,17 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Intercept => StaticResult::Scalar, Function::Slope => StaticResult::Scalar, Function::Steyx => StaticResult::Scalar, + Function::Gauss => StaticResult::Scalar, + Function::Harmean => StaticResult::Scalar, + Function::Kurt => StaticResult::Scalar, + Function::Large => StaticResult::Scalar, + Function::MaxA => StaticResult::Scalar, + Function::Median => StaticResult::Scalar, + Function::MinA => StaticResult::Scalar, + Function::RankAvg => StaticResult::Scalar, + Function::RankEq => StaticResult::Scalar, + Function::Skew => StaticResult::Scalar, + Function::SkewP => StaticResult::Scalar, + Function::Small => StaticResult::Scalar, } } diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index a66f7d9..f762722 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -207,7 +207,6 @@ pub enum Function { ChisqTest, ConfidenceNorm, ConfidenceT, - // Correl, CovarianceP, CovarianceS, Devsq, @@ -225,20 +224,18 @@ pub enum Function { GammaInv, GammaLn, GammaLnPrecise, - // Gauss, - // Growth, - // Harmean, + Gauss, + Harmean, HypGeomDist, - // Intercept, - // Kurt, - // Large, + Kurt, + Large, // Linest, // Logest, LogNormDist, LogNormInv, - // MaxA, - // Median, - // MinA, + MaxA, + Median, + MinA, // ModeMult, // ModeSingl, NegbinomDist, @@ -258,19 +255,16 @@ pub enum Function { // Prob, // QuartileExc, // QuartileInc, - // RankAvg, - // RankEq, - // Rsq - // Skew, - // SkewP, - // Slope, - // Small, + RankAvg, + RankEq, + Skew, + SkewP, + Small, Standardize, StDevP, StDevS, Stdeva, Stdevpa, - // Steyx, TDist, TDist2T, TDistRT, @@ -430,7 +424,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -765,6 +759,18 @@ impl Function { Function::Intercept, Function::Slope, Function::Steyx, + Function::Large, + Function::Median, + Function::Small, + Function::RankAvg, + Function::RankEq, + Function::Skew, + Function::SkewP, + Function::Harmean, + Function::Gauss, + Function::Kurt, + Function::MaxA, + Function::MinA, ] .into_iter() } @@ -887,6 +893,9 @@ impl Function { Function::WeibullDist => "_xlfn.WEIBULL.DIST".to_string(), Function::ZTest => "_xlfn.Z.TEST".to_string(), + Function::SkewP => "_xlfn.SKEW.P".to_string(), + Function::RankAvg => "_xlfn.RANK.AVG".to_string(), + Function::RankEq => "_xlfn.RANK.EQ".to_string(), _ => self.to_string(), } @@ -1251,6 +1260,20 @@ impl Function { "SLOPE" => Some(Function::Slope), "STEYX" => Some(Function::Steyx), + "SKEW.P" | "_XLFN.SKEW.P" => Some(Function::SkewP), + "SKEW" => Some(Function::Skew), + "KURT" => Some(Function::Kurt), + "HARMEAN" => Some(Function::Harmean), + "MEDIAN" => Some(Function::Median), + "GAUSS" => Some(Function::Gauss), + + "MINA" => Some(Function::MinA), + "MAXA" => Some(Function::MaxA), + "SMALL" => Some(Function::Small), + "LARGE" => Some(Function::Large), + "RANK.EQ" | "_XLFN.RANK.EQ" => Some(Function::RankEq), + "RANK.AVG" | "_XLFN.RANK.AVG" => Some(Function::RankAvg), + _ => None, } } @@ -1512,7 +1535,6 @@ impl fmt::Display for Function { Function::Combin => write!(f, "COMBIN"), Function::Combina => write!(f, "COMBINA"), Function::Sumsq => write!(f, "SUMSQ"), - Function::N => write!(f, "N"), Function::Cell => write!(f, "CELL"), Function::Info => write!(f, "INFO"), @@ -1529,7 +1551,6 @@ impl fmt::Display for Function { Function::Dvar => write!(f, "DVAR"), Function::Dvarp => write!(f, "DVARP"), Function::Dstdevp => write!(f, "DSTDEVP"), - Function::BetaDist => write!(f, "BETA.DIST"), Function::BetaInv => write!(f, "BETA.INV"), Function::BinomDist => write!(f, "BINOM.DIST"), @@ -1594,6 +1615,19 @@ impl fmt::Display for Function { Function::Intercept => write!(f, "INTERCEPT"), Function::Slope => write!(f, "SLOPE"), Function::Steyx => write!(f, "STEYX"), + // new ones + Function::Gauss => write!(f, "GAUSS"), + Function::Harmean => write!(f, "HARMEAN"), + Function::Kurt => write!(f, "KURT"), + Function::Large => write!(f, "LARGE"), + Function::MaxA => write!(f, "MAXA"), + Function::Median => write!(f, "MEDIAN"), + Function::MinA => write!(f, "MINA"), + Function::RankAvg => write!(f, "RANK.AVG"), + Function::RankEq => write!(f, "RANK.EQ"), + Function::Skew => write!(f, "SKEW"), + Function::SkewP => write!(f, "SKEW.P"), + Function::Small => write!(f, "SMALL"), } } } @@ -1955,6 +1989,18 @@ impl Model { Function::Intercept => self.fn_intercept(args, cell), Function::Slope => self.fn_slope(args, cell), Function::Steyx => self.fn_steyx(args, cell), + Function::Gauss => self.fn_gauss(args, cell), + Function::Harmean => self.fn_harmean(args, cell), + Function::Kurt => self.fn_kurt(args, cell), + Function::Large => self.fn_large(args, cell), + Function::MaxA => self.fn_maxa(args, cell), + Function::Median => self.fn_median(args, cell), + Function::MinA => self.fn_mina(args, cell), + Function::RankAvg => self.fn_rank_avg(args, cell), + Function::RankEq => self.fn_rank_eq(args, cell), + Function::Skew => self.fn_skew(args, cell), + Function::SkewP => self.fn_skew_p(args, cell), + Function::Small => self.fn_small(args, cell), } } } diff --git a/base/src/functions/statistical/count_and_average.rs b/base/src/functions/statistical/count_and_average.rs index eed5d51..23298f3 100644 --- a/base/src/functions/statistical/count_and_average.rs +++ b/base/src/functions/statistical/count_and_average.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::expressions::parser::ArrayNode; use crate::expressions::types::CellReferenceIndex; @@ -6,77 +8,219 @@ use crate::{ }; impl Model { - pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.is_empty() { - return CalcResult::new_args_number_error(cell); - } - let mut count = 0.0; - let mut sum = 0.0; + fn for_each_value( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + mut f: F, + ) -> Result<(), CalcResult> + where + F: FnMut(f64), + { for arg in args { match self.evaluate_node_in_context(arg, cell) { CalcResult::Number(value) => { - count += 1.0; - sum += value; + f(value); } - CalcResult::Boolean(b) => { - if let Node::ReferenceKind { .. } = arg { - } else { - sum += if b { 1.0 } else { 0.0 }; - count += 1.0; + CalcResult::Boolean(value) => { + if !matches!(arg, Node::ReferenceKind { .. }) { + f(if value { 1.0 } else { 0.0 }); + } + } + CalcResult::String(value) => { + if !matches!(arg, Node::ReferenceKind { .. }) { + if let Some(parsed) = self.cast_number(&value) { + f(parsed); + } else { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + "Argument cannot be cast into number".to_string(), + )); + } + } + } + CalcResult::Array(array) => { + for row in array { + for value in row { + match value { + ArrayNode::Number(value) => { + f(value); + } + ArrayNode::Boolean(b) => { + f(if b { 1.0 } else { 0.0 }); + } + ArrayNode::Error(error) => { + return Err(CalcResult::Error { + error, + origin: cell, + message: "Error in array".to_string(), + }); + } + _ => { + // ignore non-numeric + } + } + } } } CalcResult::Range { left, right } => { if left.sheet != right.sheet { - return CalcResult::new_error( + return Err(CalcResult::new_error( Error::VALUE, cell, "Ranges are in different sheets".to_string(), - ); + )); } - for row in left.row..(right.row + 1) { - for column in left.column..(right.column + 1) { + + for row in left.row..=right.row { + for column in left.column..=right.column { match self.evaluate_cell(CellReferenceIndex { sheet: left.sheet, row, column, }) { CalcResult::Number(value) => { - count += 1.0; - sum += value; + f(value); } - error @ CalcResult::Error { .. } => return error, + error @ CalcResult::Error { .. } => return Err(error), CalcResult::Range { .. } => { - return CalcResult::new_error( + return Err(CalcResult::new_error( Error::ERROR, cell, "Unexpected Range".to_string(), - ); + )); } _ => {} } } } } - error @ CalcResult::Error { .. } => return error, - CalcResult::String(s) => { - if let Node::ReferenceKind { .. } = arg { - // Do nothing - } else if let Ok(t) = s.parse::() { - sum += t; - count += 1.0; - } else { - return CalcResult::Error { - error: Error::VALUE, - origin: cell, - message: "Argument cannot be cast into number".to_string(), - }; + error @ CalcResult::Error { .. } => return Err(error), + // Everything else is ignored + _ => {} + } + } + + Ok(()) + } + + fn for_each_value_a( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + mut f: F, + ) -> Result<(), CalcResult> + where + F: FnMut(f64), + { + for arg in args { + match self.evaluate_node_in_context(arg, cell) { + CalcResult::Number(value) => { + f(value); + } + CalcResult::Boolean(value) => { + if !matches!(arg, Node::ReferenceKind { .. }) { + f(if value { 1.0 } else { 0.0 }); } } - _ => { - // Ignore everything else + CalcResult::String(value) => { + if !matches!(arg, Node::ReferenceKind { .. }) { + if let Some(parsed) = self.cast_number(&value) { + f(parsed); + } else { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + "Argument cannot be cast into number".to_string(), + )); + } + } } - }; + CalcResult::Array(array) => { + for row in array { + for value in row { + match value { + ArrayNode::Number(value) => { + f(value); + } + ArrayNode::Boolean(b) => { + f(if b { 1.0 } else { 0.0 }); + } + ArrayNode::String(_) => { + f(0.0); + } + ArrayNode::Error(error) => { + return Err(CalcResult::Error { + error, + origin: cell, + message: "Error in array".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(), + )); + } + + for row in left.row..=right.row { + for column in left.column..=right.column { + match self.evaluate_cell(CellReferenceIndex { + sheet: left.sheet, + row, + column, + }) { + CalcResult::Number(value) => { + f(value); + } + CalcResult::Boolean(b) => { + f(if b { 1.0 } else { 0.0 }); + } + CalcResult::String(_) => { + f(0.0); + } + 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), + // Everything else is ignored + _ => {} + } } + + Ok(()) + } + + pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + let mut count = 0.0; + let mut sum = 0.0; + if let Err(e) = self.for_each_value(args, cell, |f| { + count += 1.0; + sum += f; + }) { + return e; + } + if count == 0.0 { return CalcResult::Error { error: Error::DIV, @@ -86,6 +230,7 @@ impl Model { } CalcResult::Number(sum / count) } + pub(crate) fn fn_averagea(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { if args.is_empty() { return CalcResult::new_args_number_error(cell); @@ -443,4 +588,484 @@ impl Model { CalcResult::Number(sum_abs_dev / n) } + + pub(crate) fn fn_median(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + + let mut values: Vec = Vec::new(); + if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) { + return e; + } + + if values.is_empty() { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "No numeric values for MEDIAN".to_string(), + }; + } + + values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + + let n = values.len(); + let median = if n % 2 == 1 { + // odd + values[n / 2] + } else { + // even: average of the two middle values + let a = values[(n / 2) - 1]; + let b = values[n / 2]; + (a + b) / 2.0 + }; + + CalcResult::Number(median) + } + + pub(crate) fn fn_harmean(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + + let mut values: Vec = Vec::new(); + if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) { + return e; + } + + if values.is_empty() { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Division by Zero".to_string(), + }; + } + + // Excel HARMEAN: all values must be > 0 + if values.iter().any(|&v| v <= 0.0) { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "HARMEAN requires strictly positive values".to_string(), + }; + } + + let n = values.len() as f64; + let sum_recip: f64 = values.iter().map(|v| 1.0 / v).sum(); + + if sum_recip == 0.0 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Division by Zero".to_string(), + }; + } + + CalcResult::Number(n / sum_recip) + } + + pub(crate) fn fn_mina(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + let mut mina: Option = None; + if let Err(e) = self.for_each_value_a(args, cell, |f| { + if let Some(m) = mina { + mina = Some(m.min(f)); + } else { + mina = Some(f); + } + }) { + return e; + } + if let Some(mina) = mina { + CalcResult::Number(mina) + } else { + CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "No numeric values for MINA".to_string(), + } + } + } + + pub(crate) fn fn_maxa(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + let mut maxa: Option = None; + if let Err(e) = self.for_each_value_a(args, cell, |f| { + if let Some(m) = maxa { + maxa = Some(m.max(f)); + } else { + maxa = Some(f); + } + }) { + return e; + } + if let Some(maxa) = maxa { + CalcResult::Number(maxa) + } else { + CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "No numeric values for MAXA".to_string(), + } + } + } + + pub(crate) fn fn_skew(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + // Sample skewness (Excel SKEW) + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + + let mut values: Vec = Vec::new(); + if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) { + return e; + } + + let n = values.len(); + if n < 3 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "SKEW requires at least 3 data points".to_string(), + }; + } + + let n_f = n as f64; + let mean = values.iter().sum::() / n_f; + + let mut m2 = 0.0; + for &x in &values { + let d = x - mean; + m2 += d * d; + } + + if m2 == 0.0 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Zero variance in SKEW".to_string(), + }; + } + + let s = (m2 / (n_f - 1.0)).sqrt(); + + let mut sum_cubed = 0.0; + for &x in &values { + let z = (x - mean) / s; + sum_cubed += z * z * z; + } + + let skew = (n_f / ((n_f - 1.0) * (n_f - 2.0))) * sum_cubed; + CalcResult::Number(skew) + } + + pub(crate) fn fn_skew_p(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + // Population skewness (Excel SKEW.P) + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + + let mut values: Vec = Vec::new(); + if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) { + return e; + } + + let n = values.len(); + if n < 2 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "SKEW.P requires at least 2 data points".to_string(), + }; + } + + let n_f = n as f64; + let mean = values.iter().sum::() / n_f; + + let mut m2 = 0.0; + for &x in &values { + let d = x - mean; + m2 += d * d; + } + + if m2 == 0.0 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Zero variance in SKEW.P".to_string(), + }; + } + + let sigma = (m2 / n_f).sqrt(); + + let mut sum_cubed = 0.0; + for &x in &values { + let z = (x - mean) / sigma; + sum_cubed += z * z * z; + } + + let skew_p = sum_cubed / n_f; + CalcResult::Number(skew_p) + } + + pub(crate) fn fn_kurt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + + let mut values: Vec = Vec::new(); + if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) { + return e; + } + + let n = values.len(); + if n < 4 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "KURT requires at least 4 data points".to_string(), + }; + } + + let n_f = n as f64; + let mean = values.iter().sum::() / n_f; + + let mut m2 = 0.0; + for &x in &values { + let d = x - mean; + m2 += d * d; + } + + if m2 == 0.0 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Zero variance in KURT".to_string(), + }; + } + + let s = (m2 / (n_f - 1.0)).sqrt(); + + let mut sum_fourth = 0.0; + for &x in &values { + let z = (x - mean) / s; + sum_fourth += z * z * z * z; + } + + let term1 = (n_f * (n_f + 1.0)) / ((n_f - 1.0) * (n_f - 2.0) * (n_f - 3.0)) * sum_fourth; + let term2 = 3.0 * (n_f - 1.0) * (n_f - 1.0) / ((n_f - 2.0) * (n_f - 3.0)); + + let kurt = term1 - term2; + CalcResult::Number(kurt) + } + + pub(crate) fn fn_large(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let values = match self.evaluate_node_in_context(&args[0], cell) { + CalcResult::Array(array) => match self.values_from_array(array) { + Ok(v) => v, + Err(e) => { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: format!("Unsupported array argument: {}", e), + } + } + }, + CalcResult::Range { left, right } => match self.values_from_range(left, right) { + Ok(v) => v, + Err(e) => return e, + }, + CalcResult::Boolean(value) => { + if !matches!(args[0], Node::ReferenceKind { .. }) { + vec![Some(if value { 1.0 } else { 0.0 })] + } else { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Unsupported argument type".to_string(), + }; + } + } + CalcResult::Number(value) => { + if !matches!(args[0], Node::ReferenceKind { .. }) { + vec![Some(value)] + } else { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Unsupported argument type".to_string(), + }; + } + } + CalcResult::String(value) => { + if !matches!(args[0], Node::ReferenceKind { .. }) { + if let Some(parsed) = self.cast_number(&value) { + vec![Some(parsed)] + } else { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Unsupported argument type".to_string(), + }; + } + } else { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Unsupported argument type".to_string(), + }; + } + } + _ => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Unsupported argument type".to_string(), + } + } + }; + let k = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f.trunc(), + Err(s) => return s, + }; + if k < 1.0 { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "K must be >= 1".to_string(), + }; + } + let mut numeric_values: Vec = values.into_iter().flatten().collect(); + if numeric_values.is_empty() { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "No numeric values for LARGE".to_string(), + }; + } + numeric_values.sort_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal)); + let k_usize = k as usize; + if k_usize > numeric_values.len() { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "K is larger than the number of data points".to_string(), + }; + } + CalcResult::Number(numeric_values[k_usize - 1]) + } + + pub(crate) fn fn_small(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + + let values = match self.evaluate_node_in_context(&args[0], cell) { + CalcResult::Array(array) => match self.values_from_array(array) { + Ok(v) => v, + Err(e) => { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: format!("Unsupported array argument: {}", e), + } + } + }, + CalcResult::Range { left, right } => match self.values_from_range(left, right) { + Ok(v) => v, + Err(e) => return e, + }, + CalcResult::Boolean(value) => { + if !matches!(args[0], Node::ReferenceKind { .. }) { + vec![Some(if value { 1.0 } else { 0.0 })] + } else { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Unsupported argument type".to_string(), + }; + } + } + CalcResult::Number(value) => { + if !matches!(args[0], Node::ReferenceKind { .. }) { + vec![Some(value)] + } else { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Unsupported argument type".to_string(), + }; + } + } + CalcResult::String(value) => { + if !matches!(args[0], Node::ReferenceKind { .. }) { + if let Some(parsed) = self.cast_number(&value) { + vec![Some(parsed)] + } else { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Unsupported argument type".to_string(), + }; + } + } else { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Unsupported argument type".to_string(), + }; + } + } + _ => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Unsupported argument type".to_string(), + } + } + }; + + let k = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f.trunc(), + Err(s) => return s, + }; + + if k < 1.0 { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "K must be >= 1".to_string(), + }; + } + + let mut numeric_values: Vec = values.into_iter().flatten().collect(); + if numeric_values.is_empty() { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "No numeric values for SMALL".to_string(), + }; + } + + // For SMALL, sort ascending + numeric_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + + let k_usize = k as usize; + if k_usize > numeric_values.len() { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "K is larger than the number of data points".to_string(), + }; + } + + CalcResult::Number(numeric_values[k_usize - 1]) + } } diff --git a/base/src/functions/statistical/gauss.rs b/base/src/functions/statistical/gauss.rs new file mode 100644 index 0000000..0e4d89d --- /dev/null +++ b/base/src/functions/statistical/gauss.rs @@ -0,0 +1,39 @@ +use statrs::distribution::{ContinuousCDF, Normal}; + +use crate::expressions::token::Error; +use crate::expressions::types::CellReferenceIndex; +use crate::{calc_result::CalcResult, expressions::parser::Node, model::Model}; + +impl Model { + pub(crate) fn fn_gauss(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 1 { + return CalcResult::new_args_number_error(cell); + } + let z = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let dist = match Normal::new(0.0, 1.0) { + Ok(d) => d, + Err(_) => { + return CalcResult::Error { + error: Error::ERROR, + origin: cell, + message: "Failed to construct standard normal distribution".to_string(), + } + } + }; + + let result = dist.cdf(z) - 0.5; + + if !result.is_finite() { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Invalid result for GAUSS".to_string(), + }; + } + + CalcResult::Number(result) + } +} diff --git a/base/src/functions/statistical/mod.rs b/base/src/functions/statistical/mod.rs index 6e31366..7d9eb5c 100644 --- a/base/src/functions/statistical/mod.rs +++ b/base/src/functions/statistical/mod.rs @@ -8,6 +8,7 @@ mod devsq; mod exponential; mod fisher; mod gamma; +mod gauss; mod geomean; mod hypegeom; mod if_ifs; @@ -16,6 +17,7 @@ mod normal; mod pearson; mod phi; mod poisson; +mod rank_eq_avg; mod standard_dev; mod standardize; mod t_dist; diff --git a/base/src/functions/statistical/rank_eq_avg.rs b/base/src/functions/statistical/rank_eq_avg.rs new file mode 100644 index 0000000..c6ea771 --- /dev/null +++ b/base/src/functions/statistical/rank_eq_avg.rs @@ -0,0 +1,202 @@ +use crate::expressions::types::CellReferenceIndex; +use crate::{ + calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model, +}; + +impl Model { + // Helper to collect numeric values from the 2nd argument of RANK.* + fn collect_rank_values( + &mut self, + arg: &Node, + cell: CellReferenceIndex, + ) -> Result, CalcResult> { + let values = match self.evaluate_node_in_context(arg, cell) { + CalcResult::Array(array) => match self.values_from_array(array) { + Ok(v) => v, + Err(e) => { + return Err(CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: format!("Unsupported array argument: {}", e), + }) + } + }, + CalcResult::Range { left, right } => self.values_from_range(left, right)?, + CalcResult::Boolean(value) => { + if !matches!(arg, Node::ReferenceKind { .. }) { + vec![Some(if value { 1.0 } else { 0.0 })] + } else { + return Err(CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Unsupported argument type".to_string(), + }); + } + } + _ => { + return Err(CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Unsupported argument type".to_string(), + }) + } + }; + + let numeric_values: Vec = values.into_iter().flatten().collect(); + Ok(numeric_values) + } + + // RANK.EQ(number, ref, [order]) + pub(crate) fn fn_rank_eq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if !(2..=3).contains(&args.len()) { + return CalcResult::new_args_number_error(cell); + } + + // number + let number = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(e) => return e, + }; + + // ref + let mut values = match self.collect_rank_values(&args[1], cell) { + Ok(v) => v, + Err(e) => return e, + }; + + if values.is_empty() { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "No numeric values for RANK.EQ".to_string(), + }; + } + + // order: default 0 (descending) + let order = if args.len() == 2 { + 0.0 + } else { + match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(e) => return e, + } + }; + + values.retain(|v| !v.is_nan()); + + // "better" = greater (descending) or smaller (ascending) + let mut better = 0; + let mut equal = 0; + + if order == 0.0 { + // descending + for v in &values { + if *v > number { + better += 1; + } else if *v == number { + equal += 1; + } + } + } else { + // ascending + for v in &values { + if *v < number { + better += 1; + } else if *v == number { + equal += 1; + } + } + } + + if equal == 0 { + return CalcResult::Error { + error: Error::NA, + origin: cell, + message: "Number not found in reference for RANK.EQ".to_string(), + }; + } + + let rank = (better as f64) + 1.0; + CalcResult::Number(rank) + } + + // RANK.AVG(number, ref, [order]) + pub(crate) fn fn_rank_avg(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if !(2..=3).contains(&args.len()) { + return CalcResult::new_args_number_error(cell); + } + + // number + let number = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(e) => return e, + }; + + // ref + let mut values = match self.collect_rank_values(&args[1], cell) { + Ok(v) => v, + Err(e) => return e, + }; + + if values.is_empty() { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "No numeric values for RANK.AVG".to_string(), + }; + } + + // order: default 0 (descending) + let order = if args.len() == 2 { + 0.0 + } else { + match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(e) => return e, + } + }; + + values.retain(|v| !v.is_nan()); + + // > or < depending on order + let mut better = 0; + let mut equal = 0; + + if order == 0.0 { + // descending + for v in &values { + if *v > number { + better += 1; + } else if *v == number { + equal += 1; + } + } + } else { + // ascending + for v in &values { + if *v < number { + better += 1; + } else if *v == number { + equal += 1; + } + } + } + + if equal == 0 { + return CalcResult::Error { + error: Error::NA, + origin: cell, + message: "Number not found in reference for RANK.AVG".to_string(), + }; + } + + // For ties, average of the ranks. If the equal values occupy positions + // (better+1) ..= (better+equal), the average is: + // better + (equal + 1) / 2 + let better_f = better as f64; + let equal_f = equal as f64; + let rank = better_f + (equal_f + 1.0) / 2.0; + + CalcResult::Number(rank) + } +} diff --git a/base/src/test/statistical/mod.rs b/base/src/test/statistical/mod.rs index 8f7c66a..6d7dd77 100644 --- a/base/src/test/statistical/mod.rs +++ b/base/src/test/statistical/mod.rs @@ -9,6 +9,7 @@ mod test_fn_expon_dist; mod test_fn_f; mod test_fn_f_test; mod test_fn_fisher; +mod test_fn_gauss; mod test_fn_hyp_geom_dist; mod test_fn_log_norm; mod test_fn_norm_dist; diff --git a/base/src/test/statistical/test_fn_gauss.rs b/base/src/test/statistical/test_fn_gauss.rs new file mode 100644 index 0000000..44b3647 --- /dev/null +++ b/base/src/test/statistical/test_fn_gauss.rs @@ -0,0 +1,35 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_gauss_smoke() { + let mut model = new_empty_model(); + model._set("A1", "=GAUSS(-3)"); + model._set("A2", "=GAUSS(-2.3)"); + model._set("A3", "=GAUSS(-1.7)"); + model._set("A4", "=GAUSS(0)"); + model._set("A5", "=GAUSS(0.5)"); + model._set("A6", "=GAUSS(1)"); + model._set("A7", "=GAUSS(1.3)"); + model._set("A8", "=GAUSS(3)"); + model._set("A9", "=GAUSS(4)"); + + model._set("G6", "=GAUSS()"); + model._set("G7", "=GAUSS(1, 1)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"-0.498650102"); + assert_eq!(model._get_text("A2"), *"-0.48927589"); + assert_eq!(model._get_text("A3"), *"-0.455434537"); + assert_eq!(model._get_text("A4"), *"0"); + assert_eq!(model._get_text("A5"), *"0.191462461"); + assert_eq!(model._get_text("A6"), *"0.341344746"); + assert_eq!(model._get_text("A7"), *"0.403199515"); + assert_eq!(model._get_text("A8"), *"0.498650102"); + assert_eq!(model._get_text("A9"), *"0.499968329"); + + assert_eq!(model._get_text("G6"), *"#ERROR!"); + assert_eq!(model._get_text("G7"), *"#ERROR!"); +} diff --git a/xlsx/tests/calc_tests/MINA_MAXA.xlsx b/xlsx/tests/calc_tests/MINA_MAXA.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..2d2b11da745b925e8c3cf9faf1605f703e3106e3 GIT binary patch literal 9233 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 literal 0 HcmV?d00001 diff --git a/xlsx/tests/calc_tests/SMALL_LARGE.xlsx b/xlsx/tests/calc_tests/SMALL_LARGE.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..283cb34dc54e3f07541f82e545795360cc326e00 GIT binary patch literal 9417 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%0DNG0L6{ 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 HKmYw7kt!1Z literal 0 HcmV?d00001