UPDATE: Adds 12 more statistical functions:
* GAUSS * HARMEAN * KURT * MAXA * MEDIAN * MINA * RANK.EQ * RANK.AVG * SKEW * SKEW.P * SMALL * LARGE
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
885d344b5b
commit
c4142d4bf8
@@ -471,6 +471,20 @@ impl Parser {
|
|||||||
Node::NumberKind(s) => ArrayNode::Number(s),
|
Node::NumberKind(s) => ArrayNode::Number(s),
|
||||||
Node::StringKind(s) => ArrayNode::String(s),
|
Node::StringKind(s) => ArrayNode::String(s),
|
||||||
Node::ErrorKind(kind) => ArrayNode::Error(kind),
|
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),
|
error @ Node::ParseErrorKind { .. } => return Err(error),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Node::ParseErrorKind {
|
return Err(Node::ParseErrorKind {
|
||||||
@@ -490,6 +504,20 @@ impl Parser {
|
|||||||
Node::NumberKind(s) => ArrayNode::Number(s),
|
Node::NumberKind(s) => ArrayNode::Number(s),
|
||||||
Node::StringKind(s) => ArrayNode::String(s),
|
Node::StringKind(s) => ArrayNode::String(s),
|
||||||
Node::ErrorKind(kind) => ArrayNode::Error(kind),
|
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),
|
error @ Node::ParseErrorKind { .. } => return Err(error),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Node::ParseErrorKind {
|
return Err(Node::ParseErrorKind {
|
||||||
|
|||||||
@@ -995,6 +995,18 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
Function::Intercept => vec![Signature::Vector; 2],
|
Function::Intercept => vec![Signature::Vector; 2],
|
||||||
Function::Slope => vec![Signature::Vector; 2],
|
Function::Slope => vec![Signature::Vector; 2],
|
||||||
Function::Steyx => 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::Intercept => StaticResult::Scalar,
|
||||||
Function::Slope => StaticResult::Scalar,
|
Function::Slope => StaticResult::Scalar,
|
||||||
Function::Steyx => 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,7 +207,6 @@ pub enum Function {
|
|||||||
ChisqTest,
|
ChisqTest,
|
||||||
ConfidenceNorm,
|
ConfidenceNorm,
|
||||||
ConfidenceT,
|
ConfidenceT,
|
||||||
// Correl,
|
|
||||||
CovarianceP,
|
CovarianceP,
|
||||||
CovarianceS,
|
CovarianceS,
|
||||||
Devsq,
|
Devsq,
|
||||||
@@ -225,20 +224,18 @@ pub enum Function {
|
|||||||
GammaInv,
|
GammaInv,
|
||||||
GammaLn,
|
GammaLn,
|
||||||
GammaLnPrecise,
|
GammaLnPrecise,
|
||||||
// Gauss,
|
Gauss,
|
||||||
// Growth,
|
Harmean,
|
||||||
// Harmean,
|
|
||||||
HypGeomDist,
|
HypGeomDist,
|
||||||
// Intercept,
|
Kurt,
|
||||||
// Kurt,
|
Large,
|
||||||
// Large,
|
|
||||||
// Linest,
|
// Linest,
|
||||||
// Logest,
|
// Logest,
|
||||||
LogNormDist,
|
LogNormDist,
|
||||||
LogNormInv,
|
LogNormInv,
|
||||||
// MaxA,
|
MaxA,
|
||||||
// Median,
|
Median,
|
||||||
// MinA,
|
MinA,
|
||||||
// ModeMult,
|
// ModeMult,
|
||||||
// ModeSingl,
|
// ModeSingl,
|
||||||
NegbinomDist,
|
NegbinomDist,
|
||||||
@@ -258,19 +255,16 @@ pub enum Function {
|
|||||||
// Prob,
|
// Prob,
|
||||||
// QuartileExc,
|
// QuartileExc,
|
||||||
// QuartileInc,
|
// QuartileInc,
|
||||||
// RankAvg,
|
RankAvg,
|
||||||
// RankEq,
|
RankEq,
|
||||||
// Rsq
|
Skew,
|
||||||
// Skew,
|
SkewP,
|
||||||
// SkewP,
|
Small,
|
||||||
// Slope,
|
|
||||||
// Small,
|
|
||||||
Standardize,
|
Standardize,
|
||||||
StDevP,
|
StDevP,
|
||||||
StDevS,
|
StDevS,
|
||||||
Stdeva,
|
Stdeva,
|
||||||
Stdevpa,
|
Stdevpa,
|
||||||
// Steyx,
|
|
||||||
TDist,
|
TDist,
|
||||||
TDist2T,
|
TDist2T,
|
||||||
TDistRT,
|
TDistRT,
|
||||||
@@ -430,7 +424,7 @@ pub enum Function {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Function {
|
impl Function {
|
||||||
pub fn into_iter() -> IntoIter<Function, 333> {
|
pub fn into_iter() -> IntoIter<Function, 345> {
|
||||||
[
|
[
|
||||||
Function::And,
|
Function::And,
|
||||||
Function::False,
|
Function::False,
|
||||||
@@ -765,6 +759,18 @@ impl Function {
|
|||||||
Function::Intercept,
|
Function::Intercept,
|
||||||
Function::Slope,
|
Function::Slope,
|
||||||
Function::Steyx,
|
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()
|
.into_iter()
|
||||||
}
|
}
|
||||||
@@ -887,6 +893,9 @@ impl Function {
|
|||||||
|
|
||||||
Function::WeibullDist => "_xlfn.WEIBULL.DIST".to_string(),
|
Function::WeibullDist => "_xlfn.WEIBULL.DIST".to_string(),
|
||||||
Function::ZTest => "_xlfn.Z.TEST".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(),
|
_ => self.to_string(),
|
||||||
}
|
}
|
||||||
@@ -1251,6 +1260,20 @@ impl Function {
|
|||||||
"SLOPE" => Some(Function::Slope),
|
"SLOPE" => Some(Function::Slope),
|
||||||
"STEYX" => Some(Function::Steyx),
|
"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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1512,7 +1535,6 @@ impl fmt::Display for Function {
|
|||||||
Function::Combin => write!(f, "COMBIN"),
|
Function::Combin => write!(f, "COMBIN"),
|
||||||
Function::Combina => write!(f, "COMBINA"),
|
Function::Combina => write!(f, "COMBINA"),
|
||||||
Function::Sumsq => write!(f, "SUMSQ"),
|
Function::Sumsq => write!(f, "SUMSQ"),
|
||||||
|
|
||||||
Function::N => write!(f, "N"),
|
Function::N => write!(f, "N"),
|
||||||
Function::Cell => write!(f, "CELL"),
|
Function::Cell => write!(f, "CELL"),
|
||||||
Function::Info => write!(f, "INFO"),
|
Function::Info => write!(f, "INFO"),
|
||||||
@@ -1529,7 +1551,6 @@ impl fmt::Display for Function {
|
|||||||
Function::Dvar => write!(f, "DVAR"),
|
Function::Dvar => write!(f, "DVAR"),
|
||||||
Function::Dvarp => write!(f, "DVARP"),
|
Function::Dvarp => write!(f, "DVARP"),
|
||||||
Function::Dstdevp => write!(f, "DSTDEVP"),
|
Function::Dstdevp => write!(f, "DSTDEVP"),
|
||||||
|
|
||||||
Function::BetaDist => write!(f, "BETA.DIST"),
|
Function::BetaDist => write!(f, "BETA.DIST"),
|
||||||
Function::BetaInv => write!(f, "BETA.INV"),
|
Function::BetaInv => write!(f, "BETA.INV"),
|
||||||
Function::BinomDist => write!(f, "BINOM.DIST"),
|
Function::BinomDist => write!(f, "BINOM.DIST"),
|
||||||
@@ -1594,6 +1615,19 @@ impl fmt::Display for Function {
|
|||||||
Function::Intercept => write!(f, "INTERCEPT"),
|
Function::Intercept => write!(f, "INTERCEPT"),
|
||||||
Function::Slope => write!(f, "SLOPE"),
|
Function::Slope => write!(f, "SLOPE"),
|
||||||
Function::Steyx => write!(f, "STEYX"),
|
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::Intercept => self.fn_intercept(args, cell),
|
||||||
Function::Slope => self.fn_slope(args, cell),
|
Function::Slope => self.fn_slope(args, cell),
|
||||||
Function::Steyx => self.fn_steyx(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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||||
use crate::expressions::parser::ArrayNode;
|
use crate::expressions::parser::ArrayNode;
|
||||||
use crate::expressions::types::CellReferenceIndex;
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
@@ -6,77 +8,219 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
fn for_each_value<F>(
|
||||||
if args.is_empty() {
|
&mut self,
|
||||||
return CalcResult::new_args_number_error(cell);
|
args: &[Node],
|
||||||
}
|
cell: CellReferenceIndex,
|
||||||
let mut count = 0.0;
|
mut f: F,
|
||||||
let mut sum = 0.0;
|
) -> Result<(), CalcResult>
|
||||||
|
where
|
||||||
|
F: FnMut(f64),
|
||||||
|
{
|
||||||
for arg in args {
|
for arg in args {
|
||||||
match self.evaluate_node_in_context(arg, cell) {
|
match self.evaluate_node_in_context(arg, cell) {
|
||||||
CalcResult::Number(value) => {
|
CalcResult::Number(value) => {
|
||||||
count += 1.0;
|
f(value);
|
||||||
sum += value;
|
|
||||||
}
|
}
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(value) => {
|
||||||
if let Node::ReferenceKind { .. } = arg {
|
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 {
|
} else {
|
||||||
sum += if b { 1.0 } else { 0.0 };
|
return Err(CalcResult::new_error(
|
||||||
count += 1.0;
|
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 } => {
|
CalcResult::Range { left, right } => {
|
||||||
if left.sheet != right.sheet {
|
if left.sheet != right.sheet {
|
||||||
return CalcResult::new_error(
|
return Err(CalcResult::new_error(
|
||||||
Error::VALUE,
|
Error::VALUE,
|
||||||
cell,
|
cell,
|
||||||
"Ranges are in different sheets".to_string(),
|
"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 {
|
match self.evaluate_cell(CellReferenceIndex {
|
||||||
sheet: left.sheet,
|
sheet: left.sheet,
|
||||||
row,
|
row,
|
||||||
column,
|
column,
|
||||||
}) {
|
}) {
|
||||||
CalcResult::Number(value) => {
|
CalcResult::Number(value) => {
|
||||||
count += 1.0;
|
f(value);
|
||||||
sum += value;
|
|
||||||
}
|
}
|
||||||
error @ CalcResult::Error { .. } => return error,
|
error @ CalcResult::Error { .. } => return Err(error),
|
||||||
CalcResult::Range { .. } => {
|
CalcResult::Range { .. } => {
|
||||||
return CalcResult::new_error(
|
return Err(CalcResult::new_error(
|
||||||
Error::ERROR,
|
Error::ERROR,
|
||||||
cell,
|
cell,
|
||||||
"Unexpected Range".to_string(),
|
"Unexpected Range".to_string(),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
error @ CalcResult::Error { .. } => return error,
|
error @ CalcResult::Error { .. } => return Err(error),
|
||||||
CalcResult::String(s) => {
|
// Everything else is ignored
|
||||||
if let Node::ReferenceKind { .. } = arg {
|
_ => {}
|
||||||
// Do nothing
|
}
|
||||||
} else if let Ok(t) = s.parse::<f64>() {
|
}
|
||||||
sum += t;
|
|
||||||
count += 1.0;
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn for_each_value_a<F>(
|
||||||
|
&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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::String(value) => {
|
||||||
|
if !matches!(arg, Node::ReferenceKind { .. }) {
|
||||||
|
if let Some(parsed) = self.cast_number(&value) {
|
||||||
|
f(parsed);
|
||||||
} else {
|
} else {
|
||||||
return CalcResult::Error {
|
return Err(CalcResult::new_error(
|
||||||
error: Error::VALUE,
|
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,
|
origin: cell,
|
||||||
message: "Argument cannot be cast into number".to_string(),
|
message: "Error in array".to_string(),
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
|
||||||
// Ignore everything else
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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 {
|
if count == 0.0 {
|
||||||
return CalcResult::Error {
|
return CalcResult::Error {
|
||||||
error: Error::DIV,
|
error: Error::DIV,
|
||||||
@@ -86,6 +230,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
CalcResult::Number(sum / count)
|
CalcResult::Number(sum / count)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fn_averagea(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_averagea(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.is_empty() {
|
if args.is_empty() {
|
||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
@@ -443,4 +588,484 @@ impl Model {
|
|||||||
|
|
||||||
CalcResult::Number(sum_abs_dev / n)
|
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<f64> = 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<f64> = 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<f64> = 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<f64> = 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<f64> = 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::<f64>() / 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<f64> = 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::<f64>() / 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<f64> = 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::<f64>() / 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<f64> = 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<f64> = 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])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
base/src/functions/statistical/gauss.rs
Normal file
39
base/src/functions/statistical/gauss.rs
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ mod devsq;
|
|||||||
mod exponential;
|
mod exponential;
|
||||||
mod fisher;
|
mod fisher;
|
||||||
mod gamma;
|
mod gamma;
|
||||||
|
mod gauss;
|
||||||
mod geomean;
|
mod geomean;
|
||||||
mod hypegeom;
|
mod hypegeom;
|
||||||
mod if_ifs;
|
mod if_ifs;
|
||||||
@@ -16,6 +17,7 @@ mod normal;
|
|||||||
mod pearson;
|
mod pearson;
|
||||||
mod phi;
|
mod phi;
|
||||||
mod poisson;
|
mod poisson;
|
||||||
|
mod rank_eq_avg;
|
||||||
mod standard_dev;
|
mod standard_dev;
|
||||||
mod standardize;
|
mod standardize;
|
||||||
mod t_dist;
|
mod t_dist;
|
||||||
|
|||||||
202
base/src/functions/statistical/rank_eq_avg.rs
Normal file
202
base/src/functions/statistical/rank_eq_avg.rs
Normal file
@@ -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<Vec<f64>, 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<f64> = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ mod test_fn_expon_dist;
|
|||||||
mod test_fn_f;
|
mod test_fn_f;
|
||||||
mod test_fn_f_test;
|
mod test_fn_f_test;
|
||||||
mod test_fn_fisher;
|
mod test_fn_fisher;
|
||||||
|
mod test_fn_gauss;
|
||||||
mod test_fn_hyp_geom_dist;
|
mod test_fn_hyp_geom_dist;
|
||||||
mod test_fn_log_norm;
|
mod test_fn_log_norm;
|
||||||
mod test_fn_norm_dist;
|
mod test_fn_norm_dist;
|
||||||
|
|||||||
35
base/src/test/statistical/test_fn_gauss.rs
Normal file
35
base/src/test/statistical/test_fn_gauss.rs
Normal file
@@ -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!");
|
||||||
|
}
|
||||||
BIN
xlsx/tests/calc_tests/MINA_MAXA.xlsx
Normal file
BIN
xlsx/tests/calc_tests/MINA_MAXA.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/RANK_EQ_RANK_AVG.xlsx
Normal file
BIN
xlsx/tests/calc_tests/RANK_EQ_RANK_AVG.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/SMALL_LARGE.xlsx
Normal file
BIN
xlsx/tests/calc_tests/SMALL_LARGE.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/statistical/MEADIAN_KURT_SKEW_HARMEAN.xlsx
Normal file
BIN
xlsx/tests/statistical/MEADIAN_KURT_SKEW_HARMEAN.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user