Compare commits
53 Commits
dani/widge
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
203d640dbd | ||
|
|
ba75ffcf4f | ||
|
|
b5c977d3aa | ||
|
|
4029441cea | ||
|
|
cd47c609a0 | ||
|
|
ae6acdcdd5 | ||
|
|
c196db2115 | ||
|
|
a3c201e4e4 | ||
|
|
126e62957a | ||
|
|
294a651ae5 | ||
|
|
6f8a1e0da6 | ||
|
|
205ba6ee2d | ||
|
|
547b331773 | ||
|
|
f96612cf23 | ||
|
|
745435b950 | ||
|
|
4ca996cd3f | ||
|
|
3fbb91c414 | ||
|
|
93c9c42607 | ||
|
|
11edc2378e | ||
|
|
962e70c834 | ||
|
|
f803dad0a3 | ||
|
|
19580fc1ad | ||
|
|
e760b2d08e | ||
|
|
0e6ded7154 | ||
|
|
db26403432 | ||
|
|
9193479cce | ||
|
|
f814a75ae5 | ||
|
|
c8da5efb5f | ||
|
|
522e734395 | ||
|
|
2a7d59e512 | ||
|
|
c4142d4bf8 | ||
|
|
885d344b5b | ||
|
|
bed6f007cd | ||
|
|
dbd1b2df60 | ||
|
|
db552047c8 | ||
|
|
bcbacdb0a3 | ||
|
|
d0f37854d9 | ||
|
|
99b03f70c3 | ||
|
|
3e1605a494 | ||
|
|
d6aad08e73 | ||
|
|
8597d14a4e | ||
|
|
01b19b9c35 | ||
|
|
4649a0c78c | ||
|
|
cd0baf5ba7 | ||
|
|
167d169f1a | ||
|
|
080574b112 | ||
|
|
6056b8f122 | ||
|
|
e61b15655a | ||
|
|
6822505602 | ||
|
|
25f7891343 | ||
|
|
bdd0af0a39 | ||
|
|
261924396d | ||
|
|
67ef3bcf87 |
@@ -12,6 +12,9 @@ pub(crate) const DEFAULT_WINDOW_WIDTH: i64 = 800;
|
|||||||
pub(crate) const LAST_COLUMN: i32 = 16_384;
|
pub(crate) const LAST_COLUMN: i32 = 16_384;
|
||||||
pub(crate) const LAST_ROW: i32 = 1_048_576;
|
pub(crate) const LAST_ROW: i32 = 1_048_576;
|
||||||
|
|
||||||
|
// Excel uses 15 significant digits of precision for all numeric calculations.
|
||||||
|
pub(crate) const EXCEL_PRECISION: usize = 15;
|
||||||
|
|
||||||
// 693_594 is computed as:
|
// 693_594 is computed as:
|
||||||
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
||||||
// The 2 days offset is because of Excel 1900 bug
|
// The 2 days offset is because of Excel 1900 bug
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -711,6 +711,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
Function::Value => args_signature_scalars(arg_count, 1, 0),
|
Function::Value => args_signature_scalars(arg_count, 1, 0),
|
||||||
Function::Valuetotext => args_signature_scalars(arg_count, 1, 1),
|
Function::Valuetotext => args_signature_scalars(arg_count, 1, 1),
|
||||||
Function::Average => vec![Signature::Vector; arg_count],
|
Function::Average => vec![Signature::Vector; arg_count],
|
||||||
|
Function::Avedev => vec![Signature::Vector; arg_count],
|
||||||
Function::Averagea => vec![Signature::Vector; arg_count],
|
Function::Averagea => vec![Signature::Vector; arg_count],
|
||||||
Function::Averageif => args_signature_sumif(arg_count),
|
Function::Averageif => args_signature_sumif(arg_count),
|
||||||
Function::Averageifs => vec![Signature::Vector; arg_count],
|
Function::Averageifs => vec![Signature::Vector; arg_count],
|
||||||
@@ -871,12 +872,10 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
Function::Combin => args_signature_scalars(arg_count, 2, 0),
|
Function::Combin => args_signature_scalars(arg_count, 2, 0),
|
||||||
Function::Combina => args_signature_scalars(arg_count, 2, 0),
|
Function::Combina => args_signature_scalars(arg_count, 2, 0),
|
||||||
Function::Sumsq => vec![Signature::Vector; arg_count],
|
Function::Sumsq => vec![Signature::Vector; arg_count],
|
||||||
|
|
||||||
Function::N => args_signature_scalars(arg_count, 1, 0),
|
Function::N => args_signature_scalars(arg_count, 1, 0),
|
||||||
Function::Sheets => args_signature_scalars(arg_count, 0, 1),
|
Function::Sheets => args_signature_scalars(arg_count, 0, 1),
|
||||||
Function::Cell => args_signature_scalars(arg_count, 1, 1),
|
Function::Cell => args_signature_scalars(arg_count, 1, 1),
|
||||||
Function::Info => args_signature_scalars(arg_count, 1, 1),
|
Function::Info => args_signature_scalars(arg_count, 1, 1),
|
||||||
|
|
||||||
Function::Daverage => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
Function::Daverage => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
Function::Dcount => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
Function::Dcount => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
Function::Dget => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
Function::Dget => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
@@ -889,6 +888,125 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
Function::Dvar => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
Function::Dvar => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
Function::Dvarp => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
Function::Dvarp => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
Function::Dstdevp => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
Function::Dstdevp => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
|
Function::BetaDist => args_signature_scalars(arg_count, 4, 2),
|
||||||
|
Function::BetaInv => args_signature_scalars(arg_count, 3, 2),
|
||||||
|
Function::BinomDist => args_signature_scalars(arg_count, 4, 0),
|
||||||
|
Function::BinomDistRange => args_signature_scalars(arg_count, 3, 1),
|
||||||
|
Function::BinomInv => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::ChisqDist => args_signature_scalars(arg_count, 4, 0),
|
||||||
|
Function::ChisqDistRT => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::ChisqInv => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::ChisqInvRT => args_signature_scalars(arg_count, 2, 0),
|
||||||
|
Function::ChisqTest => {
|
||||||
|
if arg_count == 2 {
|
||||||
|
vec![Signature::Vector, Signature::Vector]
|
||||||
|
} else {
|
||||||
|
vec![Signature::Error; arg_count]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Function::ConfidenceNorm => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::ConfidenceT => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::CovarianceP => {
|
||||||
|
if arg_count == 2 {
|
||||||
|
vec![Signature::Vector, Signature::Vector]
|
||||||
|
} else {
|
||||||
|
vec![Signature::Error; arg_count]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Function::CovarianceS => {
|
||||||
|
if arg_count == 2 {
|
||||||
|
vec![Signature::Vector, Signature::Vector]
|
||||||
|
} else {
|
||||||
|
vec![Signature::Error; arg_count]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Function::Devsq => vec![Signature::Vector; arg_count],
|
||||||
|
Function::ExponDist => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::FDist => args_signature_scalars(arg_count, 4, 0),
|
||||||
|
Function::FDistRT => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::FInv => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::FInvRT => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::FTest => vec![Signature::Vector; 2],
|
||||||
|
Function::Fisher => args_signature_scalars(arg_count, 1, 0),
|
||||||
|
Function::FisherInv => args_signature_scalars(arg_count, 1, 0),
|
||||||
|
Function::Gamma => args_signature_scalars(arg_count, 1, 0),
|
||||||
|
Function::GammaDist => args_signature_scalars(arg_count, 4, 0),
|
||||||
|
Function::GammaInv => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::GammaLn => args_signature_scalars(arg_count, 1, 0),
|
||||||
|
Function::GammaLnPrecise => args_signature_scalars(arg_count, 1, 0),
|
||||||
|
Function::HypGeomDist => args_signature_scalars(arg_count, 5, 0),
|
||||||
|
Function::LogNormDist => args_signature_scalars(arg_count, 4, 0),
|
||||||
|
Function::LogNormInv => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::NegbinomDist => args_signature_scalars(arg_count, 4, 0),
|
||||||
|
Function::NormDist => args_signature_scalars(arg_count, 4, 0),
|
||||||
|
Function::NormInv => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::NormSdist => args_signature_scalars(arg_count, 2, 0),
|
||||||
|
Function::NormSInv => args_signature_scalars(arg_count, 1, 0),
|
||||||
|
Function::Pearson => {
|
||||||
|
if arg_count == 2 {
|
||||||
|
vec![Signature::Vector, Signature::Vector]
|
||||||
|
} else {
|
||||||
|
vec![Signature::Error; arg_count]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Function::Phi => args_signature_scalars(arg_count, 1, 0),
|
||||||
|
Function::PoissonDist => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::Standardize => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::StDevP => vec![Signature::Vector; arg_count],
|
||||||
|
Function::StDevS => vec![Signature::Vector; arg_count],
|
||||||
|
Function::Stdeva => vec![Signature::Vector; arg_count],
|
||||||
|
Function::Stdevpa => vec![Signature::Vector; arg_count],
|
||||||
|
Function::TDist => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::TDist2T => args_signature_scalars(arg_count, 2, 0),
|
||||||
|
Function::TDistRT => args_signature_scalars(arg_count, 2, 0),
|
||||||
|
Function::TInv => args_signature_scalars(arg_count, 2, 0),
|
||||||
|
Function::TInv2T => args_signature_scalars(arg_count, 2, 0),
|
||||||
|
Function::TTest => {
|
||||||
|
if arg_count == 4 {
|
||||||
|
vec![
|
||||||
|
Signature::Vector,
|
||||||
|
Signature::Vector,
|
||||||
|
Signature::Scalar,
|
||||||
|
Signature::Scalar,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![Signature::Error; arg_count]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Function::VarP => vec![Signature::Vector; arg_count],
|
||||||
|
Function::VarS => vec![Signature::Vector; arg_count],
|
||||||
|
Function::VarpA => vec![Signature::Vector; arg_count],
|
||||||
|
Function::VarA => vec![Signature::Vector; arg_count],
|
||||||
|
Function::WeibullDist => args_signature_scalars(arg_count, 4, 0),
|
||||||
|
Function::ZTest => {
|
||||||
|
if arg_count == 2 {
|
||||||
|
vec![Signature::Vector, Signature::Scalar]
|
||||||
|
} else if arg_count == 3 {
|
||||||
|
vec![Signature::Vector, Signature::Scalar, Signature::Scalar]
|
||||||
|
} else {
|
||||||
|
vec![Signature::Error; arg_count]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Function::Sumx2my2 => vec![Signature::Vector; 2],
|
||||||
|
Function::Sumx2py2 => vec![Signature::Vector; 2],
|
||||||
|
Function::Sumxmy2 => vec![Signature::Vector; 2],
|
||||||
|
Function::Correl => vec![Signature::Vector; 2],
|
||||||
|
Function::Rsq => vec![Signature::Vector; 2],
|
||||||
|
Function::Intercept => 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],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -914,7 +1032,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Atan => scalar_arguments(args),
|
Function::Atan => scalar_arguments(args),
|
||||||
Function::Atan2 => scalar_arguments(args),
|
Function::Atan2 => scalar_arguments(args),
|
||||||
Function::Atanh => scalar_arguments(args),
|
Function::Atanh => scalar_arguments(args),
|
||||||
Function::Choose => scalar_arguments(args), // static_analysis_choose(args, cell),
|
Function::Choose => scalar_arguments(args),
|
||||||
Function::Column => not_implemented(args),
|
Function::Column => not_implemented(args),
|
||||||
Function::Columns => not_implemented(args),
|
Function::Columns => not_implemented(args),
|
||||||
Function::Cos => scalar_arguments(args),
|
Function::Cos => scalar_arguments(args),
|
||||||
@@ -961,7 +1079,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Lookup => not_implemented(args),
|
Function::Lookup => not_implemented(args),
|
||||||
Function::Match => not_implemented(args),
|
Function::Match => not_implemented(args),
|
||||||
Function::Offset => static_analysis_offset(args),
|
Function::Offset => static_analysis_offset(args),
|
||||||
// FIXME: Row could return an array
|
|
||||||
Function::Row => StaticResult::Scalar,
|
Function::Row => StaticResult::Scalar,
|
||||||
Function::Rows => not_implemented(args),
|
Function::Rows => not_implemented(args),
|
||||||
Function::Vlookup => not_implemented(args),
|
Function::Vlookup => not_implemented(args),
|
||||||
@@ -990,6 +1107,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Valuetotext => not_implemented(args),
|
Function::Valuetotext => not_implemented(args),
|
||||||
Function::Average => not_implemented(args),
|
Function::Average => not_implemented(args),
|
||||||
Function::Averagea => not_implemented(args),
|
Function::Averagea => not_implemented(args),
|
||||||
|
Function::Avedev => not_implemented(args),
|
||||||
Function::Averageif => not_implemented(args),
|
Function::Averageif => not_implemented(args),
|
||||||
Function::Averageifs => not_implemented(args),
|
Function::Averageifs => not_implemented(args),
|
||||||
Function::Count => not_implemented(args),
|
Function::Count => not_implemented(args),
|
||||||
@@ -1152,7 +1270,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Sheets => scalar_arguments(args),
|
Function::Sheets => scalar_arguments(args),
|
||||||
Function::Cell => scalar_arguments(args),
|
Function::Cell => scalar_arguments(args),
|
||||||
Function::Info => scalar_arguments(args),
|
Function::Info => scalar_arguments(args),
|
||||||
|
|
||||||
Function::Dget => not_implemented(args),
|
Function::Dget => not_implemented(args),
|
||||||
Function::Dmax => not_implemented(args),
|
Function::Dmax => not_implemented(args),
|
||||||
Function::Dmin => not_implemented(args),
|
Function::Dmin => not_implemented(args),
|
||||||
@@ -1165,5 +1282,81 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Dvar => not_implemented(args),
|
Function::Dvar => not_implemented(args),
|
||||||
Function::Dvarp => not_implemented(args),
|
Function::Dvarp => not_implemented(args),
|
||||||
Function::Dstdevp => not_implemented(args),
|
Function::Dstdevp => not_implemented(args),
|
||||||
|
Function::BetaDist => StaticResult::Scalar,
|
||||||
|
Function::BetaInv => StaticResult::Scalar,
|
||||||
|
Function::BinomDist => StaticResult::Scalar,
|
||||||
|
Function::BinomDistRange => StaticResult::Scalar,
|
||||||
|
Function::BinomInv => StaticResult::Scalar,
|
||||||
|
Function::ChisqDist => StaticResult::Scalar,
|
||||||
|
Function::ChisqDistRT => StaticResult::Scalar,
|
||||||
|
Function::ChisqInv => StaticResult::Scalar,
|
||||||
|
Function::ChisqInvRT => StaticResult::Scalar,
|
||||||
|
Function::ChisqTest => StaticResult::Scalar,
|
||||||
|
Function::ConfidenceNorm => StaticResult::Scalar,
|
||||||
|
Function::ConfidenceT => StaticResult::Scalar,
|
||||||
|
Function::CovarianceP => StaticResult::Scalar,
|
||||||
|
Function::CovarianceS => StaticResult::Scalar,
|
||||||
|
Function::Devsq => StaticResult::Scalar,
|
||||||
|
Function::ExponDist => StaticResult::Scalar,
|
||||||
|
Function::FDist => StaticResult::Scalar,
|
||||||
|
Function::FDistRT => StaticResult::Scalar,
|
||||||
|
Function::FInv => StaticResult::Scalar,
|
||||||
|
Function::FInvRT => StaticResult::Scalar,
|
||||||
|
Function::FTest => StaticResult::Scalar,
|
||||||
|
Function::Fisher => StaticResult::Scalar,
|
||||||
|
Function::FisherInv => StaticResult::Scalar,
|
||||||
|
Function::Gamma => StaticResult::Scalar,
|
||||||
|
Function::GammaDist => StaticResult::Scalar,
|
||||||
|
Function::GammaInv => StaticResult::Scalar,
|
||||||
|
Function::GammaLn => StaticResult::Scalar,
|
||||||
|
Function::GammaLnPrecise => StaticResult::Scalar,
|
||||||
|
Function::HypGeomDist => StaticResult::Scalar,
|
||||||
|
Function::LogNormDist => StaticResult::Scalar,
|
||||||
|
Function::LogNormInv => StaticResult::Scalar,
|
||||||
|
Function::NegbinomDist => StaticResult::Scalar,
|
||||||
|
Function::NormDist => StaticResult::Scalar,
|
||||||
|
Function::NormInv => StaticResult::Scalar,
|
||||||
|
Function::NormSdist => StaticResult::Scalar,
|
||||||
|
Function::NormSInv => StaticResult::Scalar,
|
||||||
|
Function::Pearson => StaticResult::Scalar,
|
||||||
|
Function::Phi => StaticResult::Scalar,
|
||||||
|
Function::PoissonDist => StaticResult::Scalar,
|
||||||
|
Function::Standardize => StaticResult::Scalar,
|
||||||
|
Function::StDevP => StaticResult::Scalar,
|
||||||
|
Function::StDevS => StaticResult::Scalar,
|
||||||
|
Function::Stdeva => StaticResult::Scalar,
|
||||||
|
Function::Stdevpa => StaticResult::Scalar,
|
||||||
|
Function::TDist => StaticResult::Scalar,
|
||||||
|
Function::TDist2T => StaticResult::Scalar,
|
||||||
|
Function::TDistRT => StaticResult::Scalar,
|
||||||
|
Function::TInv => StaticResult::Scalar,
|
||||||
|
Function::TInv2T => StaticResult::Scalar,
|
||||||
|
Function::TTest => StaticResult::Scalar,
|
||||||
|
Function::VarP => StaticResult::Scalar,
|
||||||
|
Function::VarS => StaticResult::Scalar,
|
||||||
|
Function::VarpA => StaticResult::Scalar,
|
||||||
|
Function::VarA => StaticResult::Scalar,
|
||||||
|
Function::WeibullDist => StaticResult::Scalar,
|
||||||
|
Function::ZTest => StaticResult::Scalar,
|
||||||
|
Function::Sumx2my2 => StaticResult::Scalar,
|
||||||
|
Function::Sumx2py2 => StaticResult::Scalar,
|
||||||
|
Function::Sumxmy2 => StaticResult::Scalar,
|
||||||
|
Function::Correl => StaticResult::Scalar,
|
||||||
|
Function::Rsq => StaticResult::Scalar,
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use chrono::Months;
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use chrono::NaiveTime;
|
use chrono::NaiveTime;
|
||||||
use chrono::Timelike;
|
use chrono::Timelike;
|
||||||
|
use chrono_tz::Tz;
|
||||||
|
|
||||||
const SECONDS_PER_DAY: i32 = 86_400;
|
const SECONDS_PER_DAY: i32 = 86_400;
|
||||||
const SECONDS_PER_DAY_F64: f64 = SECONDS_PER_DAY as f64;
|
const SECONDS_PER_DAY_F64: f64 = SECONDS_PER_DAY as f64;
|
||||||
@@ -770,12 +771,12 @@ impl Model {
|
|||||||
Ok(values)
|
Ok(values)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the current date/time as an Excel serial number in the model's configured timezone.
|
// Returns the current date/time as an Excel serial number in the given timezone.
|
||||||
// Used by TODAY() and NOW().
|
// Used by TODAY() and NOW().
|
||||||
fn current_excel_serial(&self) -> Option<f64> {
|
pub(crate) fn current_excel_serial_with_timezone(&self, tz: Tz) -> Option<f64> {
|
||||||
let seconds = get_milliseconds_since_epoch() / 1000;
|
let seconds = get_milliseconds_since_epoch() / 1000;
|
||||||
DateTime::from_timestamp(seconds, 0).map(|dt| {
|
DateTime::from_timestamp(seconds, 0).map(|dt| {
|
||||||
let local_time = dt.with_timezone(&self.tz);
|
let local_time = dt.with_timezone(&tz);
|
||||||
let days_from_1900 = local_time.num_days_from_ce() - EXCEL_DATE_BASE;
|
let days_from_1900 = local_time.num_days_from_ce() - EXCEL_DATE_BASE;
|
||||||
let fraction = (local_time.num_seconds_from_midnight() as f64) / (60.0 * 60.0 * 24.0);
|
let fraction = (local_time.num_seconds_from_midnight() as f64) / (60.0 * 60.0 * 24.0);
|
||||||
days_from_1900 as f64 + fraction
|
days_from_1900 as f64 + fraction
|
||||||
@@ -978,7 +979,7 @@ impl Model {
|
|||||||
message: "Wrong number of arguments".to_string(),
|
message: "Wrong number of arguments".to_string(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
match self.current_excel_serial() {
|
match self.current_excel_serial_with_timezone(self.tz) {
|
||||||
Some(serial) => CalcResult::Number(serial.floor()),
|
Some(serial) => CalcResult::Number(serial.floor()),
|
||||||
None => CalcResult::Error {
|
None => CalcResult::Error {
|
||||||
error: Error::ERROR,
|
error: Error::ERROR,
|
||||||
@@ -989,14 +990,35 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fn_now(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_now(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if !args.is_empty() {
|
if args.len() > 1 {
|
||||||
return CalcResult::Error {
|
return CalcResult::Error {
|
||||||
error: Error::ERROR,
|
error: Error::ERROR,
|
||||||
origin: cell,
|
origin: cell,
|
||||||
message: "Wrong number of arguments".to_string(),
|
message: "Wrong number of arguments".to_string(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
match self.current_excel_serial() {
|
let tz = match args.first() {
|
||||||
|
Some(arg0) => {
|
||||||
|
// Parse timezone argument
|
||||||
|
let tz_str = match self.get_string(arg0, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let tz: Tz = match tz_str.parse() {
|
||||||
|
Ok(tz) => tz,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: format!("Invalid timezone: {}", &tz_str),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tz
|
||||||
|
}
|
||||||
|
None => self.tz,
|
||||||
|
};
|
||||||
|
match self.current_excel_serial_with_timezone(tz) {
|
||||||
Some(serial) => CalcResult::Number(serial),
|
Some(serial) => CalcResult::Number(serial),
|
||||||
None => CalcResult::Error {
|
None => CalcResult::Error {
|
||||||
error: Error::ERROR,
|
error: Error::ERROR,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use crate::cast::NumberOrArray;
|
use crate::cast::NumberOrArray;
|
||||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
use crate::constants::{EXCEL_PRECISION, LAST_COLUMN, LAST_ROW};
|
||||||
use crate::expressions::parser::ArrayNode;
|
use crate::expressions::parser::ArrayNode;
|
||||||
use crate::expressions::types::CellReferenceIndex;
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
use crate::functions::math_util::{from_roman, to_roman_with_form};
|
use crate::functions::math_util::{from_roman, to_roman_with_form};
|
||||||
use crate::number_format::to_precision;
|
use crate::number_format::{to_excel_precision, to_precision};
|
||||||
use crate::single_number_fn;
|
use crate::single_number_fn;
|
||||||
use crate::{
|
use crate::{
|
||||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
@@ -984,7 +984,9 @@ impl Model {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = f64::floor(value / significance) * significance;
|
// Apply Excel precision to the ratio to handle floating-point rounding errors
|
||||||
|
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||||
|
let result = f64::floor(ratio) * significance;
|
||||||
CalcResult::Number(result)
|
CalcResult::Number(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1022,7 +1024,7 @@ impl Model {
|
|||||||
cell: CellReferenceIndex,
|
cell: CellReferenceIndex,
|
||||||
) -> CalcResult {
|
) -> CalcResult {
|
||||||
let arg_count = args.len();
|
let arg_count = args.len();
|
||||||
if arg_count > 3 {
|
if !(1..=3).contains(&arg_count) {
|
||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let value = match self.get_number(&args[0], cell) {
|
let value = match self.get_number(&args[0], cell) {
|
||||||
@@ -1063,7 +1065,7 @@ impl Model {
|
|||||||
cell: CellReferenceIndex,
|
cell: CellReferenceIndex,
|
||||||
) -> CalcResult {
|
) -> CalcResult {
|
||||||
let arg_count = args.len();
|
let arg_count = args.len();
|
||||||
if arg_count > 2 {
|
if !(1..=2).contains(&arg_count) {
|
||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let value = match self.get_number(&args[0], cell) {
|
let value = match self.get_number(&args[0], cell) {
|
||||||
@@ -1093,7 +1095,7 @@ impl Model {
|
|||||||
|
|
||||||
pub(crate) fn fn_floor_math(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_floor_math(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
let arg_count = args.len();
|
let arg_count = args.len();
|
||||||
if arg_count > 3 {
|
if !(1..=3).contains(&arg_count) {
|
||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let value = match self.get_number(&args[0], cell) {
|
let value = match self.get_number(&args[0], cell) {
|
||||||
@@ -1121,10 +1123,14 @@ impl Model {
|
|||||||
}
|
}
|
||||||
let significance = significance.abs();
|
let significance = significance.abs();
|
||||||
if value < 0.0 && mode != 0.0 {
|
if value < 0.0 && mode != 0.0 {
|
||||||
let result = f64::ceil(value / significance) * significance;
|
// Apply Excel precision to handle floating-point rounding errors
|
||||||
|
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||||
|
let result = f64::ceil(ratio) * significance;
|
||||||
CalcResult::Number(result)
|
CalcResult::Number(result)
|
||||||
} else {
|
} else {
|
||||||
let result = f64::floor(value / significance) * significance;
|
// Apply Excel precision to handle floating-point rounding errors
|
||||||
|
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||||
|
let result = f64::floor(ratio) * significance;
|
||||||
CalcResult::Number(result)
|
CalcResult::Number(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1135,7 +1141,7 @@ impl Model {
|
|||||||
cell: CellReferenceIndex,
|
cell: CellReferenceIndex,
|
||||||
) -> CalcResult {
|
) -> CalcResult {
|
||||||
let arg_count = args.len();
|
let arg_count = args.len();
|
||||||
if arg_count > 2 {
|
if !(1..=2).contains(&arg_count) {
|
||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let value = match self.get_number(&args[0], cell) {
|
let value = match self.get_number(&args[0], cell) {
|
||||||
@@ -1154,7 +1160,9 @@ impl Model {
|
|||||||
return CalcResult::Number(0.0);
|
return CalcResult::Number(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = f64::floor(value / significance) * significance;
|
// Apply Excel precision to handle floating-point rounding errors
|
||||||
|
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||||
|
let result = f64::floor(ratio) * significance;
|
||||||
CalcResult::Number(result)
|
CalcResult::Number(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1209,7 +1217,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fn_trunc(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_trunc(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() > 2 {
|
if !(1..=2).contains(&args.len()) {
|
||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let value = match self.get_number(&args[0], cell) {
|
let value = match self.get_number(&args[0], cell) {
|
||||||
|
|||||||
230
base/src/functions/mathematical_sum.rs
Normal file
230
base/src/functions/mathematical_sum.rs
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
type TwoMatricesResult = (i32, i32, Vec<Option<f64>>, Vec<Option<f64>>);
|
||||||
|
|
||||||
|
// Helper to check if two shapes are the same or compatible 1D shapes
|
||||||
|
fn is_same_shape_or_1d(rows1: i32, cols1: i32, rows2: i32, cols2: i32) -> bool {
|
||||||
|
(rows1 == rows2 && cols1 == cols2)
|
||||||
|
|| (rows1 == 1 && cols2 == 1 && cols1 == rows2)
|
||||||
|
|| (rows2 == 1 && cols1 == 1 && cols2 == rows1)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// SUMX2MY2(array_x, array_y) - Returns the sum of the difference of squares
|
||||||
|
pub(crate) fn fn_sumx2my2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let result = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(s) => return s,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_, _, values_left, values_right) = result;
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||||
|
let x = x_opt.unwrap_or(0.0);
|
||||||
|
let y = y_opt.unwrap_or(0.0);
|
||||||
|
sum += x * x - y * y;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SUMX2PY2(array_x, array_y) - Returns the sum of the sum of squares
|
||||||
|
pub(crate) fn fn_sumx2py2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let result = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(s) => return s,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_rows, _cols, values_left, values_right) = result;
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||||
|
let x = x_opt.unwrap_or(0.0);
|
||||||
|
let y = y_opt.unwrap_or(0.0);
|
||||||
|
sum += x * x + y * y;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SUMXMY2(array_x, array_y) - Returns the sum of squares of differences
|
||||||
|
pub(crate) fn fn_sumxmy2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let result = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(s) => return s,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_, _, values_left, values_right) = result;
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||||
|
let x = x_opt.unwrap_or(0.0);
|
||||||
|
let y = y_opt.unwrap_or(0.0);
|
||||||
|
let diff = x - y;
|
||||||
|
sum += diff * diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to extract and validate two matrices (ranges or arrays) with compatible shapes.
|
||||||
|
// Returns (rows, cols, values_left, values_right) or an error.
|
||||||
|
pub(crate) fn fn_get_two_matrices(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> Result<TwoMatricesResult, CalcResult> {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return Err(CalcResult::new_args_number_error(cell));
|
||||||
|
}
|
||||||
|
let x_range = self.evaluate_node_in_context(&args[0], cell);
|
||||||
|
let y_range = self.evaluate_node_in_context(&args[1], cell);
|
||||||
|
|
||||||
|
let result = match (x_range, y_range) {
|
||||||
|
(
|
||||||
|
CalcResult::Range {
|
||||||
|
left: l1,
|
||||||
|
right: r1,
|
||||||
|
},
|
||||||
|
CalcResult::Range {
|
||||||
|
left: l2,
|
||||||
|
right: r2,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
if l1.sheet != l2.sheet {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let rows1 = r1.row - l1.row + 1;
|
||||||
|
let cols1 = r1.column - l1.column + 1;
|
||||||
|
let rows2 = r2.row - l2.row + 1;
|
||||||
|
let cols2 = r2.column - l2.column + 1;
|
||||||
|
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges must be of the same shape".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let values_left = self.values_from_range(l1, r1)?;
|
||||||
|
let values_right = self.values_from_range(l2, r2)?;
|
||||||
|
(rows1, cols1, values_left, values_right)
|
||||||
|
}
|
||||||
|
(
|
||||||
|
CalcResult::Array(left),
|
||||||
|
CalcResult::Range {
|
||||||
|
left: l2,
|
||||||
|
right: r2,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
let rows2 = r2.row - l2.row + 1;
|
||||||
|
let cols2 = r2.column - l2.column + 1;
|
||||||
|
|
||||||
|
let rows1 = left.len() as i32;
|
||||||
|
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
|
||||||
|
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Array and range must be of the same shape".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let values_left = match self.values_from_array(left) {
|
||||||
|
Err(error) => {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in first array: {:?}", error),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(v) => v,
|
||||||
|
};
|
||||||
|
let values_right = self.values_from_range(l2, r2)?;
|
||||||
|
(rows2, cols2, values_left, values_right)
|
||||||
|
}
|
||||||
|
(
|
||||||
|
CalcResult::Range {
|
||||||
|
left: l1,
|
||||||
|
right: r1,
|
||||||
|
},
|
||||||
|
CalcResult::Array(right),
|
||||||
|
) => {
|
||||||
|
let rows1 = r1.row - l1.row + 1;
|
||||||
|
let cols1 = r1.column - l1.column + 1;
|
||||||
|
|
||||||
|
let rows2 = right.len() as i32;
|
||||||
|
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
|
||||||
|
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Range and array must be of the same shape".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let values_left = self.values_from_range(l1, r1)?;
|
||||||
|
let values_right = match self.values_from_array(right) {
|
||||||
|
Err(error) => {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in second array: {:?}", error),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(v) => v,
|
||||||
|
};
|
||||||
|
(rows1, cols1, values_left, values_right)
|
||||||
|
}
|
||||||
|
(CalcResult::Array(left), CalcResult::Array(right)) => {
|
||||||
|
let rows1 = left.len() as i32;
|
||||||
|
let rows2 = right.len() as i32;
|
||||||
|
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
|
||||||
|
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
|
||||||
|
|
||||||
|
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Arrays must be of the same shape".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let values_left = match self.values_from_array(left) {
|
||||||
|
Err(error) => {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in first array: {:?}", error),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(v) => v,
|
||||||
|
};
|
||||||
|
let values_right = match self.values_from_array(right) {
|
||||||
|
Err(error) => {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in second array: {:?}", error),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(v) => v,
|
||||||
|
};
|
||||||
|
(rows1, cols1, values_left, values_right)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Both arguments must be ranges or arrays".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ mod lookup_and_reference;
|
|||||||
mod macros;
|
mod macros;
|
||||||
mod math_util;
|
mod math_util;
|
||||||
mod mathematical;
|
mod mathematical;
|
||||||
|
mod mathematical_sum;
|
||||||
mod statistical;
|
mod statistical;
|
||||||
mod subtotal;
|
mod subtotal;
|
||||||
mod text;
|
mod text;
|
||||||
@@ -76,6 +77,9 @@ pub enum Function {
|
|||||||
Sum,
|
Sum,
|
||||||
Sumif,
|
Sumif,
|
||||||
Sumifs,
|
Sumifs,
|
||||||
|
Sumx2my2,
|
||||||
|
Sumx2py2,
|
||||||
|
Sumxmy2,
|
||||||
Tan,
|
Tan,
|
||||||
Tanh,
|
Tanh,
|
||||||
Acot,
|
Acot,
|
||||||
@@ -190,6 +194,92 @@ pub enum Function {
|
|||||||
Minifs,
|
Minifs,
|
||||||
Geomean,
|
Geomean,
|
||||||
|
|
||||||
|
Avedev,
|
||||||
|
BetaDist,
|
||||||
|
BetaInv,
|
||||||
|
BinomDist,
|
||||||
|
BinomDistRange,
|
||||||
|
BinomInv,
|
||||||
|
ChisqDist,
|
||||||
|
ChisqDistRT,
|
||||||
|
ChisqInv,
|
||||||
|
ChisqInvRT,
|
||||||
|
ChisqTest,
|
||||||
|
ConfidenceNorm,
|
||||||
|
ConfidenceT,
|
||||||
|
CovarianceP,
|
||||||
|
CovarianceS,
|
||||||
|
Devsq,
|
||||||
|
ExponDist,
|
||||||
|
FDist,
|
||||||
|
FDistRT,
|
||||||
|
FInv,
|
||||||
|
FInvRT,
|
||||||
|
FTest,
|
||||||
|
Fisher,
|
||||||
|
FisherInv,
|
||||||
|
// Forecast,
|
||||||
|
Gamma,
|
||||||
|
GammaDist,
|
||||||
|
GammaInv,
|
||||||
|
GammaLn,
|
||||||
|
GammaLnPrecise,
|
||||||
|
Gauss,
|
||||||
|
Harmean,
|
||||||
|
HypGeomDist,
|
||||||
|
Kurt,
|
||||||
|
Large,
|
||||||
|
// Linest,
|
||||||
|
// Logest,
|
||||||
|
LogNormDist,
|
||||||
|
LogNormInv,
|
||||||
|
MaxA,
|
||||||
|
Median,
|
||||||
|
MinA,
|
||||||
|
// ModeMult,
|
||||||
|
// ModeSingl,
|
||||||
|
NegbinomDist,
|
||||||
|
NormDist,
|
||||||
|
NormInv,
|
||||||
|
NormSdist,
|
||||||
|
NormSInv,
|
||||||
|
Pearson,
|
||||||
|
// PercentileExc,
|
||||||
|
// PercentileInc,
|
||||||
|
// PercentrankExc,
|
||||||
|
// PercentrankInc,
|
||||||
|
// Permut,
|
||||||
|
// Permutationa,
|
||||||
|
Phi,
|
||||||
|
PoissonDist,
|
||||||
|
// Prob,
|
||||||
|
// QuartileExc,
|
||||||
|
// QuartileInc,
|
||||||
|
RankAvg,
|
||||||
|
RankEq,
|
||||||
|
Skew,
|
||||||
|
SkewP,
|
||||||
|
Small,
|
||||||
|
Standardize,
|
||||||
|
StDevP,
|
||||||
|
StDevS,
|
||||||
|
Stdeva,
|
||||||
|
Stdevpa,
|
||||||
|
TDist,
|
||||||
|
TDist2T,
|
||||||
|
TDistRT,
|
||||||
|
TInv,
|
||||||
|
TInv2T,
|
||||||
|
TTest,
|
||||||
|
// Trend,
|
||||||
|
// Trimmean,
|
||||||
|
VarP,
|
||||||
|
VarS,
|
||||||
|
VarpA,
|
||||||
|
VarA,
|
||||||
|
WeibullDist,
|
||||||
|
ZTest,
|
||||||
|
|
||||||
// Date and time
|
// Date and time
|
||||||
Date,
|
Date,
|
||||||
Datedif,
|
Datedif,
|
||||||
@@ -325,10 +415,16 @@ pub enum Function {
|
|||||||
Dvar,
|
Dvar,
|
||||||
Dvarp,
|
Dvarp,
|
||||||
Dstdevp,
|
Dstdevp,
|
||||||
|
|
||||||
|
Correl,
|
||||||
|
Rsq,
|
||||||
|
Intercept,
|
||||||
|
Slope,
|
||||||
|
Steyx,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Function {
|
impl Function {
|
||||||
pub fn into_iter() -> IntoIter<Function, 268> {
|
pub fn into_iter() -> IntoIter<Function, 345> {
|
||||||
[
|
[
|
||||||
Function::And,
|
Function::And,
|
||||||
Function::False,
|
Function::False,
|
||||||
@@ -405,6 +501,9 @@ impl Function {
|
|||||||
Function::Sum,
|
Function::Sum,
|
||||||
Function::Sumif,
|
Function::Sumif,
|
||||||
Function::Sumifs,
|
Function::Sumifs,
|
||||||
|
Function::Sumx2my2,
|
||||||
|
Function::Sumx2py2,
|
||||||
|
Function::Sumxmy2,
|
||||||
Function::Choose,
|
Function::Choose,
|
||||||
Function::Column,
|
Function::Column,
|
||||||
Function::Columns,
|
Function::Columns,
|
||||||
@@ -453,6 +552,7 @@ impl Function {
|
|||||||
Function::Type,
|
Function::Type,
|
||||||
Function::Sheet,
|
Function::Sheet,
|
||||||
Function::Average,
|
Function::Average,
|
||||||
|
Function::Avedev,
|
||||||
Function::Averagea,
|
Function::Averagea,
|
||||||
Function::Averageif,
|
Function::Averageif,
|
||||||
Function::Averageifs,
|
Function::Averageifs,
|
||||||
@@ -598,6 +698,79 @@ impl Function {
|
|||||||
Function::Dvar,
|
Function::Dvar,
|
||||||
Function::Dvarp,
|
Function::Dvarp,
|
||||||
Function::Dstdevp,
|
Function::Dstdevp,
|
||||||
|
Function::BetaDist,
|
||||||
|
Function::BetaInv,
|
||||||
|
Function::BinomDist,
|
||||||
|
Function::BinomDistRange,
|
||||||
|
Function::BinomInv,
|
||||||
|
Function::ChisqDist,
|
||||||
|
Function::ChisqDistRT,
|
||||||
|
Function::ChisqInv,
|
||||||
|
Function::ChisqInvRT,
|
||||||
|
Function::ChisqTest,
|
||||||
|
Function::ConfidenceNorm,
|
||||||
|
Function::ConfidenceT,
|
||||||
|
Function::CovarianceP,
|
||||||
|
Function::CovarianceS,
|
||||||
|
Function::Devsq,
|
||||||
|
Function::ExponDist,
|
||||||
|
Function::FDist,
|
||||||
|
Function::FDistRT,
|
||||||
|
Function::FInv,
|
||||||
|
Function::FInvRT,
|
||||||
|
Function::FTest,
|
||||||
|
Function::Fisher,
|
||||||
|
Function::FisherInv,
|
||||||
|
Function::Gamma,
|
||||||
|
Function::GammaDist,
|
||||||
|
Function::GammaInv,
|
||||||
|
Function::GammaLn,
|
||||||
|
Function::GammaLnPrecise,
|
||||||
|
Function::HypGeomDist,
|
||||||
|
Function::LogNormDist,
|
||||||
|
Function::LogNormInv,
|
||||||
|
Function::NegbinomDist,
|
||||||
|
Function::NormDist,
|
||||||
|
Function::NormInv,
|
||||||
|
Function::NormSdist,
|
||||||
|
Function::NormSInv,
|
||||||
|
Function::Pearson,
|
||||||
|
Function::Phi,
|
||||||
|
Function::PoissonDist,
|
||||||
|
Function::Standardize,
|
||||||
|
Function::StDevP,
|
||||||
|
Function::StDevS,
|
||||||
|
Function::Stdeva,
|
||||||
|
Function::Stdevpa,
|
||||||
|
Function::TDist,
|
||||||
|
Function::TDist2T,
|
||||||
|
Function::TDistRT,
|
||||||
|
Function::TInv,
|
||||||
|
Function::TInv2T,
|
||||||
|
Function::TTest,
|
||||||
|
Function::VarP,
|
||||||
|
Function::VarS,
|
||||||
|
Function::VarpA,
|
||||||
|
Function::VarA,
|
||||||
|
Function::WeibullDist,
|
||||||
|
Function::ZTest,
|
||||||
|
Function::Correl,
|
||||||
|
Function::Rsq,
|
||||||
|
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()
|
.into_iter()
|
||||||
}
|
}
|
||||||
@@ -659,6 +832,70 @@ impl Function {
|
|||||||
Function::Sec => "_xlfn.SEC".to_string(),
|
Function::Sec => "_xlfn.SEC".to_string(),
|
||||||
Function::Sech => "_xlfn.SECH".to_string(),
|
Function::Sech => "_xlfn.SECH".to_string(),
|
||||||
Function::Acot => "_xlfn.ACOT".to_string(),
|
Function::Acot => "_xlfn.ACOT".to_string(),
|
||||||
|
Function::GammaLnPrecise => "_xlfn.GAMMALN.PRECISE".to_string(),
|
||||||
|
Function::Gamma => "_xlfn.GAMMA".to_string(),
|
||||||
|
Function::GammaInv => "_xlfn.GAMMA.INV".to_string(),
|
||||||
|
Function::GammaLn => "_xlfn.GAMMALN".to_string(),
|
||||||
|
Function::BetaDist => "_xlfn.BETA.DIST".to_string(),
|
||||||
|
Function::BetaInv => "_xlfn.BETA.INV".to_string(),
|
||||||
|
|
||||||
|
Function::BinomDist => "_xlfn.BINOM.DIST".to_string(),
|
||||||
|
Function::BinomDistRange => "_xlfn.BINOM.DIST.RANGE".to_string(),
|
||||||
|
Function::BinomInv => "_xlfn.BINOM.INV".to_string(),
|
||||||
|
Function::NegbinomDist => "_xlfn.NEGBINOM.DIST".to_string(),
|
||||||
|
|
||||||
|
Function::ChisqDist => "_xlfn.CHISQ.DIST".to_string(),
|
||||||
|
Function::ChisqDistRT => "_xlfn.CHISQ.DIST.RT".to_string(),
|
||||||
|
Function::ChisqInv => "_xlfn.CHISQ.INV".to_string(),
|
||||||
|
Function::ChisqInvRT => "_xlfn.CHISQ.INV.RT".to_string(),
|
||||||
|
Function::ChisqTest => "_xlfn.CHISQ.TEST".to_string(),
|
||||||
|
|
||||||
|
Function::ConfidenceNorm => "_xlfn.CONFIDENCE.NORM".to_string(),
|
||||||
|
Function::ConfidenceT => "_xlfn.CONFIDENCE.T".to_string(),
|
||||||
|
|
||||||
|
Function::CovarianceP => "_xlfn.COVARIANCE.P".to_string(),
|
||||||
|
Function::CovarianceS => "_xlfn.COVARIANCE.S".to_string(),
|
||||||
|
|
||||||
|
Function::ExponDist => "_xlfn.EXPON.DIST".to_string(),
|
||||||
|
|
||||||
|
Function::FDist => "_xlfn.F.DIST".to_string(),
|
||||||
|
Function::FDistRT => "_xlfn.F.DIST.RT".to_string(),
|
||||||
|
Function::FInv => "_xlfn.F.INV".to_string(),
|
||||||
|
Function::FInvRT => "_xlfn.F.INV.RT".to_string(),
|
||||||
|
Function::FTest => "_xlfn.F.TEST".to_string(),
|
||||||
|
|
||||||
|
Function::HypGeomDist => "_xlfn.HYPGEOM.DIST".to_string(),
|
||||||
|
|
||||||
|
Function::LogNormDist => "_xlfn.LOGNORM.DIST".to_string(),
|
||||||
|
Function::LogNormInv => "_xlfn.LOGNORM.INV".to_string(),
|
||||||
|
|
||||||
|
Function::NormDist => "_xlfn.NORM.DIST".to_string(),
|
||||||
|
Function::NormInv => "_xlfn.NORM.INV".to_string(),
|
||||||
|
Function::NormSdist => "_xlfn.NORM.S.DIST".to_string(),
|
||||||
|
Function::NormSInv => "_xlfn.NORM.S.INV".to_string(),
|
||||||
|
|
||||||
|
Function::Phi => "_xlfn.PHI".to_string(),
|
||||||
|
|
||||||
|
Function::PoissonDist => "_xlfn.POISSON.DIST".to_string(),
|
||||||
|
|
||||||
|
Function::StDevP => "_xlfn.STDEV.P".to_string(),
|
||||||
|
Function::StDevS => "_xlfn.STDEV.S".to_string(),
|
||||||
|
|
||||||
|
Function::TDist => "_xlfn.T.DIST".to_string(),
|
||||||
|
Function::TDist2T => "_xlfn.T.DIST.2T".to_string(),
|
||||||
|
Function::TDistRT => "_xlfn.T.DIST.RT".to_string(),
|
||||||
|
Function::TInv => "_xlfn.T.INV".to_string(),
|
||||||
|
Function::TInv2T => "_xlfn.T.INV.2T".to_string(),
|
||||||
|
Function::TTest => "_xlfn.T.TEST".to_string(),
|
||||||
|
|
||||||
|
Function::VarP => "_xlfn.VAR.P".to_string(),
|
||||||
|
Function::VarS => "_xlfn.VAR.S".to_string(),
|
||||||
|
|
||||||
|
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(),
|
_ => self.to_string(),
|
||||||
}
|
}
|
||||||
@@ -811,6 +1048,7 @@ impl Function {
|
|||||||
|
|
||||||
"AVERAGE" => Some(Function::Average),
|
"AVERAGE" => Some(Function::Average),
|
||||||
"AVERAGEA" => Some(Function::Averagea),
|
"AVERAGEA" => Some(Function::Averagea),
|
||||||
|
"AVEDEV" => Some(Function::Avedev),
|
||||||
"AVERAGEIF" => Some(Function::Averageif),
|
"AVERAGEIF" => Some(Function::Averageif),
|
||||||
"AVERAGEIFS" => Some(Function::Averageifs),
|
"AVERAGEIFS" => Some(Function::Averageifs),
|
||||||
"COUNT" => Some(Function::Count),
|
"COUNT" => Some(Function::Count),
|
||||||
@@ -957,6 +1195,85 @@ impl Function {
|
|||||||
"DVARP" => Some(Function::Dvarp),
|
"DVARP" => Some(Function::Dvarp),
|
||||||
"DSTDEVP" => Some(Function::Dstdevp),
|
"DSTDEVP" => Some(Function::Dstdevp),
|
||||||
|
|
||||||
|
"BETA.DIST" | "_XLFN.BETA.DIST" => Some(Function::BetaDist),
|
||||||
|
"BETA.INV" | "_XLFN.BETA.INV" => Some(Function::BetaInv),
|
||||||
|
"BINOM.DIST" | "_XLFN.BINOM.DIST" => Some(Function::BinomDist),
|
||||||
|
"BINOM.DIST.RANGE" | "_XLFN.BINOM.DIST.RANGE" => Some(Function::BinomDistRange),
|
||||||
|
"BINOM.INV" | "_XLFN.BINOM.INV" => Some(Function::BinomInv),
|
||||||
|
"CHISQ.DIST" | "_XLFN.CHISQ.DIST" => Some(Function::ChisqDist),
|
||||||
|
"CHISQ.DIST.RT" | "_XLFN.CHISQ.DIST.RT" => Some(Function::ChisqDistRT),
|
||||||
|
"CHISQ.INV" | "_XLFN.CHISQ.INV" => Some(Function::ChisqInv),
|
||||||
|
"CHISQ.INV.RT" | "_XLFN.CHISQ.INV.RT" => Some(Function::ChisqInvRT),
|
||||||
|
"CHISQ.TEST" | "_XLFN.CHISQ.TEST" => Some(Function::ChisqTest),
|
||||||
|
"CONFIDENCE.NORM" | "_XLFN.CONFIDENCE.NORM" => Some(Function::ConfidenceNorm),
|
||||||
|
"CONFIDENCE.T" | "_XLFN.CONFIDENCE.T" => Some(Function::ConfidenceT),
|
||||||
|
"COVARIANCE.P" | "_XLFN.COVARIANCE.P" => Some(Function::CovarianceP),
|
||||||
|
"COVARIANCE.S" | "_XLFN.COVARIANCE.S" => Some(Function::CovarianceS),
|
||||||
|
"DEVSQ" => Some(Function::Devsq),
|
||||||
|
"EXPON.DIST" | "_XLFN.EXPON.DIST" => Some(Function::ExponDist),
|
||||||
|
"F.DIST" | "_XLFN.F.DIST" => Some(Function::FDist),
|
||||||
|
"F.DIST.RT" | "_XLFN.F.DIST.RT" => Some(Function::FDistRT),
|
||||||
|
"F.INV" | "_XLFN.F.INV" => Some(Function::FInv),
|
||||||
|
"F.INV.RT" | "_XLFN.F.INV.RT" => Some(Function::FInvRT),
|
||||||
|
"F.TEST" | "_XLFN.F.TEST" => Some(Function::FTest),
|
||||||
|
"FISHER" => Some(Function::Fisher),
|
||||||
|
"FISHERINV" => Some(Function::FisherInv),
|
||||||
|
"GAMMA" | "_XLFN.GAMMA" => Some(Function::Gamma),
|
||||||
|
"GAMMA.DIST" | "_XLFN.GAMMA.DIST" => Some(Function::GammaDist),
|
||||||
|
"GAMMA.INV" | "_XLFN.GAMMA.INV" => Some(Function::GammaInv),
|
||||||
|
"GAMMALN" | "_XLFN.GAMMALN" => Some(Function::GammaLn),
|
||||||
|
"GAMMALN.PRECISE" | "_XLFN.GAMMALN.PRECISE" => Some(Function::GammaLnPrecise),
|
||||||
|
"HYPGEOM.DIST" | "_XLFN.HYPGEOM.DIST" => Some(Function::HypGeomDist),
|
||||||
|
"LOGNORM.DIST" | "_XLFN.LOGNORM.DIST" => Some(Function::LogNormDist),
|
||||||
|
"LOGNORM.INV" | "_XLFN.LOGNORM.INV" => Some(Function::LogNormInv),
|
||||||
|
"NEGBINOM.DIST" | "_XLFN.NEGBINOM.DIST" => Some(Function::NegbinomDist),
|
||||||
|
"NORM.DIST" | "_XLFN.NORM.DIST" => Some(Function::NormDist),
|
||||||
|
"NORM.INV" | "_XLFN.NORM.INV" => Some(Function::NormInv),
|
||||||
|
"NORM.S.DIST" | "_XLFN.NORM.S.DIST" => Some(Function::NormSdist),
|
||||||
|
"NORM.S.INV" | "_XLFN.NORM.S.INV" => Some(Function::NormSInv),
|
||||||
|
"PEARSON" => Some(Function::Pearson),
|
||||||
|
"PHI" | "_XLFN.PHI" => Some(Function::Phi),
|
||||||
|
"POISSON.DIST" | "_XLFN.POISSON.DIST" => Some(Function::PoissonDist),
|
||||||
|
"STANDARDIZE" => Some(Function::Standardize),
|
||||||
|
"STDEV.P" | "_XLFN.STDEV.P" => Some(Function::StDevP),
|
||||||
|
"STDEV.S" | "_XLFN.STDEV.S" => Some(Function::StDevS),
|
||||||
|
"STDEVA" => Some(Function::Stdeva),
|
||||||
|
"STDEVPA" => Some(Function::Stdevpa),
|
||||||
|
"T.DIST" | "_XLFN.T.DIST" => Some(Function::TDist),
|
||||||
|
"T.DIST.2T" | "_XLFN.T.DIST.2T" => Some(Function::TDist2T),
|
||||||
|
"T.DIST.RT" | "_XLFN.T.DIST.RT" => Some(Function::TDistRT),
|
||||||
|
"T.INV" | "_XLFN.T.INV" => Some(Function::TInv),
|
||||||
|
"T.INV.2T" | "_XLFN.T.INV.2T" => Some(Function::TInv2T),
|
||||||
|
"T.TEST" | "_XLFN.T.TEST" => Some(Function::TTest),
|
||||||
|
"VAR.P" | "_XLFN.VAR.P" => Some(Function::VarP),
|
||||||
|
"VAR.S" | "_XLFN.VAR.S" => Some(Function::VarS),
|
||||||
|
"VARPA" => Some(Function::VarpA),
|
||||||
|
"VARA" => Some(Function::VarA),
|
||||||
|
"WEIBULL.DIST" | "_XLFN.WEIBULL.DIST" => Some(Function::WeibullDist),
|
||||||
|
"Z.TEST" | "_XLFN.Z.TEST" => Some(Function::ZTest),
|
||||||
|
"SUMX2MY2" => Some(Function::Sumx2my2),
|
||||||
|
"SUMX2PY2" => Some(Function::Sumx2py2),
|
||||||
|
"SUMXMY2" => Some(Function::Sumxmy2),
|
||||||
|
"CORREL" => Some(Function::Correl),
|
||||||
|
"RSQ" => Some(Function::Rsq),
|
||||||
|
"INTERCEPT" => Some(Function::Intercept),
|
||||||
|
"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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1065,6 +1382,7 @@ impl fmt::Display for Function {
|
|||||||
Function::Sheet => write!(f, "SHEET"),
|
Function::Sheet => write!(f, "SHEET"),
|
||||||
Function::Average => write!(f, "AVERAGE"),
|
Function::Average => write!(f, "AVERAGE"),
|
||||||
Function::Averagea => write!(f, "AVERAGEA"),
|
Function::Averagea => write!(f, "AVERAGEA"),
|
||||||
|
Function::Avedev => write!(f, "AVEDEV"),
|
||||||
Function::Averageif => write!(f, "AVERAGEIF"),
|
Function::Averageif => write!(f, "AVERAGEIF"),
|
||||||
Function::Averageifs => write!(f, "AVERAGEIFS"),
|
Function::Averageifs => write!(f, "AVERAGEIFS"),
|
||||||
Function::Count => write!(f, "COUNT"),
|
Function::Count => write!(f, "COUNT"),
|
||||||
@@ -1217,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"),
|
||||||
@@ -1234,6 +1551,83 @@ 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::BetaInv => write!(f, "BETA.INV"),
|
||||||
|
Function::BinomDist => write!(f, "BINOM.DIST"),
|
||||||
|
Function::BinomDistRange => write!(f, "BINOM.DIST.RANGE"),
|
||||||
|
Function::BinomInv => write!(f, "BINOM.INV"),
|
||||||
|
Function::ChisqDist => write!(f, "CHISQ.DIST"),
|
||||||
|
Function::ChisqDistRT => write!(f, "CHISQ.DIST.RT"),
|
||||||
|
Function::ChisqInv => write!(f, "CHISQ.INV"),
|
||||||
|
Function::ChisqInvRT => write!(f, "CHISQ.INV.RT"),
|
||||||
|
Function::ChisqTest => write!(f, "CHISQ.TEST"),
|
||||||
|
Function::ConfidenceNorm => write!(f, "CONFIDENCE.NORM"),
|
||||||
|
Function::ConfidenceT => write!(f, "CONFIDENCE.T"),
|
||||||
|
Function::CovarianceP => write!(f, "COVARIANCE.P"),
|
||||||
|
Function::CovarianceS => write!(f, "COVARIANCE.S"),
|
||||||
|
Function::Devsq => write!(f, "DEVSQ"),
|
||||||
|
Function::ExponDist => write!(f, "EXPON.DIST"),
|
||||||
|
Function::FDist => write!(f, "F.DIST"),
|
||||||
|
Function::FDistRT => write!(f, "F.DIST.RT"),
|
||||||
|
Function::FInv => write!(f, "F.INV"),
|
||||||
|
Function::FInvRT => write!(f, "F.INV.RT"),
|
||||||
|
Function::Fisher => write!(f, "FISHER"),
|
||||||
|
Function::FisherInv => write!(f, "FISHERINV"),
|
||||||
|
Function::FTest => write!(f, "F.TEST"),
|
||||||
|
Function::Gamma => write!(f, "GAMMA"),
|
||||||
|
Function::GammaDist => write!(f, "GAMMA.DIST"),
|
||||||
|
Function::GammaInv => write!(f, "GAMMA.INV"),
|
||||||
|
Function::GammaLn => write!(f, "GAMMALN"),
|
||||||
|
Function::GammaLnPrecise => write!(f, "GAMMALN.PRECISE"),
|
||||||
|
Function::HypGeomDist => write!(f, "HYPGEOM.DIST"),
|
||||||
|
Function::LogNormDist => write!(f, "LOGNORM.DIST"),
|
||||||
|
Function::LogNormInv => write!(f, "LOGNORM.INV"),
|
||||||
|
Function::NegbinomDist => write!(f, "NEGBINOM.DIST"),
|
||||||
|
Function::NormDist => write!(f, "NORM.DIST"),
|
||||||
|
Function::NormInv => write!(f, "NORM.INV"),
|
||||||
|
Function::NormSdist => write!(f, "NORM.S.DIST"),
|
||||||
|
Function::NormSInv => write!(f, "NORM.S.INV"),
|
||||||
|
Function::Pearson => write!(f, "PEARSON"),
|
||||||
|
Function::Phi => write!(f, "PHI"),
|
||||||
|
Function::PoissonDist => write!(f, "POISSON.DIST"),
|
||||||
|
Function::Standardize => write!(f, "STANDARDIZE"),
|
||||||
|
Function::StDevP => write!(f, "STDEV.P"),
|
||||||
|
Function::StDevS => write!(f, "STDEV.S"),
|
||||||
|
Function::Stdeva => write!(f, "STDEVA"),
|
||||||
|
Function::Stdevpa => write!(f, "STDEVPA"),
|
||||||
|
Function::TDist => write!(f, "T.DIST"),
|
||||||
|
Function::TDist2T => write!(f, "T.DIST.2T"),
|
||||||
|
Function::TDistRT => write!(f, "T.DIST.RT"),
|
||||||
|
Function::TInv => write!(f, "T.INV"),
|
||||||
|
Function::TInv2T => write!(f, "T.INV.2T"),
|
||||||
|
Function::TTest => write!(f, "T.TEST"),
|
||||||
|
Function::VarP => write!(f, "VAR.P"),
|
||||||
|
Function::VarS => write!(f, "VAR.S"),
|
||||||
|
Function::VarpA => write!(f, "VARPA"),
|
||||||
|
Function::VarA => write!(f, "VARA"),
|
||||||
|
Function::WeibullDist => write!(f, "WEIBULL.DIST"),
|
||||||
|
Function::ZTest => write!(f, "Z.TEST"),
|
||||||
|
Function::Sumx2my2 => write!(f, "SUMX2MY2"),
|
||||||
|
Function::Sumx2py2 => write!(f, "SUMX2PY2"),
|
||||||
|
Function::Sumxmy2 => write!(f, "SUMXMY2"),
|
||||||
|
Function::Correl => write!(f, "CORREL"),
|
||||||
|
Function::Rsq => write!(f, "RSQ"),
|
||||||
|
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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1354,6 +1748,7 @@ impl Model {
|
|||||||
Function::Sheet => self.fn_sheet(args, cell),
|
Function::Sheet => self.fn_sheet(args, cell),
|
||||||
Function::Average => self.fn_average(args, cell),
|
Function::Average => self.fn_average(args, cell),
|
||||||
Function::Averagea => self.fn_averagea(args, cell),
|
Function::Averagea => self.fn_averagea(args, cell),
|
||||||
|
Function::Avedev => self.fn_avedev(args, cell),
|
||||||
Function::Averageif => self.fn_averageif(args, cell),
|
Function::Averageif => self.fn_averageif(args, cell),
|
||||||
Function::Averageifs => self.fn_averageifs(args, cell),
|
Function::Averageifs => self.fn_averageifs(args, cell),
|
||||||
Function::Count => self.fn_count(args, cell),
|
Function::Count => self.fn_count(args, cell),
|
||||||
@@ -1530,6 +1925,82 @@ impl Model {
|
|||||||
Function::Dvar => self.fn_dvar(args, cell),
|
Function::Dvar => self.fn_dvar(args, cell),
|
||||||
Function::Dvarp => self.fn_dvarp(args, cell),
|
Function::Dvarp => self.fn_dvarp(args, cell),
|
||||||
Function::Dstdevp => self.fn_dstdevp(args, cell),
|
Function::Dstdevp => self.fn_dstdevp(args, cell),
|
||||||
|
Function::BetaDist => self.fn_beta_dist(args, cell),
|
||||||
|
Function::BetaInv => self.fn_beta_inv(args, cell),
|
||||||
|
Function::BinomDist => self.fn_binom_dist(args, cell),
|
||||||
|
Function::BinomDistRange => self.fn_binom_dist_range(args, cell),
|
||||||
|
Function::BinomInv => self.fn_binom_inv(args, cell),
|
||||||
|
Function::ChisqDist => self.fn_chisq_dist(args, cell),
|
||||||
|
Function::ChisqDistRT => self.fn_chisq_dist_rt(args, cell),
|
||||||
|
Function::ChisqInv => self.fn_chisq_inv(args, cell),
|
||||||
|
Function::ChisqInvRT => self.fn_chisq_inv_rt(args, cell),
|
||||||
|
Function::ChisqTest => self.fn_chisq_test(args, cell),
|
||||||
|
Function::ConfidenceNorm => self.fn_confidence_norm(args, cell),
|
||||||
|
Function::ConfidenceT => self.fn_confidence_t(args, cell),
|
||||||
|
Function::CovarianceP => self.fn_covariance_p(args, cell),
|
||||||
|
Function::CovarianceS => self.fn_covariance_s(args, cell),
|
||||||
|
Function::Devsq => self.fn_devsq(args, cell),
|
||||||
|
Function::ExponDist => self.fn_expon_dist(args, cell),
|
||||||
|
Function::FDist => self.fn_f_dist(args, cell),
|
||||||
|
Function::FDistRT => self.fn_f_dist_rt(args, cell),
|
||||||
|
Function::FInv => self.fn_f_inv(args, cell),
|
||||||
|
Function::FInvRT => self.fn_f_inv_rt(args, cell),
|
||||||
|
Function::Fisher => self.fn_fisher(args, cell),
|
||||||
|
Function::FisherInv => self.fn_fisher_inv(args, cell),
|
||||||
|
Function::FTest => self.fn_f_test(args, cell),
|
||||||
|
Function::Gamma => self.fn_gamma(args, cell),
|
||||||
|
Function::GammaDist => self.fn_gamma_dist(args, cell),
|
||||||
|
Function::GammaInv => self.fn_gamma_inv(args, cell),
|
||||||
|
Function::GammaLn => self.fn_gamma_ln(args, cell),
|
||||||
|
Function::GammaLnPrecise => self.fn_gamma_ln_precise(args, cell),
|
||||||
|
Function::HypGeomDist => self.fn_hyp_geom_dist(args, cell),
|
||||||
|
Function::LogNormDist => self.fn_log_norm_dist(args, cell),
|
||||||
|
Function::LogNormInv => self.fn_log_norm_inv(args, cell),
|
||||||
|
Function::NegbinomDist => self.fn_negbinom_dist(args, cell),
|
||||||
|
Function::NormDist => self.fn_norm_dist(args, cell),
|
||||||
|
Function::NormInv => self.fn_norm_inv(args, cell),
|
||||||
|
Function::NormSdist => self.fn_norm_s_dist(args, cell),
|
||||||
|
Function::NormSInv => self.fn_norm_s_inv(args, cell),
|
||||||
|
Function::Pearson => self.fn_pearson(args, cell),
|
||||||
|
Function::Phi => self.fn_phi(args, cell),
|
||||||
|
Function::PoissonDist => self.fn_poisson_dist(args, cell),
|
||||||
|
Function::Standardize => self.fn_standardize(args, cell),
|
||||||
|
Function::StDevP => self.fn_stdev_p(args, cell),
|
||||||
|
Function::StDevS => self.fn_stdev_s(args, cell),
|
||||||
|
Function::Stdeva => self.fn_stdeva(args, cell),
|
||||||
|
Function::Stdevpa => self.fn_stdevpa(args, cell),
|
||||||
|
Function::TDist => self.fn_t_dist(args, cell),
|
||||||
|
Function::TDist2T => self.fn_t_dist_2t(args, cell),
|
||||||
|
Function::TDistRT => self.fn_t_dist_rt(args, cell),
|
||||||
|
Function::TInv => self.fn_t_inv(args, cell),
|
||||||
|
Function::TInv2T => self.fn_t_inv_2t(args, cell),
|
||||||
|
Function::TTest => self.fn_t_test(args, cell),
|
||||||
|
Function::VarP => self.fn_var_p(args, cell),
|
||||||
|
Function::VarS => self.fn_var_s(args, cell),
|
||||||
|
Function::VarpA => self.fn_varpa(args, cell),
|
||||||
|
Function::VarA => self.fn_vara(args, cell),
|
||||||
|
Function::WeibullDist => self.fn_weibull_dist(args, cell),
|
||||||
|
Function::ZTest => self.fn_z_test(args, cell),
|
||||||
|
Function::Sumx2my2 => self.fn_sumx2my2(args, cell),
|
||||||
|
Function::Sumx2py2 => self.fn_sumx2py2(args, cell),
|
||||||
|
Function::Sumxmy2 => self.fn_sumxmy2(args, cell),
|
||||||
|
Function::Correl => self.fn_correl(args, cell),
|
||||||
|
Function::Rsq => self.fn_rsq(args, cell),
|
||||||
|
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,733 +0,0 @@
|
|||||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
|
||||||
use crate::expressions::types::CellReferenceIndex;
|
|
||||||
use crate::{
|
|
||||||
calc_result::{CalcResult, Range},
|
|
||||||
expressions::parser::Node,
|
|
||||||
expressions::token::Error,
|
|
||||||
model::Model,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::util::build_criteria;
|
|
||||||
|
|
||||||
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;
|
|
||||||
for arg in args {
|
|
||||||
match self.evaluate_node_in_context(arg, cell) {
|
|
||||||
CalcResult::Number(value) => {
|
|
||||||
count += 1.0;
|
|
||||||
sum += value;
|
|
||||||
}
|
|
||||||
CalcResult::Boolean(b) => {
|
|
||||||
if let Node::ReferenceKind { .. } = arg {
|
|
||||||
} else {
|
|
||||||
sum += if b { 1.0 } else { 0.0 };
|
|
||||||
count += 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CalcResult::Range { left, right } => {
|
|
||||||
if left.sheet != right.sheet {
|
|
||||||
return 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) {
|
|
||||||
match self.evaluate_cell(CellReferenceIndex {
|
|
||||||
sheet: left.sheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
}) {
|
|
||||||
CalcResult::Number(value) => {
|
|
||||||
count += 1.0;
|
|
||||||
sum += value;
|
|
||||||
}
|
|
||||||
error @ CalcResult::Error { .. } => return error,
|
|
||||||
CalcResult::Range { .. } => {
|
|
||||||
return 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::<f64>() {
|
|
||||||
sum += t;
|
|
||||||
count += 1.0;
|
|
||||||
} else {
|
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::VALUE,
|
|
||||||
origin: cell,
|
|
||||||
message: "Argument cannot be cast into number".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Ignore everything else
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if count == 0.0 {
|
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::DIV,
|
|
||||||
origin: cell,
|
|
||||||
message: "Division by Zero".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
let mut count = 0.0;
|
|
||||||
let mut sum = 0.0;
|
|
||||||
for arg in args {
|
|
||||||
match self.evaluate_node_in_context(arg, cell) {
|
|
||||||
CalcResult::Range { left, right } => {
|
|
||||||
if left.sheet != right.sheet {
|
|
||||||
return 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) {
|
|
||||||
match self.evaluate_cell(CellReferenceIndex {
|
|
||||||
sheet: left.sheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
}) {
|
|
||||||
CalcResult::String(_) => count += 1.0,
|
|
||||||
CalcResult::Number(value) => {
|
|
||||||
count += 1.0;
|
|
||||||
sum += value;
|
|
||||||
}
|
|
||||||
CalcResult::Boolean(b) => {
|
|
||||||
if b {
|
|
||||||
sum += 1.0;
|
|
||||||
}
|
|
||||||
count += 1.0;
|
|
||||||
}
|
|
||||||
error @ CalcResult::Error { .. } => return error,
|
|
||||||
CalcResult::Range { .. } => {
|
|
||||||
return CalcResult::new_error(
|
|
||||||
Error::ERROR,
|
|
||||||
cell,
|
|
||||||
"Unexpected Range".to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
|
||||||
CalcResult::Array(_) => {
|
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::NIMPL,
|
|
||||||
origin: cell,
|
|
||||||
message: "Arrays not supported yet".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CalcResult::Number(value) => {
|
|
||||||
count += 1.0;
|
|
||||||
sum += value;
|
|
||||||
}
|
|
||||||
CalcResult::String(s) => {
|
|
||||||
if let Node::ReferenceKind { .. } = arg {
|
|
||||||
// Do nothing
|
|
||||||
count += 1.0;
|
|
||||||
} else if let Ok(t) = s.parse::<f64>() {
|
|
||||||
sum += t;
|
|
||||||
count += 1.0;
|
|
||||||
} else {
|
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::VALUE,
|
|
||||||
origin: cell,
|
|
||||||
message: "Argument cannot be cast into number".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CalcResult::Boolean(b) => {
|
|
||||||
count += 1.0;
|
|
||||||
if b {
|
|
||||||
sum += 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
error @ CalcResult::Error { .. } => return error,
|
|
||||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
|
||||||
CalcResult::Array(_) => {
|
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::NIMPL,
|
|
||||||
origin: cell,
|
|
||||||
message: "Arrays not supported yet".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if count == 0.0 {
|
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::DIV,
|
|
||||||
origin: cell,
|
|
||||||
message: "Division by Zero".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
CalcResult::Number(sum / count)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn fn_count(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
||||||
if args.is_empty() {
|
|
||||||
return CalcResult::new_args_number_error(cell);
|
|
||||||
}
|
|
||||||
let mut result = 0.0;
|
|
||||||
for arg in args {
|
|
||||||
match self.evaluate_node_in_context(arg, cell) {
|
|
||||||
CalcResult::Number(_) => {
|
|
||||||
result += 1.0;
|
|
||||||
}
|
|
||||||
CalcResult::Boolean(_) => {
|
|
||||||
if !matches!(arg, Node::ReferenceKind { .. }) {
|
|
||||||
result += 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CalcResult::String(s) => {
|
|
||||||
if !matches!(arg, Node::ReferenceKind { .. }) && s.parse::<f64>().is_ok() {
|
|
||||||
result += 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CalcResult::Range { left, right } => {
|
|
||||||
if left.sheet != right.sheet {
|
|
||||||
return 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) {
|
|
||||||
if let CalcResult::Number(_) = self.evaluate_cell(CellReferenceIndex {
|
|
||||||
sheet: left.sheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
}) {
|
|
||||||
result += 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Ignore everything else
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
CalcResult::Number(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn fn_counta(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
||||||
if args.is_empty() {
|
|
||||||
return CalcResult::new_args_number_error(cell);
|
|
||||||
}
|
|
||||||
let mut result = 0.0;
|
|
||||||
for arg in args {
|
|
||||||
match self.evaluate_node_in_context(arg, cell) {
|
|
||||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
|
||||||
CalcResult::Range { left, right } => {
|
|
||||||
if left.sheet != right.sheet {
|
|
||||||
return 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) {
|
|
||||||
match self.evaluate_cell(CellReferenceIndex {
|
|
||||||
sheet: left.sheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
}) {
|
|
||||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
|
||||||
_ => {
|
|
||||||
result += 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
result += 1.0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
CalcResult::Number(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn fn_countblank(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
||||||
// COUNTBLANK requires only one argument
|
|
||||||
if args.len() != 1 {
|
|
||||||
return CalcResult::new_args_number_error(cell);
|
|
||||||
}
|
|
||||||
let mut result = 0.0;
|
|
||||||
for arg in args {
|
|
||||||
match self.evaluate_node_in_context(arg, cell) {
|
|
||||||
CalcResult::EmptyCell | CalcResult::EmptyArg => result += 1.0,
|
|
||||||
CalcResult::String(s) => {
|
|
||||||
if s.is_empty() {
|
|
||||||
result += 1.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CalcResult::Range { left, right } => {
|
|
||||||
if left.sheet != right.sheet {
|
|
||||||
return 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) {
|
|
||||||
match self.evaluate_cell(CellReferenceIndex {
|
|
||||||
sheet: left.sheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
}) {
|
|
||||||
CalcResult::EmptyCell | CalcResult::EmptyArg => result += 1.0,
|
|
||||||
CalcResult::String(s) => {
|
|
||||||
if s.is_empty() {
|
|
||||||
result += 1.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
CalcResult::Number(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn fn_countif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
||||||
if args.len() == 2 {
|
|
||||||
let arguments = vec![args[0].clone(), args[1].clone()];
|
|
||||||
self.fn_countifs(&arguments, cell)
|
|
||||||
} else {
|
|
||||||
CalcResult::new_args_number_error(cell)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// AVERAGEIF(criteria_range, criteria, [average_range])
|
|
||||||
/// if average_rage is missing then criteria_range will be used
|
|
||||||
pub(crate) fn fn_averageif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
||||||
if args.len() == 2 {
|
|
||||||
let arguments = vec![args[0].clone(), args[0].clone(), args[1].clone()];
|
|
||||||
self.fn_averageifs(&arguments, cell)
|
|
||||||
} else if args.len() == 3 {
|
|
||||||
let arguments = vec![args[2].clone(), args[0].clone(), args[1].clone()];
|
|
||||||
self.fn_averageifs(&arguments, cell)
|
|
||||||
} else {
|
|
||||||
CalcResult::new_args_number_error(cell)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: This function shares a lot of code with apply_ifs. Can we merge them?
|
|
||||||
pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
||||||
let args_count = args.len();
|
|
||||||
if args_count < 2 || !args_count.is_multiple_of(2) {
|
|
||||||
return CalcResult::new_args_number_error(cell);
|
|
||||||
}
|
|
||||||
|
|
||||||
let case_count = args_count / 2;
|
|
||||||
// NB: this is a beautiful example of the borrow checker
|
|
||||||
// The order of these two definitions cannot be swapped.
|
|
||||||
let mut criteria = Vec::new();
|
|
||||||
let mut fn_criteria = Vec::new();
|
|
||||||
let ranges = &mut Vec::new();
|
|
||||||
for case_index in 0..case_count {
|
|
||||||
let criterion = self.evaluate_node_in_context(&args[case_index * 2 + 1], cell);
|
|
||||||
criteria.push(criterion);
|
|
||||||
// NB: We cannot do:
|
|
||||||
// fn_criteria.push(build_criteria(&criterion));
|
|
||||||
// because criterion doesn't live long enough
|
|
||||||
let result = self.evaluate_node_in_context(&args[case_index * 2], cell);
|
|
||||||
if result.is_error() {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if let CalcResult::Range { left, right } = result {
|
|
||||||
if left.sheet != right.sheet {
|
|
||||||
return CalcResult::new_error(
|
|
||||||
Error::VALUE,
|
|
||||||
cell,
|
|
||||||
"Ranges are in different sheets".to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// TODO test ranges are of the same size as sum_range
|
|
||||||
ranges.push(Range { left, right });
|
|
||||||
} else {
|
|
||||||
return CalcResult::new_error(Error::VALUE, cell, "Expected a range".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for criterion in criteria.iter() {
|
|
||||||
fn_criteria.push(build_criteria(criterion));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut total = 0.0;
|
|
||||||
let first_range = &ranges[0];
|
|
||||||
let left_row = first_range.left.row;
|
|
||||||
let left_column = first_range.left.column;
|
|
||||||
let right_row = first_range.right.row;
|
|
||||||
let right_column = first_range.right.column;
|
|
||||||
|
|
||||||
let dimension = match self.workbook.worksheet(first_range.left.sheet) {
|
|
||||||
Ok(s) => s.dimension(),
|
|
||||||
Err(_) => {
|
|
||||||
return CalcResult::new_error(
|
|
||||||
Error::ERROR,
|
|
||||||
cell,
|
|
||||||
format!("Invalid worksheet index: '{}'", first_range.left.sheet),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let max_row = dimension.max_row;
|
|
||||||
let max_column = dimension.max_column;
|
|
||||||
|
|
||||||
let open_row = left_row == 1 && right_row == LAST_ROW;
|
|
||||||
let open_column = left_column == 1 && right_column == LAST_COLUMN;
|
|
||||||
|
|
||||||
for row in left_row..right_row + 1 {
|
|
||||||
if open_row && row > max_row {
|
|
||||||
// If the row is larger than the max row in the sheet then all cells are empty.
|
|
||||||
// We compute it only once
|
|
||||||
let mut is_true = true;
|
|
||||||
for fn_criterion in fn_criteria.iter() {
|
|
||||||
if !fn_criterion(&CalcResult::EmptyCell) {
|
|
||||||
is_true = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if is_true {
|
|
||||||
total += ((LAST_ROW - max_row) * (right_column - left_column + 1)) as f64;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
for column in left_column..right_column + 1 {
|
|
||||||
if open_column && column > max_column {
|
|
||||||
// If the column is larger than the max column in the sheet then all cells are empty.
|
|
||||||
// We compute it only once
|
|
||||||
let mut is_true = true;
|
|
||||||
for fn_criterion in fn_criteria.iter() {
|
|
||||||
if !fn_criterion(&CalcResult::EmptyCell) {
|
|
||||||
is_true = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if is_true {
|
|
||||||
total += (LAST_COLUMN - max_column) as f64;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let mut is_true = true;
|
|
||||||
for case_index in 0..case_count {
|
|
||||||
// We check if value in range n meets criterion n
|
|
||||||
let range = &ranges[case_index];
|
|
||||||
let fn_criterion = &fn_criteria[case_index];
|
|
||||||
let value = self.evaluate_cell(CellReferenceIndex {
|
|
||||||
sheet: range.left.sheet,
|
|
||||||
row: range.left.row + row - first_range.left.row,
|
|
||||||
column: range.left.column + column - first_range.left.column,
|
|
||||||
});
|
|
||||||
if !fn_criterion(&value) {
|
|
||||||
is_true = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if is_true {
|
|
||||||
total += 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CalcResult::Number(total)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn apply_ifs<F>(
|
|
||||||
&mut self,
|
|
||||||
args: &[Node],
|
|
||||||
cell: CellReferenceIndex,
|
|
||||||
mut apply: F,
|
|
||||||
) -> Result<(), CalcResult>
|
|
||||||
where
|
|
||||||
F: FnMut(f64),
|
|
||||||
{
|
|
||||||
let args_count = args.len();
|
|
||||||
if args_count < 3 || args_count.is_multiple_of(2) {
|
|
||||||
return Err(CalcResult::new_args_number_error(cell));
|
|
||||||
}
|
|
||||||
let arg_0 = self.evaluate_node_in_context(&args[0], cell);
|
|
||||||
if arg_0.is_error() {
|
|
||||||
return Err(arg_0);
|
|
||||||
}
|
|
||||||
let sum_range = if let CalcResult::Range { left, right } = arg_0 {
|
|
||||||
if left.sheet != right.sheet {
|
|
||||||
return Err(CalcResult::new_error(
|
|
||||||
Error::VALUE,
|
|
||||||
cell,
|
|
||||||
"Ranges are in different sheets".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Range { left, right }
|
|
||||||
} else {
|
|
||||||
return Err(CalcResult::new_error(
|
|
||||||
Error::VALUE,
|
|
||||||
cell,
|
|
||||||
"Expected a range".to_string(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
let case_count = (args_count - 1) / 2;
|
|
||||||
// NB: this is a beautiful example of the borrow checker
|
|
||||||
// The order of these two definitions cannot be swapped.
|
|
||||||
let mut criteria = Vec::new();
|
|
||||||
let mut fn_criteria = Vec::new();
|
|
||||||
let ranges = &mut Vec::new();
|
|
||||||
for case_index in 1..=case_count {
|
|
||||||
let criterion = self.evaluate_node_in_context(&args[case_index * 2], cell);
|
|
||||||
// NB: criterion might be an error. That's ok
|
|
||||||
criteria.push(criterion);
|
|
||||||
// NB: We cannot do:
|
|
||||||
// fn_criteria.push(build_criteria(&criterion));
|
|
||||||
// because criterion doesn't live long enough
|
|
||||||
let result = self.evaluate_node_in_context(&args[case_index * 2 - 1], cell);
|
|
||||||
if result.is_error() {
|
|
||||||
return Err(result);
|
|
||||||
}
|
|
||||||
if let CalcResult::Range { left, right } = result {
|
|
||||||
if left.sheet != right.sheet {
|
|
||||||
return Err(CalcResult::new_error(
|
|
||||||
Error::VALUE,
|
|
||||||
cell,
|
|
||||||
"Ranges are in different sheets".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
// TODO test ranges are of the same size as sum_range
|
|
||||||
ranges.push(Range { left, right });
|
|
||||||
} else {
|
|
||||||
return Err(CalcResult::new_error(
|
|
||||||
Error::VALUE,
|
|
||||||
cell,
|
|
||||||
"Expected a range".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for criterion in criteria.iter() {
|
|
||||||
fn_criteria.push(build_criteria(criterion));
|
|
||||||
}
|
|
||||||
|
|
||||||
let left_row = sum_range.left.row;
|
|
||||||
let left_column = sum_range.left.column;
|
|
||||||
let mut right_row = sum_range.right.row;
|
|
||||||
let mut right_column = sum_range.right.column;
|
|
||||||
|
|
||||||
if left_row == 1 && right_row == LAST_ROW {
|
|
||||||
right_row = match self.workbook.worksheet(sum_range.left.sheet) {
|
|
||||||
Ok(s) => s.dimension().max_row,
|
|
||||||
Err(_) => {
|
|
||||||
return Err(CalcResult::new_error(
|
|
||||||
Error::ERROR,
|
|
||||||
cell,
|
|
||||||
format!("Invalid worksheet index: '{}'", sum_range.left.sheet),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if left_column == 1 && right_column == LAST_COLUMN {
|
|
||||||
right_column = match self.workbook.worksheet(sum_range.left.sheet) {
|
|
||||||
Ok(s) => s.dimension().max_column,
|
|
||||||
Err(_) => {
|
|
||||||
return Err(CalcResult::new_error(
|
|
||||||
Error::ERROR,
|
|
||||||
cell,
|
|
||||||
format!("Invalid worksheet index: '{}'", sum_range.left.sheet),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
for row in left_row..right_row + 1 {
|
|
||||||
for column in left_column..right_column + 1 {
|
|
||||||
let mut is_true = true;
|
|
||||||
for case_index in 0..case_count {
|
|
||||||
// We check if value in range n meets criterion n
|
|
||||||
let range = &ranges[case_index];
|
|
||||||
let fn_criterion = &fn_criteria[case_index];
|
|
||||||
let value = self.evaluate_cell(CellReferenceIndex {
|
|
||||||
sheet: range.left.sheet,
|
|
||||||
row: range.left.row + row - sum_range.left.row,
|
|
||||||
column: range.left.column + column - sum_range.left.column,
|
|
||||||
});
|
|
||||||
if !fn_criterion(&value) {
|
|
||||||
is_true = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if is_true {
|
|
||||||
let v = self.evaluate_cell(CellReferenceIndex {
|
|
||||||
sheet: sum_range.left.sheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
});
|
|
||||||
match v {
|
|
||||||
CalcResult::Number(n) => apply(n),
|
|
||||||
CalcResult::Error { .. } => return Err(v),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn fn_averageifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
||||||
let mut total = 0.0;
|
|
||||||
let mut count = 0.0;
|
|
||||||
|
|
||||||
let average = |value: f64| {
|
|
||||||
total += value;
|
|
||||||
count += 1.0;
|
|
||||||
};
|
|
||||||
if let Err(e) = self.apply_ifs(args, cell, average) {
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if count == 0.0 {
|
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::DIV,
|
|
||||||
origin: cell,
|
|
||||||
message: "division by 0".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
CalcResult::Number(total / count)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn fn_minifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
||||||
let mut min = f64::INFINITY;
|
|
||||||
let apply_min = |value: f64| min = value.min(min);
|
|
||||||
if let Err(e) = self.apply_ifs(args, cell, apply_min) {
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if min.is_infinite() {
|
|
||||||
min = 0.0;
|
|
||||||
}
|
|
||||||
CalcResult::Number(min)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn fn_maxifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
||||||
let mut max = -f64::INFINITY;
|
|
||||||
let apply_max = |value: f64| max = value.max(max);
|
|
||||||
if let Err(e) = self.apply_ifs(args, cell, apply_max) {
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
if max.is_infinite() {
|
|
||||||
max = 0.0;
|
|
||||||
}
|
|
||||||
CalcResult::Number(max)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn fn_geomean(&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 product = 1.0;
|
|
||||||
for arg in args {
|
|
||||||
match self.evaluate_node_in_context(arg, cell) {
|
|
||||||
CalcResult::Number(value) => {
|
|
||||||
count += 1.0;
|
|
||||||
product *= value;
|
|
||||||
}
|
|
||||||
CalcResult::Boolean(b) => {
|
|
||||||
if let Node::ReferenceKind { .. } = arg {
|
|
||||||
} else {
|
|
||||||
product *= if b { 1.0 } else { 0.0 };
|
|
||||||
count += 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CalcResult::Range { left, right } => {
|
|
||||||
if left.sheet != right.sheet {
|
|
||||||
return 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) {
|
|
||||||
match self.evaluate_cell(CellReferenceIndex {
|
|
||||||
sheet: left.sheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
}) {
|
|
||||||
CalcResult::Number(value) => {
|
|
||||||
count += 1.0;
|
|
||||||
product *= value;
|
|
||||||
}
|
|
||||||
error @ CalcResult::Error { .. } => return error,
|
|
||||||
CalcResult::Range { .. } => {
|
|
||||||
return 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::<f64>() {
|
|
||||||
product *= t;
|
|
||||||
count += 1.0;
|
|
||||||
} else {
|
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::VALUE,
|
|
||||||
origin: cell,
|
|
||||||
message: "Argument cannot be cast into number".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Ignore everything else
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if count == 0.0 {
|
|
||||||
return CalcResult::Error {
|
|
||||||
error: Error::DIV,
|
|
||||||
origin: cell,
|
|
||||||
message: "Division by Zero".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
CalcResult::Number(product.powf(1.0 / count))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
213
base/src/functions/statistical/beta.rs
Normal file
213
base/src/functions/statistical/beta.rs
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
use statrs::distribution::{Beta, Continuous, ContinuousCDF};
|
||||||
|
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// BETA.DIST(x, alpha, beta, cumulative, [A], [B])
|
||||||
|
pub(crate) fn fn_beta_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let arg_count = args.len();
|
||||||
|
if !(4..=6).contains(&arg_count) {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let alpha = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let beta_param = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// cumulative argument: interpret like Excel
|
||||||
|
let cumulative = match self.evaluate_node_in_context(&args[3], cell) {
|
||||||
|
CalcResult::Boolean(b) => b,
|
||||||
|
CalcResult::Number(n) => n != 0.0,
|
||||||
|
CalcResult::String(s) => {
|
||||||
|
let up = s.to_ascii_uppercase();
|
||||||
|
if up == "TRUE" {
|
||||||
|
true
|
||||||
|
} else if up == "FALSE" {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "cumulative must be TRUE/FALSE or numeric".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid cumulative argument".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optional A, B
|
||||||
|
let a = if arg_count >= 5 {
|
||||||
|
match self.get_number_no_bools(&args[4], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let b = if arg_count >= 6 {
|
||||||
|
match self.get_number_no_bools(&args[5], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Excel: alpha <= 0 or beta <= 0 → #NUM!
|
||||||
|
if alpha <= 0.0 || beta_param <= 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"alpha and beta must be > 0 in BETA.DIST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Excel: if x < A, x > B, or A = B → #NUM!
|
||||||
|
if b == a || x < a || x > b {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"x must be between A and B and A < B in BETA.DIST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform to standard Beta(0,1)
|
||||||
|
let width = b - a;
|
||||||
|
let t = (x - a) / width;
|
||||||
|
|
||||||
|
let dist = match Beta::new(alpha, beta_param) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for Beta distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = if cumulative {
|
||||||
|
dist.cdf(t)
|
||||||
|
} else {
|
||||||
|
// general-interval beta pdf: f_X(x) = f_T(t) / (B - A), t=(x-A)/(B-A)
|
||||||
|
dist.pdf(t) / width
|
||||||
|
};
|
||||||
|
|
||||||
|
if result.is_nan() || result.is_infinite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid result for BETA.DIST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_beta_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let arg_count = args.len();
|
||||||
|
if !(3..=5).contains(&arg_count) {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let alpha = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let beta_param = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let a = if arg_count >= 4 {
|
||||||
|
match self.get_number_no_bools(&args[3], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let b = if arg_count >= 5 {
|
||||||
|
match self.get_number_no_bools(&args[4], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
if alpha <= 0.0 || beta_param <= 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"alpha and beta must be > 0 in BETA.INV".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// probability <= 0 or probability > 1 → #NUM!
|
||||||
|
if p <= 0.0 || p > 1.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"probability must be in (0,1] in BETA.INV".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if b <= a {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"A must be < B in BETA.INV".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match Beta::new(alpha, beta_param) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for Beta distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let t = dist.inverse_cdf(p);
|
||||||
|
if t.is_nan() || t.is_infinite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid result for BETA.INV".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map back from [0,1] to [A,B]
|
||||||
|
let x = a + t * (b - a);
|
||||||
|
CalcResult::Number(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
311
base/src/functions/statistical/binom.rs
Normal file
311
base/src/functions/statistical/binom.rs
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
use statrs::distribution::{Binomial, Discrete, DiscreteCDF};
|
||||||
|
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub(crate) fn fn_binom_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 4 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// number_s
|
||||||
|
let number_s = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// trials
|
||||||
|
let trials = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// probability_s
|
||||||
|
let p = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// cumulative (logical)
|
||||||
|
let cumulative = match self.get_boolean(&args[3], cell) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Domain checks
|
||||||
|
if trials < 0.0
|
||||||
|
|| number_s < 0.0
|
||||||
|
|| number_s > trials
|
||||||
|
|| p.is_nan()
|
||||||
|
|| !(0.0..=1.0).contains(&p)
|
||||||
|
{
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for BINOM.DIST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit to u64
|
||||||
|
if trials > u64::MAX as f64 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Number of trials too large".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = trials as u64;
|
||||||
|
let k = number_s as u64;
|
||||||
|
|
||||||
|
let dist = match Binomial::new(p, n) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for binomial distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let prob = if cumulative { dist.cdf(k) } else { dist.pmf(k) };
|
||||||
|
|
||||||
|
if prob.is_nan() || prob.is_infinite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid result for BINOM.DIST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(prob)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_binom_dist_range(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> CalcResult {
|
||||||
|
if args.len() < 3 || args.len() > 4 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// trials
|
||||||
|
let trials = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// probability_s
|
||||||
|
let p = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// number_s (lower)
|
||||||
|
let number_s = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// number_s2 (upper, optional)
|
||||||
|
let number_s2 = if args.len() == 4 {
|
||||||
|
match self.get_number_no_bools(&args[3], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
number_s
|
||||||
|
};
|
||||||
|
|
||||||
|
if trials < 0.0
|
||||||
|
|| number_s < 0.0
|
||||||
|
|| number_s2 < 0.0
|
||||||
|
|| number_s > number_s2
|
||||||
|
|| number_s2 > trials
|
||||||
|
|| p.is_nan()
|
||||||
|
|| !(0.0..=1.0).contains(&p)
|
||||||
|
{
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for BINOM.DIST.RANGE".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if trials > u64::MAX as f64 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Number of trials too large".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = trials as u64;
|
||||||
|
let lower = number_s as u64;
|
||||||
|
let upper = number_s2 as u64;
|
||||||
|
|
||||||
|
let dist = match Binomial::new(p, n) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for binomial distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let prob = if lower == 0 {
|
||||||
|
dist.cdf(upper)
|
||||||
|
} else {
|
||||||
|
let cdf_upper = dist.cdf(upper);
|
||||||
|
let cdf_below_lower = dist.cdf(lower - 1);
|
||||||
|
cdf_upper - cdf_below_lower
|
||||||
|
};
|
||||||
|
|
||||||
|
if prob.is_nan() || prob.is_infinite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid result for BINOM.DIST.RANGE".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(prob)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_binom_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// trials
|
||||||
|
let trials = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// probability_s
|
||||||
|
let p = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// alpha
|
||||||
|
let alpha = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if trials < 0.0
|
||||||
|
|| trials > u64::MAX as f64
|
||||||
|
|| p.is_nan()
|
||||||
|
|| !(0.0..=1.0).contains(&p)
|
||||||
|
|| alpha.is_nan()
|
||||||
|
|| !(0.0..=1.0).contains(&alpha)
|
||||||
|
{
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for BINOM.INV".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = trials as u64;
|
||||||
|
|
||||||
|
let dist = match Binomial::new(p, n) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for binomial distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DiscreteCDF::inverse_cdf returns u64 for binomial
|
||||||
|
let k = statrs::distribution::DiscreteCDF::inverse_cdf(&dist, alpha);
|
||||||
|
|
||||||
|
CalcResult::Number(k as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_negbinom_dist(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> CalcResult {
|
||||||
|
use statrs::distribution::{Discrete, DiscreteCDF, NegativeBinomial};
|
||||||
|
|
||||||
|
if args.len() != 4 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let number_f = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let number_s = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let probability_s = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let cumulative = match self.get_boolean(&args[3], cell) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if number_f < 0.0 || number_s < 1.0 || !(0.0..=1.0).contains(&probability_s) {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameter for NEGBINOM.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard against absurdly large failures that won't fit in u64
|
||||||
|
if number_f > (u64::MAX as f64) {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameter for NEGBINOM.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match NegativeBinomial::new(number_s, probability_s) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameter for NEGBINOM.DIST".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let f_u = number_f as u64;
|
||||||
|
let result = if cumulative {
|
||||||
|
dist.cdf(f_u)
|
||||||
|
} else {
|
||||||
|
dist.pmf(f_u)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !result.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameter for NEGBINOM.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
397
base/src/functions/statistical/chisq.rs
Normal file
397
base/src/functions/statistical/chisq.rs
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
use statrs::distribution::{ChiSquared, Continuous, ContinuousCDF};
|
||||||
|
|
||||||
|
use crate::expressions::parser::ArrayNode;
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// CHISQ.DIST(x, deg_freedom, cumulative)
|
||||||
|
pub(crate) fn fn_chisq_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let df = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cumulative = match self.get_boolean(&args[2], cell) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if x < 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"x must be >= 0 in CHISQ.DIST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if df < 1.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"degrees of freedom must be >= 1 in CHISQ.DIST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match ChiSquared::new(df) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for Chi-squared distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
|
||||||
|
|
||||||
|
if result.is_nan() || result.is_infinite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid result for CHISQ.DIST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHISQ.DIST.RT(x, deg_freedom)
|
||||||
|
pub(crate) fn fn_chisq_dist_rt(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> CalcResult {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let df_raw = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let df = df_raw.trunc();
|
||||||
|
|
||||||
|
if x < 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"x must be >= 0 in CHISQ.DIST.RT".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if df < 1.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"degrees of freedom must be >= 1 in CHISQ.DIST.RT".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match ChiSquared::new(df) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for Chi-squared distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Right-tail probability: P(X > x).
|
||||||
|
// Use sf(x) directly for better numerical properties than 1 - cdf(x).
|
||||||
|
let result = dist.sf(x);
|
||||||
|
|
||||||
|
if result.is_nan() || result.is_infinite() || result < 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid result for CHISQ.DIST.RT".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHISQ.INV(probability, deg_freedom)
|
||||||
|
pub(crate) fn fn_chisq_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let df = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// if probability < 0 or > 1 → #NUM!
|
||||||
|
if !(0.0..=1.0).contains(&p) {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"probability must be in [0,1] in CHISQ.INV".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if df < 1.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"degrees of freedom must be >= 1 in CHISQ.INV".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match ChiSquared::new(df) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for Chi-squared distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let x = dist.inverse_cdf(p);
|
||||||
|
|
||||||
|
if x.is_nan() || x.is_infinite() || x < 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid result for CHISQ.INV".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHISQ.INV.RT(probability, deg_freedom)
|
||||||
|
pub(crate) fn fn_chisq_inv_rt(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> CalcResult {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let df_raw = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let df = df_raw.trunc();
|
||||||
|
|
||||||
|
// if probability < 0 or > 1 → #NUM!
|
||||||
|
if !(0.0..=1.0).contains(&p) {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"probability must be in [0,1] in CHISQ.INV.RT".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if df < 1.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"degrees of freedom must be >= 1 in CHISQ.INV.RT".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match ChiSquared::new(df) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for Chi-squared distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Right-tail inverse: p = P(X > x) = SF(x) = 1 - CDF(x)
|
||||||
|
// So x = inverse_cdf(1 - p).
|
||||||
|
let x = dist.inverse_cdf(1.0 - p);
|
||||||
|
|
||||||
|
if x.is_nan() || x.is_infinite() || x < 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid result for CHISQ.INV.RT".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn values_from_range(
|
||||||
|
&mut self,
|
||||||
|
left: CellReferenceIndex,
|
||||||
|
right: CellReferenceIndex,
|
||||||
|
) -> Result<Vec<Option<f64>>, CalcResult> {
|
||||||
|
let mut values = Vec::new();
|
||||||
|
for row_offset in 0..=(right.row - left.row) {
|
||||||
|
for col_offset in 0..=(right.column - left.column) {
|
||||||
|
let cell_ref = CellReferenceIndex {
|
||||||
|
sheet: left.sheet,
|
||||||
|
row: left.row + row_offset,
|
||||||
|
column: left.column + col_offset,
|
||||||
|
};
|
||||||
|
let cell_value = self.evaluate_cell(cell_ref);
|
||||||
|
match cell_value {
|
||||||
|
CalcResult::Number(v) => {
|
||||||
|
values.push(Some(v));
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return Err(error),
|
||||||
|
_ => {
|
||||||
|
values.push(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn values_from_array(
|
||||||
|
&mut self,
|
||||||
|
array: Vec<Vec<ArrayNode>>,
|
||||||
|
) -> Result<Vec<Option<f64>>, Error> {
|
||||||
|
let mut values = Vec::new();
|
||||||
|
for row in array {
|
||||||
|
for item in row {
|
||||||
|
match item {
|
||||||
|
ArrayNode::Number(f) => {
|
||||||
|
values.push(Some(f));
|
||||||
|
}
|
||||||
|
ArrayNode::Error(error) => {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
values.push(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHISQ.TEST(actual_range, expected_range)
|
||||||
|
pub(crate) fn fn_chisq_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (width, height, values_left, values_right) = match self.fn_get_two_matrices(args, cell)
|
||||||
|
{
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(r) => return r,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut values = Vec::with_capacity(values_left.len());
|
||||||
|
|
||||||
|
// Now we have:
|
||||||
|
// - values: flattened (observed, expected)
|
||||||
|
// - width, height: shape
|
||||||
|
for i in 0..values_left.len() {
|
||||||
|
match (values_left[i], values_right[i]) {
|
||||||
|
(Some(v1), Some(v2)) => {
|
||||||
|
values.push((v1, v2));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
values.push((1.0, 1.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if width == 0 || height == 0 || values.len() < 2 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"CHISQ.TEST requires at least two data points".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut chi2 = 0.0;
|
||||||
|
for (obs, exp) in &values {
|
||||||
|
if *obs < 0.0 || *exp < 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Negative value in CHISQ.TEST data".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if *exp == 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Zero expected value in CHISQ.TEST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let diff = obs - exp;
|
||||||
|
chi2 += (diff * diff) / exp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if chi2 < 0.0 && chi2 > -1e-12 {
|
||||||
|
chi2 = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = width * height;
|
||||||
|
if total <= 1 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"CHISQ.TEST degrees of freedom is zero".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let df = if width > 1 && height > 1 {
|
||||||
|
(width - 1) * (height - 1)
|
||||||
|
} else {
|
||||||
|
total - 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let dist = match ChiSquared::new(df as f64) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid degrees of freedom in CHISQ.TEST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut p = 1.0 - dist.cdf(chi2);
|
||||||
|
|
||||||
|
// clamp tiny fp noise
|
||||||
|
if p < 0.0 && p > -1e-15 {
|
||||||
|
p = 0.0;
|
||||||
|
}
|
||||||
|
if p > 1.0 && p < 1.0 + 1e-15 {
|
||||||
|
p = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
227
base/src/functions/statistical/correl.rs
Normal file
227
base/src/functions/statistical/correl.rs
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// CORREL(array1, array2) - Returns the correlation coefficient of two data sets
|
||||||
|
pub(crate) fn fn_correl(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (_, _, values_left, values_right) = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut n = 0.0;
|
||||||
|
let mut sum_x = 0.0;
|
||||||
|
let mut sum_y = 0.0;
|
||||||
|
let mut sum_x2 = 0.0;
|
||||||
|
let mut sum_y2 = 0.0;
|
||||||
|
let mut sum_xy = 0.0;
|
||||||
|
|
||||||
|
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||||
|
if let (Some(x), Some(y)) = (x_opt, y_opt) {
|
||||||
|
n += 1.0;
|
||||||
|
sum_x += x;
|
||||||
|
sum_y += y;
|
||||||
|
sum_x2 += x * x;
|
||||||
|
sum_y2 += y * y;
|
||||||
|
sum_xy += x * y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need at least 2 valid pairs
|
||||||
|
if n < 2.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"CORREL requires at least two numeric data points in each range".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let num = n * sum_xy - sum_x * sum_y;
|
||||||
|
let denom_x = n * sum_x2 - sum_x * sum_x;
|
||||||
|
let denom_y = n * sum_y2 - sum_y * sum_y;
|
||||||
|
let denom = (denom_x * denom_y).sqrt();
|
||||||
|
|
||||||
|
if denom == 0.0 || !denom.is_finite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Division by zero in CORREL".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = num / denom;
|
||||||
|
CalcResult::Number(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SLOPE(known_y's, known_x's) - Returns the slope of the linear regression line
|
||||||
|
pub(crate) fn fn_slope(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut n = 0.0;
|
||||||
|
let mut sum_x = 0.0;
|
||||||
|
let mut sum_y = 0.0;
|
||||||
|
let mut sum_x2 = 0.0;
|
||||||
|
let mut sum_xy = 0.0;
|
||||||
|
|
||||||
|
let len = values_y.len().min(values_x.len());
|
||||||
|
for i in 0..len {
|
||||||
|
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||||
|
n += 1.0;
|
||||||
|
sum_x += x;
|
||||||
|
sum_y += y;
|
||||||
|
sum_x2 += x * x;
|
||||||
|
sum_xy += x * y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 2.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"SLOPE requires at least two numeric data points".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let denom = n * sum_x2 - sum_x * sum_x;
|
||||||
|
if denom == 0.0 || !denom.is_finite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Division by zero in SLOPE".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let num = n * sum_xy - sum_x * sum_y;
|
||||||
|
let slope = num / denom;
|
||||||
|
|
||||||
|
CalcResult::Number(slope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// INTERCEPT(known_y's, known_x's) - Returns the y-intercept of the linear regression line
|
||||||
|
pub(crate) fn fn_intercept(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut n = 0.0;
|
||||||
|
let mut sum_x = 0.0;
|
||||||
|
let mut sum_y = 0.0;
|
||||||
|
let mut sum_x2 = 0.0;
|
||||||
|
let mut sum_xy = 0.0;
|
||||||
|
|
||||||
|
let len = values_y.len().min(values_x.len());
|
||||||
|
for i in 0..len {
|
||||||
|
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||||
|
n += 1.0;
|
||||||
|
sum_x += x;
|
||||||
|
sum_y += y;
|
||||||
|
sum_x2 += x * x;
|
||||||
|
sum_xy += x * y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 2.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"INTERCEPT requires at least two numeric data points".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let denom = n * sum_x2 - sum_x * sum_x;
|
||||||
|
if denom == 0.0 || !denom.is_finite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Division by zero in INTERCEPT".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let num = n * sum_xy - sum_x * sum_y;
|
||||||
|
let slope = num / denom;
|
||||||
|
let intercept = (sum_y - slope * sum_x) / n;
|
||||||
|
|
||||||
|
CalcResult::Number(intercept)
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEYX(known_y's, known_x's) - Returns the standard error of the predicted y-values
|
||||||
|
pub(crate) fn fn_steyx(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut n = 0.0;
|
||||||
|
let mut sum_x = 0.0;
|
||||||
|
let mut sum_y = 0.0;
|
||||||
|
let mut sum_x2 = 0.0;
|
||||||
|
let mut sum_xy = 0.0;
|
||||||
|
|
||||||
|
// We need the actual pairs again later for residuals
|
||||||
|
let mut pairs: Vec<(f64, f64)> = Vec::new();
|
||||||
|
|
||||||
|
let len = values_y.len().min(values_x.len());
|
||||||
|
for i in 0..len {
|
||||||
|
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||||
|
n += 1.0;
|
||||||
|
sum_x += x;
|
||||||
|
sum_y += y;
|
||||||
|
sum_x2 += x * x;
|
||||||
|
sum_xy += x * y;
|
||||||
|
pairs.push((x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need at least 3 points for STEYX (n - 2 in denominator)
|
||||||
|
if n < 3.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"STEYX requires at least three numeric data points".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let denom = n * sum_x2 - sum_x * sum_x;
|
||||||
|
if denom == 0.0 || !denom.is_finite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Division by zero in STEYX".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let num = n * sum_xy - sum_x * sum_y;
|
||||||
|
let slope = num / denom;
|
||||||
|
let intercept = (sum_y - slope * sum_x) / n;
|
||||||
|
|
||||||
|
// Sum of squared residuals: Σ (y - ŷ)^2, ŷ = intercept + slope * x
|
||||||
|
let mut sse = 0.0;
|
||||||
|
for (x, y) in pairs {
|
||||||
|
let y_hat = intercept + slope * x;
|
||||||
|
let diff = y - y_hat;
|
||||||
|
sse += diff * diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dof = n - 2.0;
|
||||||
|
if dof <= 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"STEYX has non-positive degrees of freedom".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sey = (sse / dof).sqrt();
|
||||||
|
if !sey.is_finite() {
|
||||||
|
return CalcResult::new_error(Error::DIV, cell, "Numerical error in STEYX".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(sey)
|
||||||
|
}
|
||||||
|
}
|
||||||
1071
base/src/functions/statistical/count_and_average.rs
Normal file
1071
base/src/functions/statistical/count_and_average.rs
Normal file
File diff suppressed because it is too large
Load Diff
264
base/src/functions/statistical/covariance.rs
Normal file
264
base/src/functions/statistical/covariance.rs
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub(crate) fn fn_covariance_p(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> CalcResult {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let values1_opts = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
|
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => return error,
|
||||||
|
},
|
||||||
|
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in first array: {:?}", error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"First argument must be a range or array".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let values2_opts = match self.evaluate_node_in_context(&args[1], cell) {
|
||||||
|
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => return error,
|
||||||
|
},
|
||||||
|
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in second array: {:?}", error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Second argument must be a range or array".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Same number of cells
|
||||||
|
if values1_opts.len() != values2_opts.len() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NA,
|
||||||
|
cell,
|
||||||
|
"COVARIANCE.P requires arrays of the same size".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count numeric data points in each array (ignoring text/booleans/empty)
|
||||||
|
let count1 = values1_opts.iter().filter(|v| v.is_some()).count();
|
||||||
|
let count2 = values2_opts.iter().filter(|v| v.is_some()).count();
|
||||||
|
|
||||||
|
if count1 == 0 || count2 == 0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"COVARIANCE.P requires at least one numeric value in each array".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if count1 != count2 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NA,
|
||||||
|
cell,
|
||||||
|
"COVARIANCE.P arrays must have the same number of numeric data points".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build paired numeric vectors, position by position
|
||||||
|
let mut xs: Vec<f64> = Vec::with_capacity(count1);
|
||||||
|
let mut ys: Vec<f64> = Vec::with_capacity(count2);
|
||||||
|
|
||||||
|
for (v1_opt, v2_opt) in values1_opts.into_iter().zip(values2_opts.into_iter()) {
|
||||||
|
if let (Some(x), Some(y)) = (v1_opt, v2_opt) {
|
||||||
|
xs.push(x);
|
||||||
|
ys.push(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = xs.len();
|
||||||
|
if n == 0 {
|
||||||
|
// Should be impossible given the checks above, but guard anyway
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"COVARIANCE.P has no paired numeric data points".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n_f = n as f64;
|
||||||
|
|
||||||
|
let mut sum_x = 0.0;
|
||||||
|
let mut sum_y = 0.0;
|
||||||
|
for i in 0..n {
|
||||||
|
sum_x += xs[i];
|
||||||
|
sum_y += ys[i];
|
||||||
|
}
|
||||||
|
let mean_x = sum_x / n_f;
|
||||||
|
let mean_y = sum_y / n_f;
|
||||||
|
|
||||||
|
let mut sum_prod = 0.0;
|
||||||
|
for i in 0..n {
|
||||||
|
let dx = xs[i] - mean_x;
|
||||||
|
let dy = ys[i] - mean_y;
|
||||||
|
sum_prod += dx * dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cov = sum_prod / n_f;
|
||||||
|
CalcResult::Number(cov)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_covariance_s(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> CalcResult {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let values1_opts = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
|
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => return error,
|
||||||
|
},
|
||||||
|
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in first array: {:?}", error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"First argument must be a range or array".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let values2_opts = match self.evaluate_node_in_context(&args[1], cell) {
|
||||||
|
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => return error,
|
||||||
|
},
|
||||||
|
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in second array: {:?}", error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Second argument must be a range or array".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Same number of cells
|
||||||
|
if values1_opts.len() != values2_opts.len() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NA,
|
||||||
|
cell,
|
||||||
|
"COVARIANCE.S requires arrays of the same size".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count numeric data points in each array (ignoring text/booleans/empty)
|
||||||
|
let count1 = values1_opts.iter().filter(|v| v.is_some()).count();
|
||||||
|
let count2 = values2_opts.iter().filter(|v| v.is_some()).count();
|
||||||
|
|
||||||
|
if count1 == 0 || count2 == 0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"COVARIANCE.S requires numeric values in each array".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if count1 != count2 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NA,
|
||||||
|
cell,
|
||||||
|
"COVARIANCE.S arrays must have the same number of numeric data points".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build paired numeric vectors
|
||||||
|
let mut xs: Vec<f64> = Vec::with_capacity(count1);
|
||||||
|
let mut ys: Vec<f64> = Vec::with_capacity(count2);
|
||||||
|
|
||||||
|
for (v1_opt, v2_opt) in values1_opts.into_iter().zip(values2_opts.into_iter()) {
|
||||||
|
if let (Some(x), Some(y)) = (v1_opt, v2_opt) {
|
||||||
|
xs.push(x);
|
||||||
|
ys.push(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = xs.len();
|
||||||
|
if n < 2 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"COVARIANCE.S requires at least two paired data points".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n_f = n as f64;
|
||||||
|
|
||||||
|
let mut sum_x = 0.0;
|
||||||
|
let mut sum_y = 0.0;
|
||||||
|
for i in 0..n {
|
||||||
|
sum_x += xs[i];
|
||||||
|
sum_y += ys[i];
|
||||||
|
}
|
||||||
|
let mean_x = sum_x / n_f;
|
||||||
|
let mean_y = sum_y / n_f;
|
||||||
|
|
||||||
|
let mut sum_prod = 0.0;
|
||||||
|
for i in 0..n {
|
||||||
|
let dx = xs[i] - mean_x;
|
||||||
|
let dy = ys[i] - mean_y;
|
||||||
|
sum_prod += dx * dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cov = sum_prod / (n_f - 1.0);
|
||||||
|
|
||||||
|
CalcResult::Number(cov)
|
||||||
|
}
|
||||||
|
}
|
||||||
135
base/src/functions/statistical/devsq.rs
Normal file
135
base/src/functions/statistical/devsq.rs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||||
|
use crate::expressions::parser::ArrayNode;
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// DEVSQ(number1, [number2], ...)
|
||||||
|
pub(crate) fn fn_devsq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
let mut sumsq = 0.0;
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
|
||||||
|
// tiny helper so we don't repeat ourselves
|
||||||
|
#[inline]
|
||||||
|
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||||
|
*sum += value;
|
||||||
|
*sumsq += value * value;
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
match self.evaluate_node_in_context(arg, cell) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
CalcResult::Range { left, right } => {
|
||||||
|
if left.sheet != right.sheet {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let row1 = left.row;
|
||||||
|
let mut row2 = right.row;
|
||||||
|
let column1 = left.column;
|
||||||
|
let mut column2 = right.column;
|
||||||
|
|
||||||
|
if row1 == 1 && row2 == LAST_ROW {
|
||||||
|
row2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_row,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if column1 == 1 && column2 == LAST_COLUMN {
|
||||||
|
column2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_column,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in row1..row2 + 1 {
|
||||||
|
for column in column1..(column2 + 1) {
|
||||||
|
match self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: left.sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
}) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// We ignore booleans and strings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Array(array) => {
|
||||||
|
for row in array {
|
||||||
|
for value in row {
|
||||||
|
match value {
|
||||||
|
ArrayNode::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
ArrayNode::Error(error) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error,
|
||||||
|
origin: cell,
|
||||||
|
message: "Error in array".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// We ignore booleans and strings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// We ignore booleans and strings
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
// No numeric data at all
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"DEVSQ with no numeric data".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = count as f64;
|
||||||
|
let mut result = sumsq - (sum * sum) / n;
|
||||||
|
|
||||||
|
// Numerical noise can make result slightly negative when it should be 0
|
||||||
|
if result < 0.0 && result > -1e-12 {
|
||||||
|
result = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
54
base/src/functions/statistical/exponential.rs
Normal file
54
base/src/functions/statistical/exponential.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub(crate) fn fn_expon_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
// EXPON.DIST(x, lambda, cumulative)
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let lambda = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cumulative = match self.get_boolean(&args[2], cell) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if x < 0.0 || lambda <= 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for EXPON.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = if cumulative {
|
||||||
|
// CDF
|
||||||
|
1.0 - (-lambda * x).exp()
|
||||||
|
} else {
|
||||||
|
// PDF
|
||||||
|
lambda * (-lambda * x).exp()
|
||||||
|
};
|
||||||
|
|
||||||
|
if result.is_nan() || result.is_infinite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for EXPON.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
418
base/src/functions/statistical/fisher.rs
Normal file
418
base/src/functions/statistical/fisher.rs
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
use statrs::distribution::{Continuous, ContinuousCDF, FisherSnedecor};
|
||||||
|
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::functions::statistical::t_dist::sample_var;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// FISHER(x) = 0.5 * ln((1 + x) / (1 - x))
|
||||||
|
pub(crate) fn fn_fisher(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if x <= -1.0 || x >= 1.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "x must be between -1 and 1 (exclusive) in FISHER".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let ratio = (1.0 + x) / (1.0 - x);
|
||||||
|
let result = 0.5 * ratio.ln();
|
||||||
|
|
||||||
|
if result.is_nan() || result.is_infinite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for FISHER".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FISHERINV(y) = (e^(2y) - 1) / (e^(2y) + 1) = tanh(y)
|
||||||
|
pub(crate) fn fn_fisher_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let y = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use tanh directly to avoid overflow from exp(2y)
|
||||||
|
let result = y.tanh();
|
||||||
|
|
||||||
|
if result.is_nan() || result.is_infinite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for FISHERINV".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// F.DIST(x, deg_freedom1, deg_freedom2, cumulative)
|
||||||
|
pub(crate) fn fn_f_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 4 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let df1 = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let df2 = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cumulative = match self.get_boolean(&args[3], cell) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Excel domain checks
|
||||||
|
if x < 0.0 {
|
||||||
|
return CalcResult::new_error(Error::NUM, cell, "x must be >= 0 in F.DIST".to_string());
|
||||||
|
}
|
||||||
|
if df1 < 1.0 || df2 < 1.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"degrees of freedom must be >= 1 in F.DIST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match FisherSnedecor::new(df1, df2) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for F distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
|
||||||
|
|
||||||
|
if result.is_nan() || result.is_infinite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid result for F.DIST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_f_dist_rt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
// F.DIST.RT(x, deg_freedom1, deg_freedom2)
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let df1 = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let df2 = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if x < 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"x must be >= 0 in F.DIST.RT".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if df1 < 1.0 || df2 < 1.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"degrees of freedom must be >= 1 in F.DIST.RT".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match FisherSnedecor::new(df1, df2) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for F distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Right-tail probability: P(F > x) = 1 - CDF(x)
|
||||||
|
let result = 1.0 - dist.cdf(x);
|
||||||
|
|
||||||
|
if result.is_nan() || result.is_infinite() || result < 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid result for F.DIST.RT".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// F.INV(probability, deg_freedom1, deg_freedom2)
|
||||||
|
pub(crate) fn fn_f_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let df1 = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let df2 = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// probability < 0 or > 1 → #NUM!
|
||||||
|
if !(0.0..=1.0).contains(&p) {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"probability must be in [0,1] in F.INV".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if df1 < 1.0 || df2 < 1.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"degrees of freedom must be >= 1 in F.INV".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match FisherSnedecor::new(df1, df2) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for F distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let x = dist.inverse_cdf(p);
|
||||||
|
if x.is_nan() || x.is_infinite() || x < 0.0 {
|
||||||
|
return CalcResult::new_error(Error::NUM, cell, "Invalid result for F.INV".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// F.INV.RT(probability, deg_freedom1, deg_freedom2)
|
||||||
|
pub(crate) fn fn_f_inv_rt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let df1 = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let df2 = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if p <= 0.0 || p > 1.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"probability must be in (0,1] in F.INV.RT".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if df1 < 1.0 || df2 < 1.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"degrees of freedom must be >= 1 in F.INV.RT".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match FisherSnedecor::new(df1, df2) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for F distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// p is right-tail: p = P(F > x) = 1 - CDF(x)
|
||||||
|
let x = dist.inverse_cdf(1.0 - p);
|
||||||
|
if x.is_nan() || x.is_infinite() || x < 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid result for F.INV.RT".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// F.TEST(array1, array2)
|
||||||
|
pub(crate) fn fn_f_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let values1_opts = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
|
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => return error,
|
||||||
|
},
|
||||||
|
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in first array: {:?}", error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"First argument must be a range or array".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get second sample as Vec<Option<f64>>
|
||||||
|
let values2_opts = match self.evaluate_node_in_context(&args[1], cell) {
|
||||||
|
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => return error,
|
||||||
|
},
|
||||||
|
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in second array: {:?}", error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Second argument must be a range or array".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let values1: Vec<f64> = values1_opts.into_iter().flatten().collect();
|
||||||
|
let values2: Vec<f64> = values2_opts.into_iter().flatten().collect();
|
||||||
|
|
||||||
|
let n1 = values1.len();
|
||||||
|
let n2 = values2.len();
|
||||||
|
|
||||||
|
// If fewer than 2 numeric values in either sample -> #DIV/0!
|
||||||
|
if n1 < 2 || n2 < 2 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"F.TEST requires at least two numeric values in each sample".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let v1 = sample_var(&values1);
|
||||||
|
let v2 = sample_var(&values2);
|
||||||
|
|
||||||
|
if v1 <= 0.0 || v2 <= 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Variance of one sample is zero in F.TEST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// F ratio: larger variance / smaller variance
|
||||||
|
let mut f = v1 / v2;
|
||||||
|
let mut df1 = (n1 - 1) as f64;
|
||||||
|
let mut df2 = (n2 - 1) as f64;
|
||||||
|
|
||||||
|
if f < 1.0 {
|
||||||
|
f = 1.0 / f;
|
||||||
|
std::mem::swap(&mut df1, &mut df2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match FisherSnedecor::new(df1, df2) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for F distribution in F.TEST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// One-tailed right-tail probability
|
||||||
|
let tail = 1.0 - dist.cdf(f);
|
||||||
|
// F.TEST is two-tailed: p = 2 * tail (with F >= 1)
|
||||||
|
let mut p = 2.0 * tail;
|
||||||
|
|
||||||
|
// Clamp tiny FP noise
|
||||||
|
if p < 0.0 && p > -1e-15 {
|
||||||
|
p = 0.0;
|
||||||
|
}
|
||||||
|
if p > 1.0 && p < 1.0 + 1e-15 {
|
||||||
|
p = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
194
base/src/functions/statistical/gamma.rs
Normal file
194
base/src/functions/statistical/gamma.rs
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
use statrs::distribution::{Continuous, ContinuousCDF, Gamma};
|
||||||
|
use statrs::function::gamma::{gamma, ln_gamma};
|
||||||
|
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub(crate) fn fn_gamma(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(s) => return s,
|
||||||
|
};
|
||||||
|
if x < 0.0 && x.floor() == x {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameter for Gamma function".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let result = gamma(x);
|
||||||
|
if result.is_nan() || result.is_infinite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameter for Gamma function".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_gamma_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
// GAMMA.DIST(x, alpha, beta, cumulative)
|
||||||
|
if args.len() != 4 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let alpha = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let beta_scale = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cumulative = match self.get_boolean(&args[3], cell) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if x < 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"x must be >= 0 in GAMMA.DIST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if alpha <= 0.0 || beta_scale <= 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"alpha and beta must be > 0 in GAMMA.DIST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rate = 1.0 / beta_scale;
|
||||||
|
|
||||||
|
let dist = match Gamma::new(alpha, rate) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for Gamma distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
|
||||||
|
|
||||||
|
if result.is_nan() || result.is_infinite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid result for GAMMA.DIST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_gamma_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
// GAMMA.INV(probability, alpha, beta)
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let alpha = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let beta_scale = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(0.0..=1.0).contains(&p) {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"probability must be in [0,1] in GAMMA.INV".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if alpha <= 0.0 || beta_scale <= 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"alpha and beta must be > 0 in GAMMA.INV".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rate = 1.0 / beta_scale;
|
||||||
|
|
||||||
|
let dist = match Gamma::new(alpha, rate) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for Gamma distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let x = dist.inverse_cdf(p);
|
||||||
|
if x.is_nan() || x.is_infinite() || x < 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid result for GAMMA.INV".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_gamma_ln(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(s) => return s,
|
||||||
|
};
|
||||||
|
if x < 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameter for Gamma function".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let result = ln_gamma(x);
|
||||||
|
if result.is_nan() || result.is_infinite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameter for Gamma Ln function".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_gamma_ln_precise(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> CalcResult {
|
||||||
|
self.fn_gamma_ln(args, cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
87
base/src/functions/statistical/geomean.rs
Normal file
87
base/src/functions/statistical/geomean.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub(crate) fn fn_geomean(&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 product = 1.0;
|
||||||
|
for arg in args {
|
||||||
|
match self.evaluate_node_in_context(arg, cell) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
count += 1.0;
|
||||||
|
product *= value;
|
||||||
|
}
|
||||||
|
CalcResult::Boolean(b) => {
|
||||||
|
if let Node::ReferenceKind { .. } = arg {
|
||||||
|
} else {
|
||||||
|
product *= if b { 1.0 } else { 0.0 };
|
||||||
|
count += 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Range { left, right } => {
|
||||||
|
if left.sheet != right.sheet {
|
||||||
|
return 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) {
|
||||||
|
match self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: left.sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
}) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
count += 1.0;
|
||||||
|
product *= value;
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
CalcResult::Range { .. } => {
|
||||||
|
return 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::<f64>() {
|
||||||
|
product *= t;
|
||||||
|
count += 1.0;
|
||||||
|
} else {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "Argument cannot be cast into number".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Ignore everything else
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if count == 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "Division by Zero".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
CalcResult::Number(product.powf(1.0 / count))
|
||||||
|
}
|
||||||
|
}
|
||||||
108
base/src/functions/statistical/hypegeom.rs
Normal file
108
base/src/functions/statistical/hypegeom.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use statrs::distribution::{Discrete, DiscreteCDF, Hypergeometric};
|
||||||
|
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// =HYPGEOM.DIST(sample_s, number_sample, population_s, number_pop, cumulative)
|
||||||
|
pub(crate) fn fn_hyp_geom_dist(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> CalcResult {
|
||||||
|
if args.len() != 5 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sample_s (number of successes in the sample)
|
||||||
|
let sample_s = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// number_sample (sample size)
|
||||||
|
let number_sample = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// population_s (number of successes in the population)
|
||||||
|
let population_s = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// number_pop (population size)
|
||||||
|
let number_pop = match self.get_number_no_bools(&args[3], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cumulative = match self.get_boolean(&args[4], cell) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if sample_s < 0.0 || sample_s > f64::min(number_sample, population_s) {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for HYPGEOM.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if sample_s < f64::max(0.0, number_sample + population_s - number_pop) {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for HYPGEOM.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if number_sample <= 0.0 || number_sample > number_pop {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for HYPGEOM.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if population_s <= 0.0 || population_s > number_pop {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for HYPGEOM.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let n_pop = number_pop as u64;
|
||||||
|
let k_pop = population_s as u64;
|
||||||
|
let n_sample = number_sample as u64;
|
||||||
|
let k = sample_s as u64;
|
||||||
|
|
||||||
|
let dist = match Hypergeometric::new(n_pop, k_pop, n_sample) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for hypergeometric distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let prob = if cumulative { dist.cdf(k) } else { dist.pmf(k) };
|
||||||
|
|
||||||
|
if !prob.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for HYPGEOM.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(prob)
|
||||||
|
}
|
||||||
|
}
|
||||||
337
base/src/functions/statistical/if_ifs.rs
Normal file
337
base/src/functions/statistical/if_ifs.rs
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::functions::util::build_criteria;
|
||||||
|
use crate::{
|
||||||
|
calc_result::{CalcResult, Range},
|
||||||
|
expressions::parser::Node,
|
||||||
|
expressions::token::Error,
|
||||||
|
model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub(crate) fn fn_countif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() == 2 {
|
||||||
|
let arguments = vec![args[0].clone(), args[1].clone()];
|
||||||
|
self.fn_countifs(&arguments, cell)
|
||||||
|
} else {
|
||||||
|
CalcResult::new_args_number_error(cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AVERAGEIF(criteria_range, criteria, [average_range])
|
||||||
|
/// if average_rage is missing then criteria_range will be used
|
||||||
|
pub(crate) fn fn_averageif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() == 2 {
|
||||||
|
let arguments = vec![args[0].clone(), args[0].clone(), args[1].clone()];
|
||||||
|
self.fn_averageifs(&arguments, cell)
|
||||||
|
} else if args.len() == 3 {
|
||||||
|
let arguments = vec![args[2].clone(), args[0].clone(), args[1].clone()];
|
||||||
|
self.fn_averageifs(&arguments, cell)
|
||||||
|
} else {
|
||||||
|
CalcResult::new_args_number_error(cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: This function shares a lot of code with apply_ifs. Can we merge them?
|
||||||
|
pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let args_count = args.len();
|
||||||
|
if args_count < 2 || !args_count.is_multiple_of(2) {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let case_count = args_count / 2;
|
||||||
|
// NB: this is a beautiful example of the borrow checker
|
||||||
|
// The order of these two definitions cannot be swapped.
|
||||||
|
let mut criteria = Vec::new();
|
||||||
|
let mut fn_criteria = Vec::new();
|
||||||
|
let ranges = &mut Vec::new();
|
||||||
|
for case_index in 0..case_count {
|
||||||
|
let criterion = self.evaluate_node_in_context(&args[case_index * 2 + 1], cell);
|
||||||
|
criteria.push(criterion);
|
||||||
|
// NB: We cannot do:
|
||||||
|
// fn_criteria.push(build_criteria(&criterion));
|
||||||
|
// because criterion doesn't live long enough
|
||||||
|
let result = self.evaluate_node_in_context(&args[case_index * 2], cell);
|
||||||
|
if result.is_error() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if let CalcResult::Range { left, right } = result {
|
||||||
|
if left.sheet != right.sheet {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// TODO test ranges are of the same size as sum_range
|
||||||
|
ranges.push(Range { left, right });
|
||||||
|
} else {
|
||||||
|
return CalcResult::new_error(Error::VALUE, cell, "Expected a range".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for criterion in criteria.iter() {
|
||||||
|
fn_criteria.push(build_criteria(criterion));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut total = 0.0;
|
||||||
|
let first_range = &ranges[0];
|
||||||
|
let left_row = first_range.left.row;
|
||||||
|
let left_column = first_range.left.column;
|
||||||
|
let right_row = first_range.right.row;
|
||||||
|
let right_column = first_range.right.column;
|
||||||
|
|
||||||
|
let dimension = match self.workbook.worksheet(first_range.left.sheet) {
|
||||||
|
Ok(s) => s.dimension(),
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", first_range.left.sheet),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let max_row = dimension.max_row;
|
||||||
|
let max_column = dimension.max_column;
|
||||||
|
|
||||||
|
let open_row = left_row == 1 && right_row == LAST_ROW;
|
||||||
|
let open_column = left_column == 1 && right_column == LAST_COLUMN;
|
||||||
|
|
||||||
|
for row in left_row..right_row + 1 {
|
||||||
|
if open_row && row > max_row {
|
||||||
|
// If the row is larger than the max row in the sheet then all cells are empty.
|
||||||
|
// We compute it only once
|
||||||
|
let mut is_true = true;
|
||||||
|
for fn_criterion in fn_criteria.iter() {
|
||||||
|
if !fn_criterion(&CalcResult::EmptyCell) {
|
||||||
|
is_true = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if is_true {
|
||||||
|
total += ((LAST_ROW - max_row) * (right_column - left_column + 1)) as f64;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for column in left_column..right_column + 1 {
|
||||||
|
if open_column && column > max_column {
|
||||||
|
// If the column is larger than the max column in the sheet then all cells are empty.
|
||||||
|
// We compute it only once
|
||||||
|
let mut is_true = true;
|
||||||
|
for fn_criterion in fn_criteria.iter() {
|
||||||
|
if !fn_criterion(&CalcResult::EmptyCell) {
|
||||||
|
is_true = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if is_true {
|
||||||
|
total += (LAST_COLUMN - max_column) as f64;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let mut is_true = true;
|
||||||
|
for case_index in 0..case_count {
|
||||||
|
// We check if value in range n meets criterion n
|
||||||
|
let range = &ranges[case_index];
|
||||||
|
let fn_criterion = &fn_criteria[case_index];
|
||||||
|
let value = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: range.left.sheet,
|
||||||
|
row: range.left.row + row - first_range.left.row,
|
||||||
|
column: range.left.column + column - first_range.left.column,
|
||||||
|
});
|
||||||
|
if !fn_criterion(&value) {
|
||||||
|
is_true = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if is_true {
|
||||||
|
total += 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Number(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn apply_ifs<F>(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
mut apply: F,
|
||||||
|
) -> Result<(), CalcResult>
|
||||||
|
where
|
||||||
|
F: FnMut(f64),
|
||||||
|
{
|
||||||
|
let args_count = args.len();
|
||||||
|
if args_count < 3 || args_count.is_multiple_of(2) {
|
||||||
|
return Err(CalcResult::new_args_number_error(cell));
|
||||||
|
}
|
||||||
|
let arg_0 = self.evaluate_node_in_context(&args[0], cell);
|
||||||
|
if arg_0.is_error() {
|
||||||
|
return Err(arg_0);
|
||||||
|
}
|
||||||
|
let sum_range = if let CalcResult::Range { left, right } = arg_0 {
|
||||||
|
if left.sheet != right.sheet {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Range { left, right }
|
||||||
|
} else {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Expected a range".to_string(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let case_count = (args_count - 1) / 2;
|
||||||
|
// NB: this is a beautiful example of the borrow checker
|
||||||
|
// The order of these two definitions cannot be swapped.
|
||||||
|
let mut criteria = Vec::new();
|
||||||
|
let mut fn_criteria = Vec::new();
|
||||||
|
let ranges = &mut Vec::new();
|
||||||
|
for case_index in 1..=case_count {
|
||||||
|
let criterion = self.evaluate_node_in_context(&args[case_index * 2], cell);
|
||||||
|
// NB: criterion might be an error. That's ok
|
||||||
|
criteria.push(criterion);
|
||||||
|
// NB: We cannot do:
|
||||||
|
// fn_criteria.push(build_criteria(&criterion));
|
||||||
|
// because criterion doesn't live long enough
|
||||||
|
let result = self.evaluate_node_in_context(&args[case_index * 2 - 1], cell);
|
||||||
|
if result.is_error() {
|
||||||
|
return Err(result);
|
||||||
|
}
|
||||||
|
if let CalcResult::Range { left, right } = result {
|
||||||
|
if left.sheet != right.sheet {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// TODO test ranges are of the same size as sum_range
|
||||||
|
ranges.push(Range { left, right });
|
||||||
|
} else {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Expected a range".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for criterion in criteria.iter() {
|
||||||
|
fn_criteria.push(build_criteria(criterion));
|
||||||
|
}
|
||||||
|
|
||||||
|
let left_row = sum_range.left.row;
|
||||||
|
let left_column = sum_range.left.column;
|
||||||
|
let mut right_row = sum_range.right.row;
|
||||||
|
let mut right_column = sum_range.right.column;
|
||||||
|
|
||||||
|
if left_row == 1 && right_row == LAST_ROW {
|
||||||
|
right_row = match self.workbook.worksheet(sum_range.left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_row,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", sum_range.left.sheet),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if left_column == 1 && right_column == LAST_COLUMN {
|
||||||
|
right_column = match self.workbook.worksheet(sum_range.left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_column,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", sum_range.left.sheet),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in left_row..right_row + 1 {
|
||||||
|
for column in left_column..right_column + 1 {
|
||||||
|
let mut is_true = true;
|
||||||
|
for case_index in 0..case_count {
|
||||||
|
// We check if value in range n meets criterion n
|
||||||
|
let range = &ranges[case_index];
|
||||||
|
let fn_criterion = &fn_criteria[case_index];
|
||||||
|
let value = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: range.left.sheet,
|
||||||
|
row: range.left.row + row - sum_range.left.row,
|
||||||
|
column: range.left.column + column - sum_range.left.column,
|
||||||
|
});
|
||||||
|
if !fn_criterion(&value) {
|
||||||
|
is_true = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if is_true {
|
||||||
|
let v = self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: sum_range.left.sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
});
|
||||||
|
match v {
|
||||||
|
CalcResult::Number(n) => apply(n),
|
||||||
|
CalcResult::Error { .. } => return Err(v),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_averageifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let mut total = 0.0;
|
||||||
|
let mut count = 0.0;
|
||||||
|
|
||||||
|
let average = |value: f64| {
|
||||||
|
total += value;
|
||||||
|
count += 1.0;
|
||||||
|
};
|
||||||
|
if let Err(e) = self.apply_ifs(args, cell, average) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "division by 0".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
CalcResult::Number(total / count)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_minifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let mut min = f64::INFINITY;
|
||||||
|
let apply_min = |value: f64| min = value.min(min);
|
||||||
|
if let Err(e) = self.apply_ifs(args, cell, apply_min) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if min.is_infinite() {
|
||||||
|
min = 0.0;
|
||||||
|
}
|
||||||
|
CalcResult::Number(min)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_maxifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let mut max = -f64::INFINITY;
|
||||||
|
let apply_max = |value: f64| max = value.max(max);
|
||||||
|
if let Err(e) = self.apply_ifs(args, cell, apply_max) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
if max.is_infinite() {
|
||||||
|
max = 0.0;
|
||||||
|
}
|
||||||
|
CalcResult::Number(max)
|
||||||
|
}
|
||||||
|
}
|
||||||
124
base/src/functions/statistical/log_normal.rs
Normal file
124
base/src/functions/statistical/log_normal.rs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
use statrs::distribution::{Continuous, ContinuousCDF, LogNormal};
|
||||||
|
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub(crate) fn fn_log_norm_dist(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> CalcResult {
|
||||||
|
if args.len() != 4 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let mean = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let std_dev = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let cumulative = match self.get_boolean(&args[3], cell) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Excel domain checks
|
||||||
|
if x <= 0.0 || std_dev <= 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameter for LOGNORM.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match LogNormal::new(mean, std_dev) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameter for LOGNORM.DIST".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
|
||||||
|
|
||||||
|
if !result.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameter for LOGNORM.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_log_norm_inv(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> CalcResult {
|
||||||
|
use statrs::distribution::{ContinuousCDF, LogNormal};
|
||||||
|
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let mean = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let std_dev = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Excel domain checks
|
||||||
|
if p <= 0.0 || p >= 1.0 || std_dev <= 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameter for LOGNORM.INV".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match LogNormal::new(mean, std_dev) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameter for LOGNORM.INV".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = dist.inverse_cdf(p);
|
||||||
|
|
||||||
|
if !result.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameter for LOGNORM.INV".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
26
base/src/functions/statistical/mod.rs
Normal file
26
base/src/functions/statistical/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
mod beta;
|
||||||
|
mod binom;
|
||||||
|
mod chisq;
|
||||||
|
mod correl;
|
||||||
|
mod count_and_average;
|
||||||
|
mod covariance;
|
||||||
|
mod devsq;
|
||||||
|
mod exponential;
|
||||||
|
mod fisher;
|
||||||
|
mod gamma;
|
||||||
|
mod gauss;
|
||||||
|
mod geomean;
|
||||||
|
mod hypegeom;
|
||||||
|
mod if_ifs;
|
||||||
|
mod log_normal;
|
||||||
|
mod normal;
|
||||||
|
mod pearson;
|
||||||
|
mod phi;
|
||||||
|
mod poisson;
|
||||||
|
mod rank_eq_avg;
|
||||||
|
mod standard_dev;
|
||||||
|
mod standardize;
|
||||||
|
mod t_dist;
|
||||||
|
mod variance;
|
||||||
|
mod weibull;
|
||||||
|
mod z_test;
|
||||||
325
base/src/functions/statistical/normal.rs
Normal file
325
base/src/functions/statistical/normal.rs
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
use statrs::distribution::{Continuous, ContinuousCDF, Normal, StudentsT};
|
||||||
|
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// NORM.DIST(x, mean, standard_dev, cumulative)
|
||||||
|
pub(crate) fn fn_norm_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 4 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let mean = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let std_dev = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let cumulative = match self.get_boolean(&args[3], cell) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Excel: standard_dev must be > 0
|
||||||
|
if std_dev <= 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "standard_dev must be > 0 in NORM.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match Normal::new(mean, std_dev) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for NORM.DIST".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
|
||||||
|
|
||||||
|
if !result.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for NORM.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NORM.INV(probability, mean, standard_dev)
|
||||||
|
pub(crate) fn fn_norm_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let mean = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let std_dev = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if p <= 0.0 || p >= 1.0 || std_dev <= 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for NORM.INV".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match Normal::new(mean, std_dev) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for NORM.INV".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let x = dist.inverse_cdf(p);
|
||||||
|
|
||||||
|
if !x.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for NORM.INV".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NORM.S.DIST(z, cumulative)
|
||||||
|
pub(crate) fn fn_norm_s_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let z = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let cumulative = match self.get_boolean(&args[1], cell) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = if cumulative { dist.cdf(z) } else { dist.pdf(z) };
|
||||||
|
|
||||||
|
if !result.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for NORM.S.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NORM.S.INV(probability)
|
||||||
|
pub(crate) fn fn_norm_s_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if p <= 0.0 || p >= 1.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "probability must be in (0,1) in NORM.S.INV".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 z = dist.inverse_cdf(p);
|
||||||
|
|
||||||
|
if !z.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for NORM.S.INV".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(z)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_confidence_norm(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let alpha = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let std_dev = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let size = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f.floor(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if alpha <= 0.0 || alpha >= 1.0 || std_dev <= 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for CONFIDENCE.NORM".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if size < 1.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Sample size must be at least 1".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let normal = match Normal::new(0.0, 1.0) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
"Failed to construct normal distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let quantile = normal.inverse_cdf(1.0 - alpha / 2.0);
|
||||||
|
if !quantile.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid quantile for CONFIDENCE.NORM".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let margin = quantile * std_dev / size.sqrt();
|
||||||
|
CalcResult::Number(margin)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_confidence_t(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let alpha = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let std_dev = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let size = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Domain checks
|
||||||
|
if alpha <= 0.0 || alpha >= 1.0 || std_dev <= 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for CONFIDENCE.T".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need at least 2 observations so df = n - 1 > 0
|
||||||
|
if size < 2.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "Sample size must be at least 2".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let df = size - 1.0;
|
||||||
|
|
||||||
|
let t_dist = match StudentsT::new(0.0, 1.0, df) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
"Failed to construct Student's t distribution".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Two-sided CI => use 1 - alpha/2
|
||||||
|
let t_crit = t_dist.inverse_cdf(1.0 - alpha / 2.0);
|
||||||
|
if !t_crit.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid quantile for CONFIDENCE.T".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let margin = t_crit * std_dev / size.sqrt();
|
||||||
|
CalcResult::Number(margin)
|
||||||
|
}
|
||||||
|
}
|
||||||
113
base/src/functions/statistical/pearson.rs
Normal file
113
base/src/functions/statistical/pearson.rs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// PEARSON(array1, array2)
|
||||||
|
pub(crate) fn fn_pearson(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (_, _, values_left, values_right) = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Flatten into (x, y) pairs, skipping non-numeric entries (None)
|
||||||
|
let mut n: f64 = 0.0;
|
||||||
|
let mut sum_x = 0.0;
|
||||||
|
let mut sum_y = 0.0;
|
||||||
|
let mut sum_x2 = 0.0;
|
||||||
|
let mut sum_y2 = 0.0;
|
||||||
|
let mut sum_xy = 0.0;
|
||||||
|
|
||||||
|
let len = values_left.len().min(values_right.len());
|
||||||
|
for i in 0..len {
|
||||||
|
match (values_left[i], values_right[i]) {
|
||||||
|
(Some(x), Some(y)) => {
|
||||||
|
n += 1.0;
|
||||||
|
sum_x += x;
|
||||||
|
sum_y += y;
|
||||||
|
sum_x2 += x * x;
|
||||||
|
sum_y2 += y * y;
|
||||||
|
sum_xy += x * y;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Ignore pairs where at least one side is non-numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 2.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"PEARSON requires at least two numeric pairs".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pearson correlation:
|
||||||
|
// r = [ n*Σxy - (Σx)(Σy) ] / sqrt( [n*Σx² - (Σx)²] [n*Σy² - (Σy)²] )
|
||||||
|
let num = n * sum_xy - sum_x * sum_y;
|
||||||
|
let denom_x = n * sum_x2 - sum_x * sum_x;
|
||||||
|
let denom_y = n * sum_y2 - sum_y * sum_y;
|
||||||
|
|
||||||
|
if denom_x.abs() < 1e-15 || denom_y.abs() < 1e-15 {
|
||||||
|
// Zero variance in at least one series
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"PEARSON cannot be computed when one series has zero variance".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let denom = (denom_x * denom_y).sqrt();
|
||||||
|
|
||||||
|
CalcResult::Number(num / denom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RSQ(array1, array2) = CORREL(array1, array2)^2
|
||||||
|
pub(crate) fn fn_rsq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (_rows, _cols, values1, values2) = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut n = 0.0_f64;
|
||||||
|
let mut sum_x = 0.0_f64;
|
||||||
|
let mut sum_y = 0.0_f64;
|
||||||
|
let mut sum_x2 = 0.0_f64;
|
||||||
|
let mut sum_y2 = 0.0_f64;
|
||||||
|
let mut sum_xy = 0.0_f64;
|
||||||
|
|
||||||
|
let len = values1.len().min(values2.len());
|
||||||
|
for i in 0..len {
|
||||||
|
if let (Some(x), Some(y)) = (values1[i], values2[i]) {
|
||||||
|
n += 1.0;
|
||||||
|
sum_x += x;
|
||||||
|
sum_y += y;
|
||||||
|
sum_x2 += x * x;
|
||||||
|
sum_y2 += y * y;
|
||||||
|
sum_xy += x * y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 2.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"RSQ requires at least two numeric data points in each range".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let num = n * sum_xy - sum_x * sum_y;
|
||||||
|
let denom_x = n * sum_x2 - sum_x * sum_x;
|
||||||
|
let denom_y = n * sum_y2 - sum_y * sum_y;
|
||||||
|
let denom = (denom_x * denom_y).sqrt();
|
||||||
|
|
||||||
|
if denom == 0.0 || !denom.is_finite() {
|
||||||
|
return CalcResult::new_error(Error::DIV, cell, "Division by zero in RSQ".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = num / denom;
|
||||||
|
CalcResult::Number(r * r)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
base/src/functions/statistical/phi.rs
Normal file
21
base/src/functions/statistical/phi.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{calc_result::CalcResult, expressions::parser::Node, model::Model};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// PHI(x) = standard normal PDF at x
|
||||||
|
pub(crate) fn fn_phi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Standard normal PDF: (1 / sqrt(2π)) * exp(-x^2 / 2)
|
||||||
|
let result = (-(x * x) / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt();
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
94
base/src/functions/statistical/poisson.rs
Normal file
94
base/src/functions/statistical/poisson.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
use statrs::distribution::{Discrete, DiscreteCDF, Poisson};
|
||||||
|
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// =POISSON.DIST(x, mean, cumulative)
|
||||||
|
pub(crate) fn fn_poisson_dist(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// x
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// mean (lambda)
|
||||||
|
let lambda = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cumulative = match self.get_boolean(&args[2], cell) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if x < 0.0 || lambda < 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for POISSON.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard against insane k for u64
|
||||||
|
if x < 0.0 || x > (u64::MAX as f64) {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for POISSON.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let k = x as u64;
|
||||||
|
|
||||||
|
// Special-case lambda = 0: degenerate distribution at 0
|
||||||
|
if lambda == 0.0 {
|
||||||
|
let result = if cumulative {
|
||||||
|
// For x >= 0, P(X <= x) = 1
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
// P(X = 0) = 1, P(X = k>0) = 0
|
||||||
|
if k == 0 {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return CalcResult::Number(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match Poisson::new(lambda) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for POISSON.DIST".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let prob = if cumulative { dist.cdf(k) } else { dist.pmf(k) };
|
||||||
|
|
||||||
|
if !prob.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for POISSON.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(prob)
|
||||||
|
}
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
519
base/src/functions/statistical/standard_dev.rs
Normal file
519
base/src/functions/statistical/standard_dev.rs
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||||
|
use crate::expressions::parser::ArrayNode;
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub(crate) fn fn_stdev_p(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
let mut sumsq = 0.0;
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||||
|
*sum += value;
|
||||||
|
*sumsq += value * value;
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
match self.evaluate_node_in_context(arg, cell) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
CalcResult::Range { left, right } => {
|
||||||
|
if left.sheet != right.sheet {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let row1 = left.row;
|
||||||
|
let mut row2 = right.row;
|
||||||
|
let column1 = left.column;
|
||||||
|
let mut column2 = right.column;
|
||||||
|
|
||||||
|
if row1 == 1 && row2 == LAST_ROW {
|
||||||
|
row2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_row,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if column1 == 1 && column2 == LAST_COLUMN {
|
||||||
|
column2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_column,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in row1..row2 + 1 {
|
||||||
|
for column in column1..(column2 + 1) {
|
||||||
|
match self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: left.sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
}) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Array(array) => {
|
||||||
|
for row in array {
|
||||||
|
for value in row {
|
||||||
|
match value {
|
||||||
|
ArrayNode::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
ArrayNode::Error(error) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error,
|
||||||
|
origin: cell,
|
||||||
|
message: "Error in array".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"STDEV.P with no numeric data".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = count as f64;
|
||||||
|
let mut var = (sumsq - (sum * sum) / n) / n;
|
||||||
|
|
||||||
|
// clamp tiny negatives from FP noise
|
||||||
|
if var < 0.0 && var > -1e-12 {
|
||||||
|
var = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(var.sqrt())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_stdev_s(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
let mut sumsq = 0.0;
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||||
|
*sum += value;
|
||||||
|
*sumsq += value * value;
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
match self.evaluate_node_in_context(arg, cell) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
CalcResult::Range { left, right } => {
|
||||||
|
if left.sheet != right.sheet {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let row1 = left.row;
|
||||||
|
let mut row2 = right.row;
|
||||||
|
let column1 = left.column;
|
||||||
|
let mut column2 = right.column;
|
||||||
|
|
||||||
|
if row1 == 1 && row2 == LAST_ROW {
|
||||||
|
row2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_row,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if column1 == 1 && column2 == LAST_COLUMN {
|
||||||
|
column2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_column,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in row1..row2 + 1 {
|
||||||
|
for column in column1..(column2 + 1) {
|
||||||
|
match self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: left.sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
}) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Array(array) => {
|
||||||
|
for row in array {
|
||||||
|
for value in row {
|
||||||
|
match value {
|
||||||
|
ArrayNode::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
ArrayNode::Error(error) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error,
|
||||||
|
origin: cell,
|
||||||
|
message: "Error in array".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count <= 1 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"STDEV.S requires at least two numeric values".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = count as f64;
|
||||||
|
let mut var = (sumsq - (sum * sum) / n) / (n - 1.0);
|
||||||
|
|
||||||
|
if var < 0.0 && var > -1e-12 {
|
||||||
|
var = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(var.sqrt())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_stdeva(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
let mut sumsq = 0.0;
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||||
|
*sum += value;
|
||||||
|
*sumsq += value * value;
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
match self.evaluate_node_in_context(arg, cell) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
CalcResult::Range { left, right } => {
|
||||||
|
if left.sheet != right.sheet {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let row1 = left.row;
|
||||||
|
let mut row2 = right.row;
|
||||||
|
let column1 = left.column;
|
||||||
|
let mut column2 = right.column;
|
||||||
|
|
||||||
|
if row1 == 1 && row2 == LAST_ROW {
|
||||||
|
row2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_row,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if column1 == 1 && column2 == LAST_COLUMN {
|
||||||
|
column2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_column,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in row1..row2 + 1 {
|
||||||
|
for column in column1..(column2 + 1) {
|
||||||
|
match self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: left.sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
}) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
CalcResult::String(_) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, 0.0);
|
||||||
|
}
|
||||||
|
CalcResult::Boolean(value) => {
|
||||||
|
let val = if value { 1.0 } else { 0.0 };
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, val);
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Array(array) => {
|
||||||
|
for row in array {
|
||||||
|
for value in row {
|
||||||
|
match value {
|
||||||
|
ArrayNode::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
ArrayNode::Error(error) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error,
|
||||||
|
origin: cell,
|
||||||
|
message: "Error in array".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count <= 1 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"STDEVA requires at least two numeric values".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = count as f64;
|
||||||
|
let mut var = (sumsq - (sum * sum) / n) / (n - 1.0);
|
||||||
|
|
||||||
|
if var < 0.0 && var > -1e-12 {
|
||||||
|
var = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(var.sqrt())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_stdevpa(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
let mut sumsq = 0.0;
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||||
|
*sum += value;
|
||||||
|
*sumsq += value * value;
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
match self.evaluate_node_in_context(arg, cell) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
CalcResult::Range { left, right } => {
|
||||||
|
if left.sheet != right.sheet {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let row1 = left.row;
|
||||||
|
let mut row2 = right.row;
|
||||||
|
let column1 = left.column;
|
||||||
|
let mut column2 = right.column;
|
||||||
|
|
||||||
|
if row1 == 1 && row2 == LAST_ROW {
|
||||||
|
row2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_row,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if column1 == 1 && column2 == LAST_COLUMN {
|
||||||
|
column2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_column,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in row1..row2 + 1 {
|
||||||
|
for column in column1..(column2 + 1) {
|
||||||
|
match self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: left.sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
}) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
CalcResult::String(_) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, 0.0);
|
||||||
|
}
|
||||||
|
CalcResult::Boolean(value) => {
|
||||||
|
let val = if value { 1.0 } else { 0.0 };
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, val);
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Array(array) => {
|
||||||
|
for row in array {
|
||||||
|
for value in row {
|
||||||
|
match value {
|
||||||
|
ArrayNode::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
ArrayNode::Error(error) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error,
|
||||||
|
origin: cell,
|
||||||
|
message: "Error in array".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"STDEVPA with no numeric data".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = count as f64;
|
||||||
|
let mut var = (sumsq - (sum * sum) / n) / n;
|
||||||
|
|
||||||
|
if var < 0.0 && var > -1e-12 {
|
||||||
|
var = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(var.sqrt())
|
||||||
|
}
|
||||||
|
}
|
||||||
38
base/src/functions/statistical/standardize.rs
Normal file
38
base/src/functions/statistical/standardize.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub(crate) fn fn_standardize(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
// STANDARDIZE(x, mean, standard_dev)
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let mean = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let std_dev = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if std_dev <= 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "standard_dev must be > 0 in STANDARDIZE".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let z = (x - mean) / std_dev;
|
||||||
|
|
||||||
|
CalcResult::Number(z)
|
||||||
|
}
|
||||||
|
}
|
||||||
588
base/src/functions/statistical/t_dist.rs
Normal file
588
base/src/functions/statistical/t_dist.rs
Normal file
@@ -0,0 +1,588 @@
|
|||||||
|
use statrs::distribution::{Continuous, ContinuousCDF, StudentsT};
|
||||||
|
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn mean(xs: &[f64]) -> f64 {
|
||||||
|
let n = xs.len();
|
||||||
|
if n == 0 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let mut s = 0.0;
|
||||||
|
for &x in xs {
|
||||||
|
s += x;
|
||||||
|
}
|
||||||
|
s / (n as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sample_var(xs: &[f64]) -> f64 {
|
||||||
|
let n = xs.len();
|
||||||
|
if n < 2 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let m = mean(xs);
|
||||||
|
let mut s = 0.0;
|
||||||
|
for &x in xs {
|
||||||
|
let d = x - m;
|
||||||
|
s += d * d;
|
||||||
|
}
|
||||||
|
s / ((n - 1) as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TTestType {
|
||||||
|
Paired,
|
||||||
|
TwoSampleEqualVar,
|
||||||
|
TwoSampleUnequalVar,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TTestTails {
|
||||||
|
OneTailed,
|
||||||
|
TwoTailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// T.DIST(x, deg_freedom, cumulative)
|
||||||
|
pub(crate) fn fn_t_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let df = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cumulative = match self.get_boolean(&args[2], cell) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if df < 1.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "deg_freedom must be >= 1 in T.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match StudentsT::new(0.0, 1.0, df) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for T.DIST".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
|
||||||
|
|
||||||
|
if !result.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for T.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// T.DIST.2T(x, deg_freedom)
|
||||||
|
pub(crate) fn fn_t_dist_2t(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let df = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if x < 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "x must be >= 0 in T.DIST.2T".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if df < 1.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "deg_freedom must be >= 1 in T.DIST.2T".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match StudentsT::new(0.0, 1.0, df) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for T.DIST.2T".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let upper_tail = 1.0 - dist.cdf(x);
|
||||||
|
let mut result = 2.0 * upper_tail;
|
||||||
|
|
||||||
|
result = result.clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
if !result.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for T.DIST.2T".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// T.DIST.RT(x, deg_freedom)
|
||||||
|
pub(crate) fn fn_t_dist_rt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let df = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if df < 1.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "deg_freedom must be >= 1 in T.DIST.RT".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match StudentsT::new(0.0, 1.0, df) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for T.DIST.RT".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = 1.0 - dist.cdf(x);
|
||||||
|
|
||||||
|
if !result.is_finite() || result < 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for T.DIST.RT".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// T.INV(probability, deg_freedom)
|
||||||
|
pub(crate) fn fn_t_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let df = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if p <= 0.0 || p >= 1.0 || df < 1.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for T.INV".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match StudentsT::new(0.0, 1.0, df) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for T.INV".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let x = dist.inverse_cdf(p);
|
||||||
|
|
||||||
|
if !x.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for T.INV".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// T.INV.2T(probability, deg_freedom)
|
||||||
|
pub(crate) fn fn_t_inv_2t(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let df = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if p <= 0.0 || p > 1.0 || df < 1.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for T.INV.2T".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match StudentsT::new(0.0, 1.0, df) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for T.INV.2T".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Two-sided: F(x) = 1 - p/2
|
||||||
|
let target_cdf = 1.0 - p / 2.0;
|
||||||
|
let x = dist.inverse_cdf(target_cdf);
|
||||||
|
|
||||||
|
if !x.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for T.INV.2T".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(x.abs())
|
||||||
|
}
|
||||||
|
|
||||||
|
// T.TEST(array1, array2, tails, type)
|
||||||
|
pub(crate) fn fn_t_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 4 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let values1_opts = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
|
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => return error,
|
||||||
|
},
|
||||||
|
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in first array: {:?}", error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"First argument must be a range or array".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let values2_opts = match self.evaluate_node_in_context(&args[1], cell) {
|
||||||
|
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => return error,
|
||||||
|
},
|
||||||
|
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in second array: {:?}", error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Second argument must be a range or array".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let tails = match self.get_number(&args[2], cell) {
|
||||||
|
Ok(f) => {
|
||||||
|
let tf = f.trunc();
|
||||||
|
if tf == 1.0 {
|
||||||
|
TTestTails::OneTailed
|
||||||
|
} else if tf == 2.0 {
|
||||||
|
TTestTails::TwoTailed
|
||||||
|
} else {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"tails must be 1 or 2".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let test_type = match self.get_number(&args[3], cell) {
|
||||||
|
Ok(f) => {
|
||||||
|
let tf = f.trunc();
|
||||||
|
match tf {
|
||||||
|
1.0 => TTestType::Paired,
|
||||||
|
2.0 => TTestType::TwoSampleEqualVar,
|
||||||
|
3.0 => TTestType::TwoSampleUnequalVar,
|
||||||
|
_ => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"type must be 1, 2, or 3".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (values1, values2): (Vec<f64>, Vec<f64>) = if matches!(test_type, TTestType::Paired) {
|
||||||
|
values1_opts
|
||||||
|
.into_iter()
|
||||||
|
.zip(values2_opts)
|
||||||
|
.filter_map(|(o1, o2)| match (o1, o2) {
|
||||||
|
(Some(v1), Some(v2)) => Some((v1, v2)),
|
||||||
|
_ => None, // skip if either is None
|
||||||
|
})
|
||||||
|
.unzip()
|
||||||
|
} else {
|
||||||
|
// keep only numeric entries, ignore non-numeric (Option::None)
|
||||||
|
let v1: Vec<f64> = values1_opts.into_iter().flatten().collect();
|
||||||
|
let v2: Vec<f64> = values2_opts.into_iter().flatten().collect();
|
||||||
|
(v1, v2)
|
||||||
|
};
|
||||||
|
|
||||||
|
let n1 = values1.len();
|
||||||
|
let n2 = values2.len();
|
||||||
|
|
||||||
|
if n1 == 0 || n2 == 0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"T.TEST requires non-empty samples".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (t_stat, df) = match test_type {
|
||||||
|
TTestType::Paired => {
|
||||||
|
if n1 != n2 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NA,
|
||||||
|
cell,
|
||||||
|
"For paired T.TEST, both samples must have the same length".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if n1 < 2 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Paired T.TEST requires at least two pairs".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut diffs = Vec::with_capacity(n1);
|
||||||
|
for i in 0..n1 {
|
||||||
|
diffs.push(values1[i] - values2[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let nd = diffs.len();
|
||||||
|
let md = mean(&diffs);
|
||||||
|
let vd = sample_var(&diffs);
|
||||||
|
if vd <= 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Zero variance in paired T.TEST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let sd = vd.sqrt();
|
||||||
|
let t_stat = md / (sd / (nd as f64).sqrt());
|
||||||
|
let df = (nd - 1) as f64;
|
||||||
|
(t_stat, df)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2: two-sample, equal variance (homoscedastic)
|
||||||
|
TTestType::TwoSampleEqualVar => {
|
||||||
|
if n1 < 2 || n2 < 2 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Two-sample T.TEST type 2 requires at least two values in each sample"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let m1 = mean(&values1);
|
||||||
|
let m2 = mean(&values2);
|
||||||
|
let v1 = sample_var(&values1);
|
||||||
|
let v2 = sample_var(&values2);
|
||||||
|
|
||||||
|
let df_i = (n1 + n2 - 2) as i32;
|
||||||
|
if df_i <= 0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Degrees of freedom must be positive in T.TEST type 2".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let df = df_i as f64;
|
||||||
|
|
||||||
|
let sp2 = (((n1 - 1) as f64) * v1 + ((n2 - 1) as f64) * v2) / df; // pooled variance
|
||||||
|
|
||||||
|
if sp2 <= 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Zero pooled variance in T.TEST type 2".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let denom = (sp2 * (1.0 / (n1 as f64) + 1.0 / (n2 as f64))).sqrt();
|
||||||
|
if denom == 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Zero denominator in T.TEST type 2".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let t_stat = (m1 - m2) / denom;
|
||||||
|
(t_stat, df)
|
||||||
|
}
|
||||||
|
|
||||||
|
// two-sample, unequal variance (Welch)
|
||||||
|
TTestType::TwoSampleUnequalVar => {
|
||||||
|
if n1 < 2 || n2 < 2 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Two-sample T.TEST type 3 requires at least two values in each sample"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let m1 = mean(&values1);
|
||||||
|
let m2 = mean(&values2);
|
||||||
|
let v1 = sample_var(&values1);
|
||||||
|
let v2 = sample_var(&values2);
|
||||||
|
|
||||||
|
let s1n = v1 / (n1 as f64);
|
||||||
|
let s2n = v2 / (n2 as f64);
|
||||||
|
let denom = (s1n + s2n).sqrt();
|
||||||
|
if denom == 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Zero denominator in T.TEST type 3".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let t_stat = (m1 - m2) / denom;
|
||||||
|
|
||||||
|
let num_df = (s1n + s2n).powi(2);
|
||||||
|
let den_df = (s1n * s1n) / ((n1 - 1) as f64) + (s2n * s2n) / ((n2 - 1) as f64);
|
||||||
|
if den_df == 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Invalid degrees of freedom in T.TEST type 3".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let df = num_df / den_df;
|
||||||
|
(t_stat, df)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if df <= 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Degrees of freedom must be positive in T.TEST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match StudentsT::new(0.0, 1.0, df) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for Student's t distribution".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let t_abs = t_stat.abs();
|
||||||
|
let cdf = dist.cdf(t_abs);
|
||||||
|
|
||||||
|
let mut p = match tails {
|
||||||
|
TTestTails::OneTailed => 1.0 - cdf,
|
||||||
|
TTestTails::TwoTailed => 2.0 * (1.0 - cdf),
|
||||||
|
};
|
||||||
|
|
||||||
|
// clamp tiny fp noise
|
||||||
|
if p < 0.0 && p > -1e-15 {
|
||||||
|
p = 0.0;
|
||||||
|
}
|
||||||
|
if p > 1.0 && p < 1.0 + 1e-15 {
|
||||||
|
p = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
518
base/src/functions/statistical/variance.rs
Normal file
518
base/src/functions/statistical/variance.rs
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||||
|
use crate::expressions::parser::ArrayNode;
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub(crate) fn fn_var_p(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
let mut sumsq = 0.0;
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||||
|
*sum += value;
|
||||||
|
*sumsq += value * value;
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
match self.evaluate_node_in_context(arg, cell) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
CalcResult::Range { left, right } => {
|
||||||
|
if left.sheet != right.sheet {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let row1 = left.row;
|
||||||
|
let mut row2 = right.row;
|
||||||
|
let column1 = left.column;
|
||||||
|
let mut column2 = right.column;
|
||||||
|
|
||||||
|
if row1 == 1 && row2 == LAST_ROW {
|
||||||
|
row2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_row,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if column1 == 1 && column2 == LAST_COLUMN {
|
||||||
|
column2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_column,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in row1..row2 + 1 {
|
||||||
|
for column in column1..(column2 + 1) {
|
||||||
|
match self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: left.sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
}) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Array(array) => {
|
||||||
|
for row in array {
|
||||||
|
for value in row {
|
||||||
|
match value {
|
||||||
|
ArrayNode::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
ArrayNode::Error(error) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error,
|
||||||
|
origin: cell,
|
||||||
|
message: "Error in array".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"VAR.P with no numeric data".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = count as f64;
|
||||||
|
let mut var = (sumsq - (sum * sum) / n) / n;
|
||||||
|
|
||||||
|
if var < 0.0 && var > -1e-12 {
|
||||||
|
var = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(var)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_var_s(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
let mut sumsq = 0.0;
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||||
|
*sum += value;
|
||||||
|
*sumsq += value * value;
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
match self.evaluate_node_in_context(arg, cell) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
CalcResult::Range { left, right } => {
|
||||||
|
if left.sheet != right.sheet {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let row1 = left.row;
|
||||||
|
let mut row2 = right.row;
|
||||||
|
let column1 = left.column;
|
||||||
|
let mut column2 = right.column;
|
||||||
|
|
||||||
|
if row1 == 1 && row2 == LAST_ROW {
|
||||||
|
row2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_row,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if column1 == 1 && column2 == LAST_COLUMN {
|
||||||
|
column2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_column,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in row1..row2 + 1 {
|
||||||
|
for column in column1..(column2 + 1) {
|
||||||
|
match self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: left.sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
}) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Array(array) => {
|
||||||
|
for row in array {
|
||||||
|
for value in row {
|
||||||
|
match value {
|
||||||
|
ArrayNode::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
ArrayNode::Error(error) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error,
|
||||||
|
origin: cell,
|
||||||
|
message: "Error in array".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count <= 1 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"VAR.S requires at least two numeric values".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = count as f64;
|
||||||
|
let mut var = (sumsq - (sum * sum) / n) / (n - 1.0);
|
||||||
|
|
||||||
|
if var < 0.0 && var > -1e-12 {
|
||||||
|
var = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(var)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_vara(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
let mut sumsq = 0.0;
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||||
|
*sum += value;
|
||||||
|
*sumsq += value * value;
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
match self.evaluate_node_in_context(arg, cell) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
CalcResult::Range { left, right } => {
|
||||||
|
if left.sheet != right.sheet {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let row1 = left.row;
|
||||||
|
let mut row2 = right.row;
|
||||||
|
let column1 = left.column;
|
||||||
|
let mut column2 = right.column;
|
||||||
|
|
||||||
|
if row1 == 1 && row2 == LAST_ROW {
|
||||||
|
row2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_row,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if column1 == 1 && column2 == LAST_COLUMN {
|
||||||
|
column2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_column,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in row1..=row2 {
|
||||||
|
for column in column1..=column2 {
|
||||||
|
match self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: left.sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
}) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
CalcResult::String(_) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, 0.0);
|
||||||
|
}
|
||||||
|
CalcResult::Boolean(value) => {
|
||||||
|
let val = if value { 1.0 } else { 0.0 };
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, val);
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric for now (A semantics to be added)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Array(array) => {
|
||||||
|
for row in array {
|
||||||
|
for value in row {
|
||||||
|
match value {
|
||||||
|
ArrayNode::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
ArrayNode::Error(error) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error,
|
||||||
|
origin: cell,
|
||||||
|
message: "Error in array".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric for now (A semantics to be added)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric for now (A semantics to be added)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count <= 1 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"VARA requires at least two numeric values".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = count as f64;
|
||||||
|
let mut var = (sumsq - (sum * sum) / n) / (n - 1.0);
|
||||||
|
|
||||||
|
if var < 0.0 && var > -1e-12 {
|
||||||
|
var = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(var)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_varpa(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
let mut sumsq = 0.0;
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||||
|
*sum += value;
|
||||||
|
*sumsq += value * value;
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
match self.evaluate_node_in_context(arg, cell) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
CalcResult::Range { left, right } => {
|
||||||
|
if left.sheet != right.sheet {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let row1 = left.row;
|
||||||
|
let mut row2 = right.row;
|
||||||
|
let column1 = left.column;
|
||||||
|
let mut column2 = right.column;
|
||||||
|
|
||||||
|
if row1 == 1 && row2 == LAST_ROW {
|
||||||
|
row2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_row,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if column1 == 1 && column2 == LAST_COLUMN {
|
||||||
|
column2 = match self.workbook.worksheet(left.sheet) {
|
||||||
|
Ok(s) => s.dimension().max_column,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in row1..row2 + 1 {
|
||||||
|
for column in column1..(column2 + 1) {
|
||||||
|
match self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: left.sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
}) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
CalcResult::String(_) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, 0.0);
|
||||||
|
}
|
||||||
|
CalcResult::Boolean(value) => {
|
||||||
|
let val = if value { 1.0 } else { 0.0 };
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, val);
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Array(array) => {
|
||||||
|
for row in array {
|
||||||
|
for value in row {
|
||||||
|
match value {
|
||||||
|
ArrayNode::Number(value) => {
|
||||||
|
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||||
|
}
|
||||||
|
ArrayNode::Error(error) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error,
|
||||||
|
origin: cell,
|
||||||
|
message: "Error in array".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
// ignore non-numeric for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"VARPA with no numeric data".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = count as f64;
|
||||||
|
let mut var = (sumsq - (sum * sum) / n) / n;
|
||||||
|
|
||||||
|
if var < 0.0 && var > -1e-12 {
|
||||||
|
var = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(var)
|
||||||
|
}
|
||||||
|
}
|
||||||
71
base/src/functions/statistical/weibull.rs
Normal file
71
base/src/functions/statistical/weibull.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
use statrs::distribution::{Continuous, ContinuousCDF, Weibull};
|
||||||
|
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// WEIBULL.DIST(x, alpha, beta, cumulative)
|
||||||
|
pub(crate) fn fn_weibull_dist(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> CalcResult {
|
||||||
|
if args.len() != 4 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let alpha = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let beta = match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cumulative = match self.get_boolean(&args[3], cell) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if x < 0.0 || alpha <= 0.0 || beta <= 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for WEIBULL.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// statrs::Weibull: shape = k (alpha), scale = lambda (beta)
|
||||||
|
let dist = match Weibull::new(alpha, beta) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid parameters for WEIBULL.DIST".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
|
||||||
|
|
||||||
|
if !result.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for WEIBULL.DIST".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
171
base/src/functions/statistical/z_test.rs
Normal file
171
base/src/functions/statistical/z_test.rs
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
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 {
|
||||||
|
// Z.TEST(array, x, [sigma])
|
||||||
|
pub(crate) fn fn_z_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
// 2 or 3 arguments
|
||||||
|
if args.len() < 2 || args.len() > 3 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let array_arg = self.evaluate_node_in_context(&args[0], cell);
|
||||||
|
|
||||||
|
// Flatten first argument into Vec<Option<f64>> (numeric / non-numeric)
|
||||||
|
let values = match array_arg {
|
||||||
|
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => return error,
|
||||||
|
},
|
||||||
|
CalcResult::Array(array) => match self.values_from_array(array) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in array argument: {:?}", error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CalcResult::Number(v) => vec![Some(v)],
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Z.TEST first argument must be a range or array".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Collect basic stats on numeric entries
|
||||||
|
let mut sum = 0.0;
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
|
||||||
|
for x in values.iter().flatten() {
|
||||||
|
sum += x;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Excel: if array has no numeric values -> #N/A
|
||||||
|
if count == 0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NA,
|
||||||
|
cell,
|
||||||
|
"Z.TEST array has no numeric data".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = count as f64;
|
||||||
|
let mean = sum / n;
|
||||||
|
|
||||||
|
// x argument (hypothesized population mean)
|
||||||
|
let x_value = match self.evaluate_node_in_context(&args[1], cell) {
|
||||||
|
CalcResult::Number(v) => v,
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Z.TEST second argument (x) must be numeric".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optional sigma
|
||||||
|
let mut sigma: Option<f64> = None;
|
||||||
|
if args.len() == 3 {
|
||||||
|
match self.evaluate_node_in_context(&args[2], cell) {
|
||||||
|
CalcResult::Number(v) => {
|
||||||
|
if v == 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Z.TEST sigma cannot be zero".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
sigma = Some(v);
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return error,
|
||||||
|
_ => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Z.TEST sigma (third argument) must be numeric".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If sigma omitted, use sample standard deviation STDEV(array)
|
||||||
|
let sigma_value = if let Some(s) = sigma {
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
// Excel: if only one numeric value and sigma omitted -> #DIV/0!
|
||||||
|
if count <= 1 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Z.TEST requires at least two values when sigma is omitted".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute sum of squared deviations
|
||||||
|
let mut sumsq_dev = 0.0;
|
||||||
|
for x in values.iter().flatten() {
|
||||||
|
let d = x - mean;
|
||||||
|
sumsq_dev += d * d;
|
||||||
|
}
|
||||||
|
|
||||||
|
let var = sumsq_dev / (n - 1.0);
|
||||||
|
if var <= 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Z.TEST standard deviation is zero".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var.sqrt()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compute z statistic: (mean - x) / (sigma / sqrt(n))
|
||||||
|
let denom = sigma_value / n.sqrt();
|
||||||
|
if denom == 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Z.TEST denominator is zero".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let z = (mean - x_value) / denom;
|
||||||
|
|
||||||
|
// Standard normal CDF
|
||||||
|
let dist = match Normal::new(0.0, 1.0) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Cannot create standard normal distribution in Z.TEST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut p = 1.0 - dist.cdf(z);
|
||||||
|
|
||||||
|
// clamp tiny FP noise
|
||||||
|
if p < 0.0 && p > -1e-15 {
|
||||||
|
p = 0.0;
|
||||||
|
}
|
||||||
|
if p > 1.0 && p < 1.0 + 1e-15 {
|
||||||
|
p = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
mod test_actions;
|
mod test_actions;
|
||||||
|
mod test_arabic_roman;
|
||||||
mod test_binary_search;
|
mod test_binary_search;
|
||||||
mod test_cell;
|
mod test_cell;
|
||||||
mod test_cell_clear_contents;
|
mod test_cell_clear_contents;
|
||||||
@@ -39,12 +40,14 @@ mod test_metadata;
|
|||||||
mod test_model_cell_clear_all;
|
mod test_model_cell_clear_all;
|
||||||
mod test_model_is_empty_cell;
|
mod test_model_is_empty_cell;
|
||||||
mod test_move_formula;
|
mod test_move_formula;
|
||||||
|
mod test_mround_trunc_int;
|
||||||
mod test_quote_prefix;
|
mod test_quote_prefix;
|
||||||
mod test_row_column_styles;
|
mod test_row_column_styles;
|
||||||
mod test_set_user_input;
|
mod test_set_user_input;
|
||||||
mod test_sheet_markup;
|
mod test_sheet_markup;
|
||||||
mod test_sheets;
|
mod test_sheets;
|
||||||
mod test_styles;
|
mod test_styles;
|
||||||
|
mod test_sumsq;
|
||||||
mod test_trigonometric;
|
mod test_trigonometric;
|
||||||
mod test_true_false;
|
mod test_true_false;
|
||||||
mod test_weekday_return_types;
|
mod test_weekday_return_types;
|
||||||
@@ -55,12 +58,18 @@ mod test_yearfrac_basis;
|
|||||||
pub(crate) mod util;
|
pub(crate) mod util;
|
||||||
|
|
||||||
mod engineering;
|
mod engineering;
|
||||||
|
mod statistical;
|
||||||
mod test_fn_offset;
|
mod test_fn_offset;
|
||||||
mod test_number_format;
|
mod test_number_format;
|
||||||
|
|
||||||
mod test_arrays;
|
mod test_arrays;
|
||||||
|
mod test_combin_combina;
|
||||||
mod test_escape_quotes;
|
mod test_escape_quotes;
|
||||||
|
mod test_even_odd;
|
||||||
|
mod test_exp_sign;
|
||||||
mod test_extend;
|
mod test_extend;
|
||||||
|
mod test_floor;
|
||||||
|
mod test_fn_datevalue_timevalue;
|
||||||
mod test_fn_fv;
|
mod test_fn_fv;
|
||||||
mod test_fn_round;
|
mod test_fn_round;
|
||||||
mod test_fn_type;
|
mod test_fn_type;
|
||||||
@@ -80,5 +89,6 @@ mod test_percentage;
|
|||||||
mod test_set_functions_error_handling;
|
mod test_set_functions_error_handling;
|
||||||
mod test_sheet_names;
|
mod test_sheet_names;
|
||||||
mod test_today;
|
mod test_today;
|
||||||
|
mod test_trigonometric_reciprocals;
|
||||||
mod test_types;
|
mod test_types;
|
||||||
mod user_model;
|
mod user_model;
|
||||||
|
|||||||
24
base/src/test/statistical/mod.rs
Normal file
24
base/src/test/statistical/mod.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
mod test_fn_avedev;
|
||||||
|
mod test_fn_binom;
|
||||||
|
mod test_fn_chisq;
|
||||||
|
mod test_fn_chisq_test;
|
||||||
|
mod test_fn_confidence;
|
||||||
|
mod test_fn_covariance;
|
||||||
|
mod test_fn_devsq;
|
||||||
|
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;
|
||||||
|
mod test_fn_pearson;
|
||||||
|
mod test_fn_phi;
|
||||||
|
mod test_fn_poisson;
|
||||||
|
mod test_fn_stdev;
|
||||||
|
mod test_fn_t_dist;
|
||||||
|
mod test_fn_t_test;
|
||||||
|
mod test_fn_var;
|
||||||
|
mod test_fn_weibull;
|
||||||
|
mod test_fn_z_test;
|
||||||
40
base/src/test/statistical/test_fn_avedev.rs
Normal file
40
base/src/test/statistical/test_fn_avedev.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn smoke_test() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=STDEV.P(10, 12, 23, 23, 16, 23, 21)");
|
||||||
|
model._set("A2", "=STDEV.S(10, 12, 23, 23, 16, 23, 21)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"5.174505793");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A2"), *"5.589105048");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn numbers() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
model._set("A2", "24");
|
||||||
|
model._set("A3", "25");
|
||||||
|
model._set("A4", "27");
|
||||||
|
model._set("A5", "23");
|
||||||
|
model._set("A6", "45");
|
||||||
|
model._set("A7", "23.5");
|
||||||
|
model._set("A8", "34");
|
||||||
|
model._set("A9", "23");
|
||||||
|
model._set("A10", "23");
|
||||||
|
model._set("A11", "TRUE");
|
||||||
|
model._set("A12", "'23");
|
||||||
|
model._set("A13", "Text");
|
||||||
|
model._set("A14", "FALSE");
|
||||||
|
model._set("A15", "45");
|
||||||
|
|
||||||
|
model._set("B1", "=AVEDEV(A2:A15)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("B1"), *"7.25");
|
||||||
|
}
|
||||||
86
base/src/test/statistical/test_fn_binom.rs
Normal file
86
base/src/test/statistical/test_fn_binom.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_binom_dist_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=BINOM.DIST(6, 10, 0.5, TRUE)");
|
||||||
|
model._set("A2", "=BINOM.DIST(6, 10, 0.5, FALSE)");
|
||||||
|
model._set("A3", "=BINOM.DIST(6, 10, 0.5)"); // wrong args
|
||||||
|
model._set("A4", "=BINOM.DIST(6, 10, 0.5, TRUE, FALSE)"); // too many args
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// P(X <= 6) for X ~ Bin(10, 0.5) = 0.828125
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.828125");
|
||||||
|
|
||||||
|
// P(X = 6) for X ~ Bin(10, 0.5) = 0.205078125
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.205078125");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_binom_dist_range_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=BINOM.DIST.RANGE(60, 0.75, 48)");
|
||||||
|
model._set("A2", "=BINOM.DIST.RANGE(60, 0.75, 45, 50)");
|
||||||
|
model._set("A3", "=BINOM.DIST.RANGE(60, 1.2, 45, 50)"); // p > 1 -> #NUM!
|
||||||
|
model._set("A4", "=BINOM.DIST.RANGE(60, 0.75, 50, 45)"); // lower > upper -> #NUM!");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.083974967");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.523629793");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_binom_inv_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=BINOM.INV(6, 0.5, 0.75)");
|
||||||
|
model._set("A2", "=BINOM.INV(6, 0.5, -0.1)"); // alpha < 0 -> #NUM!
|
||||||
|
model._set("A3", "=BINOM.INV(6, 1.2, 0.75)"); // p > 1 -> #NUM!
|
||||||
|
model._set("A4", "=BINOM.INV(6, 0.5)"); // args error
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"4");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_negbinom_dist_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid: PMF (non-cumulative) and CDF (cumulative)
|
||||||
|
model._set("A1", "=NEGBINOM.DIST(10, 5, 0.25, FALSE)");
|
||||||
|
model._set("A2", "=NEGBINOM.DIST(10, 5, 0.25, TRUE)");
|
||||||
|
|
||||||
|
// Wrong number of arguments -> #ERROR!
|
||||||
|
model._set("A3", "=NEGBINOM.DIST(10, 5, 0.25)");
|
||||||
|
model._set("A4", "=NEGBINOM.DIST(10, 5, 0.25, TRUE, FALSE)");
|
||||||
|
|
||||||
|
// Domain errors:
|
||||||
|
// p < 0 or p > 1 -> #NUM!
|
||||||
|
model._set("A5", "=NEGBINOM.DIST(10, 5, 1.5, TRUE)");
|
||||||
|
// number_f < 0 -> #NUM!
|
||||||
|
model._set("A6", "=NEGBINOM.DIST(-1, 5, 0.25, TRUE)");
|
||||||
|
// number_s < 1 -> #NUM!
|
||||||
|
model._set("A7", "=NEGBINOM.DIST(10, 0, 0.25, TRUE)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.05504866");
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.313514058");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||||
|
}
|
||||||
140
base/src/test/statistical/test_fn_chisq.rs
Normal file
140
base/src/test/statistical/test_fn_chisq.rs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_chisq_dist_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid: CDF
|
||||||
|
model._set("A1", "=CHISQ.DIST(0.5, 4, TRUE)");
|
||||||
|
|
||||||
|
// Valid: PDF
|
||||||
|
model._set("A2", "=CHISQ.DIST(0.5, 4, FALSE)");
|
||||||
|
|
||||||
|
// Valid: CDF with numeric cumulative (1 -> TRUE)
|
||||||
|
model._set("A3", "=CHISQ.DIST(0.5, 4, 1)");
|
||||||
|
|
||||||
|
// Wrong number of args -> #ERROR!
|
||||||
|
model._set("A4", "=CHISQ.DIST(0.5, 4)");
|
||||||
|
model._set("A5", "=CHISQ.DIST(0.5, 4, TRUE, FALSE)");
|
||||||
|
|
||||||
|
// Domain errors
|
||||||
|
// x < 0 -> #NUM!
|
||||||
|
model._set("A6", "=CHISQ.DIST(-1, 4, TRUE)");
|
||||||
|
// deg_freedom < 1 -> #NUM!
|
||||||
|
model._set("A7", "=CHISQ.DIST(0.5, 0, TRUE)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// Values for df = 4
|
||||||
|
// CDF(0.5) ≈ 0.026499021, PDF(0.5) ≈ 0.097350098
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.026499021");
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.097350098");
|
||||||
|
assert_eq!(model._get_text("A3"), *"0.026499021");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_chisq_dist_rt_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid calls
|
||||||
|
model._set("A1", "=CHISQ.DIST.RT(0.5, 4)");
|
||||||
|
model._set("A2", "=CHISQ.DIST.RT(5, 4)");
|
||||||
|
|
||||||
|
// Too few / too many args -> #ERROR!
|
||||||
|
model._set("A3", "=CHISQ.DIST.RT(0.5)");
|
||||||
|
model._set("A4", "=CHISQ.DIST.RT(0.5, 4, 1)");
|
||||||
|
|
||||||
|
// Domain errors
|
||||||
|
// x < 0 -> #NUM!
|
||||||
|
model._set("A5", "=CHISQ.DIST.RT(-1, 4)");
|
||||||
|
// deg_freedom < 1 -> #NUM!
|
||||||
|
model._set("A6", "=CHISQ.DIST.RT(0.5, 0)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// For df = 4:
|
||||||
|
// right tail at 0.5 ≈ 0.973500979
|
||||||
|
// right tail at 5.0 ≈ 0.287297495
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.973500979");
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.287297495");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_chisq_inv_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid calls
|
||||||
|
model._set("A1", "=CHISQ.INV(0.95, 4)");
|
||||||
|
model._set("A2", "=CHISQ.INV(0.1, 10)");
|
||||||
|
|
||||||
|
// Wrong number of args -> #ERROR!
|
||||||
|
model._set("A3", "=CHISQ.INV(0.95)");
|
||||||
|
model._set("A4", "=CHISQ.INV(0.95, 4, 1)");
|
||||||
|
|
||||||
|
// Domain errors
|
||||||
|
// probability < 0 or > 1 -> #NUM!
|
||||||
|
model._set("A5", "=CHISQ.INV(-0.1, 4)");
|
||||||
|
model._set("A6", "=CHISQ.INV(1.1, 4)");
|
||||||
|
// deg_freedom < 1 -> #NUM!
|
||||||
|
model._set("A7", "=CHISQ.INV(0.5, 0)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// Standard critical values:
|
||||||
|
// CHISQ.INV(0.95, 4) ≈ 9.487729037
|
||||||
|
// CHISQ.INV(0.1, 10) ≈ 4.865182052
|
||||||
|
assert_eq!(model._get_text("A1"), *"9.487729037");
|
||||||
|
assert_eq!(model._get_text("A2"), *"4.865182052");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_chisq_inv_rt_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid calls
|
||||||
|
model._set("A1", "=CHISQ.INV.RT(0.05, 4)");
|
||||||
|
model._set("A2", "=CHISQ.INV.RT(0.9, 10)");
|
||||||
|
|
||||||
|
// Wrong number of args -> #ERROR!
|
||||||
|
model._set("A3", "=CHISQ.INV.RT(0.05)");
|
||||||
|
model._set("A4", "=CHISQ.INV.RT(0.05, 4, 1)");
|
||||||
|
|
||||||
|
// Domain errors
|
||||||
|
// probability < 0 or > 1 -> #NUM!
|
||||||
|
model._set("A5", "=CHISQ.INV.RT(-0.1, 4)");
|
||||||
|
model._set("A6", "=CHISQ.INV.RT(1.1, 4)");
|
||||||
|
// deg_freedom < 1 -> #NUM!
|
||||||
|
model._set("A7", "=CHISQ.INV.RT(0.5, 0)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// For chi-square:
|
||||||
|
// CHISQ.INV.RT(0.05, 4) = CHISQ.INV(0.95, 4) ≈ 9.487729037
|
||||||
|
// CHISQ.INV.RT(0.9, 10) = CHISQ.INV(0.1, 10) ≈ 4.865182052
|
||||||
|
assert_eq!(model._get_text("A1"), *"9.487729037");
|
||||||
|
assert_eq!(model._get_text("A2"), *"4.865182052");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||||
|
}
|
||||||
127
base/src/test/statistical/test_fn_chisq_test.rs
Normal file
127
base/src/test/statistical/test_fn_chisq_test.rs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_chisq_test_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A2", "48");
|
||||||
|
model._set("A3", "32");
|
||||||
|
model._set("A4", "12");
|
||||||
|
model._set("A5", "1");
|
||||||
|
model._set("A6", "'13");
|
||||||
|
model._set("A7", "TRUE");
|
||||||
|
model._set("A8", "1");
|
||||||
|
model._set("A9", "13");
|
||||||
|
model._set("A10", "15");
|
||||||
|
|
||||||
|
model._set("B2", "55");
|
||||||
|
model._set("B3", "34");
|
||||||
|
model._set("B4", "13");
|
||||||
|
model._set("B5", "blah");
|
||||||
|
model._set("B6", "13");
|
||||||
|
model._set("B7", "1");
|
||||||
|
model._set("B8", "TRUE");
|
||||||
|
model._set("B9", "'14");
|
||||||
|
model._set("B10", "16");
|
||||||
|
|
||||||
|
model._set("C1", "=CHISQ.TEST(A2:A10, B2:B10)");
|
||||||
|
model.evaluate();
|
||||||
|
assert_eq!(model._get_text("C1"), *"0.997129538");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrays() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A2", "TRUE");
|
||||||
|
model._set("A3", "4");
|
||||||
|
model._set("A4", "'3");
|
||||||
|
model._set("B2", "2");
|
||||||
|
model._set("B3", "2");
|
||||||
|
model._set("B4", "2");
|
||||||
|
model._set("C1", "=CHISQ.TEST(A2:A4, B2:B4)");
|
||||||
|
|
||||||
|
model._set("G5", "=CHISQ.TEST({TRUE,4,\"3\"}, {2,2,2})");
|
||||||
|
|
||||||
|
// 1D arrays with different shapes
|
||||||
|
model._set("G6", "=CHISQ.TEST({1,2,3}, {3;3;4})");
|
||||||
|
|
||||||
|
// 2D array
|
||||||
|
model._set("G7", "=CHISQ.TEST({1,2;3,4},{2,3;2,2})");
|
||||||
|
|
||||||
|
// 1D arrays with same shape
|
||||||
|
model._set("G8", "=CHISQ.TEST({1,2,3,4}, {2,3,4,5})");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
assert_eq!(model._get_text("C1"), *"0.367879441");
|
||||||
|
assert_eq!(model._get_text("G5"), *"0.367879441");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("G6"), *"0.383531573");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("G7"), *"0.067889155");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("G8"), *"0.733094495");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn more_arrays() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("V20", "2");
|
||||||
|
model._set("V21", "4");
|
||||||
|
model._set("W20", "3");
|
||||||
|
model._set("W21", "5");
|
||||||
|
model._set("C1", "=CHISQ.TEST({1,2;3,4},V20:W21)");
|
||||||
|
model._set("C2", "=CHISQ.TEST({1,2;3,4}, {2,3;4,5})");
|
||||||
|
model.evaluate();
|
||||||
|
assert_eq!(model._get_text("C1"), *"0.257280177");
|
||||||
|
assert_eq!(model._get_text("C2"), *"0.257280177");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn array_ranges() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A2", "TRUE");
|
||||||
|
model._set("A3", "4");
|
||||||
|
model._set("A4", "'3");
|
||||||
|
model._set("B2", "2");
|
||||||
|
model._set("B3", "2");
|
||||||
|
model._set("B4", "2");
|
||||||
|
model._set("C1", "=CHISQ.TEST(A2:A4, {2;2;2})");
|
||||||
|
|
||||||
|
model._set("G5", "=CHISQ.TEST({TRUE;4;\"3\"}, B2:B4)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("C1"), *"0.367879441");
|
||||||
|
assert_eq!(model._get_text("G5"), *"0.367879441");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn array_2d_ranges() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A2", "2");
|
||||||
|
model._set("B2", "3");
|
||||||
|
model._set("C2", "4");
|
||||||
|
model._set("A3", "5");
|
||||||
|
model._set("B3", "6");
|
||||||
|
model._set("C3", "7");
|
||||||
|
model._set("G1", "=CHISQ.TEST({1,2,3;4,2,6}, A2:C3)");
|
||||||
|
model.evaluate();
|
||||||
|
assert_eq!(model._get_text("G1"), *"0.129195493");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ranges_1d() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A2", "1");
|
||||||
|
model._set("A3", "2");
|
||||||
|
model._set("A4", "3");
|
||||||
|
model._set("B2", "4");
|
||||||
|
model._set("C2", "5");
|
||||||
|
model._set("D2", "6");
|
||||||
|
model._set("G1", "=CHISQ.TEST(A2:A4, B2:D2)");
|
||||||
|
model._set("G2", "=CHISQ.TEST(B2:D2, A2:A4)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
assert_eq!(model._get_text("G1"), *"0.062349477");
|
||||||
|
assert_eq!(model._get_text("G2"), *"0.000261259");
|
||||||
|
}
|
||||||
51
base/src/test/statistical/test_fn_confidence.rs
Normal file
51
base/src/test/statistical/test_fn_confidence.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_confidence_norm_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
model._set("A1", "=CONFIDENCE.NORM(0.05, 2.5, 50)");
|
||||||
|
|
||||||
|
// Some edge/error cases
|
||||||
|
model._set("A2", "=CONFIDENCE.NORM(0, 2.5, 50)"); // alpha <= 0 -> #NUM!
|
||||||
|
model._set("A3", "=CONFIDENCE.NORM(1, 2.5, 50)"); // alpha >= 1 -> #NUM!
|
||||||
|
model._set("A4", "=CONFIDENCE.NORM(0.05, -1, 50)"); // std_dev <=0 -> #NUM!
|
||||||
|
model._set("A5", "=CONFIDENCE.NORM(0.05, 2.5, 1)");
|
||||||
|
model._set("A6", "=CONFIDENCE.NORM(0.05, 2.5, 0.99)"); // size < 1 -> #NUM!
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.692951912");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"4.899909961");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_confidence_t_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
model._set("A1", "=CONFIDENCE.T(0.05, 50000, 100)");
|
||||||
|
|
||||||
|
// Some edge/error cases
|
||||||
|
model._set("A2", "=CONFIDENCE.T(0, 50000, 100)"); // alpha <= 0 -> #NUM!
|
||||||
|
model._set("A3", "=CONFIDENCE.T(1, 50000, 100)"); // alpha >= 1 -> #NUM!
|
||||||
|
model._set("A4", "=CONFIDENCE.T(0.05, -1, 100)");
|
||||||
|
model._set("A5", "=CONFIDENCE.T(0.05, 50000, 1)");
|
||||||
|
model._set("A6", "=CONFIDENCE.T(0.05, 50000, 1.7)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"9921.08475793");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#DIV/0!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#DIV/0!");
|
||||||
|
}
|
||||||
57
base/src/test/statistical/test_fn_covariance.rs
Normal file
57
base/src/test/statistical/test_fn_covariance.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_covariance_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "3");
|
||||||
|
model._set("A2", "9");
|
||||||
|
model._set("A3", "2");
|
||||||
|
model._set("A4", "7");
|
||||||
|
model._set("A5", "4");
|
||||||
|
model._set("A6", "12");
|
||||||
|
|
||||||
|
model._set("B1", "5");
|
||||||
|
model._set("B2", "15");
|
||||||
|
model._set("B3", "6");
|
||||||
|
model._set("B4", "17");
|
||||||
|
model._set("B5", "8");
|
||||||
|
model._set("B6", "20");
|
||||||
|
|
||||||
|
model._set("C1", "=COVARIANCE.P(A1:A6, B1:B6)");
|
||||||
|
model._set("C2", "=COVARIANCE.S(A1:A6, B1:B6)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("C1"), *"19.194444444");
|
||||||
|
assert_eq!(model._get_text("C2"), *"23.033333333");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrays_mixed() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
model._set("A2", "2");
|
||||||
|
model._set("A3", "4");
|
||||||
|
model._set("A4", "6");
|
||||||
|
model._set("A5", "8");
|
||||||
|
|
||||||
|
model._set("B2", "1");
|
||||||
|
model._set("B3", "3");
|
||||||
|
model._set("B4", "5");
|
||||||
|
model._set("B5", "7");
|
||||||
|
|
||||||
|
model._set("C1", "=COVARIANCE.P(A2:A5, {1,3,5,7})");
|
||||||
|
model._set("C2", "=COVARIANCE.S(A2:A5, {1,3,5,7})");
|
||||||
|
model._set("C3", "=COVARIANCE.P(A2:A5, B2:B5)");
|
||||||
|
model._set("C4", "=COVARIANCE.S(A2:A5, B2:B5)");
|
||||||
|
model._set("C5", "=COVARIANCE.P({2,4,6,8}, B2:B5)");
|
||||||
|
model._set("C6", "=COVARIANCE.S({2,4,6,8}, B2:B5)");
|
||||||
|
model._set("C7", "=COVARIANCE.P({2,4,6,8}, {1,3,5,7})");
|
||||||
|
model._set("C8", "=COVARIANCE.S({2,4,6,8}, {1,3,5,7})");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("C1"), *"5");
|
||||||
|
assert_eq!(model._get_text("C2"), *"6.666666667");
|
||||||
|
}
|
||||||
50
base/src/test/statistical/test_fn_devsq.rs
Normal file
50
base/src/test/statistical/test_fn_devsq.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arguments_smoke_test() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=DEVSQ()");
|
||||||
|
model._set("A2", "=DEVSQ(1, 2, 3)");
|
||||||
|
model._set("A3", "=DEVSQ(1, )");
|
||||||
|
model._set("A4", "=DEVSQ(1, , 3)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A2"), *"2");
|
||||||
|
assert_eq!(model._get_text("A3"), *"0");
|
||||||
|
assert_eq!(model._get_text("A4"), *"2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ranges() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=DEVSQ(A2:A8)");
|
||||||
|
model._set("A2", "4");
|
||||||
|
model._set("A3", "5");
|
||||||
|
model._set("A4", "8");
|
||||||
|
model._set("A5", "7");
|
||||||
|
model._set("A6", "11");
|
||||||
|
model._set("A7", "4");
|
||||||
|
model._set("A8", "3");
|
||||||
|
model.evaluate();
|
||||||
|
assert_eq!(model._get_text("A1"), *"48");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrays() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=DEVSQ({1, 2, 3})");
|
||||||
|
model._set("A2", "=DEVSQ({1; 2; 3})");
|
||||||
|
model._set("A3", "=DEVSQ({1, 2; 3, 4})");
|
||||||
|
model._set("A4", "=DEVSQ({1, 2; 3, 4; 5, 6})");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"2");
|
||||||
|
assert_eq!(model._get_text("A2"), *"2");
|
||||||
|
assert_eq!(model._get_text("A3"), *"5");
|
||||||
|
assert_eq!(model._get_text("A4"), *"17.5");
|
||||||
|
}
|
||||||
32
base/src/test/statistical/test_fn_expon_dist.rs
Normal file
32
base/src/test/statistical/test_fn_expon_dist.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_expon_dist_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// λ = 1, x = 0.5
|
||||||
|
// CDF = 1 - e^-0.5 ≈ 0.393469340
|
||||||
|
// PDF = e^-0.5 ≈ 0.606530660
|
||||||
|
model._set("A1", "=EXPON.DIST(0.5, 1, TRUE)");
|
||||||
|
model._set("A2", "=EXPON.DIST(0.5, 1, FALSE)");
|
||||||
|
|
||||||
|
// Wrong number of args
|
||||||
|
model._set("A3", "=EXPON.DIST(0.5, 1)");
|
||||||
|
model._set("A4", "=EXPON.DIST(0.5, 1, TRUE, FALSE)");
|
||||||
|
|
||||||
|
// Domain errors
|
||||||
|
model._set("A5", "=EXPON.DIST(-1, 1, TRUE)"); // x < 0
|
||||||
|
model._set("A6", "=EXPON.DIST(0.5, 0, TRUE)"); // lambda <= 0
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.39346934");
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.60653066");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
}
|
||||||
75
base/src/test/statistical/test_fn_f.rs
Normal file
75
base/src/test/statistical/test_fn_f.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_f_dist_sanity() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=F.DIST(15, 6, 4, TRUE)");
|
||||||
|
model._set("A2", "=F.DIST(15, 6, 4, FALSE)");
|
||||||
|
model._set("A3", "=F.DIST(15, 6, 4)");
|
||||||
|
model._set("A4", "=F.DIST(15, 6, 4, TRUE, FALSE)");
|
||||||
|
model.evaluate();
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.989741952");
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.001271447");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_f_dist_rt_sanity() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid call
|
||||||
|
model._set("A1", "=F.DIST.RT(15, 6, 4)");
|
||||||
|
// Too few args
|
||||||
|
model._set("A2", "=F.DIST.RT(15, 6)");
|
||||||
|
// Too many args
|
||||||
|
model._set("A3", "=F.DIST.RT(15, 6, 4, 1)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.010258048");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_f_inv_sanity() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid call: left-tail inverse
|
||||||
|
model._set("A1", "=F.INV(0.9897419523940, 6, 4)");
|
||||||
|
|
||||||
|
// Too many args
|
||||||
|
model._set("A2", "=F.INV(0.5, 6, 4, 2)");
|
||||||
|
|
||||||
|
// Too few args
|
||||||
|
model._set("A3", "=F.INV(0.5, 6)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"15");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_f_inv_rt_sanity() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid call: left-tail inverse
|
||||||
|
model._set("A1", "=F.INV.RT(0.0102580476059808, 6, 4)");
|
||||||
|
|
||||||
|
// Too many args
|
||||||
|
model._set("A2", "=F.INV.RT(0.5, 6, 4, 2)");
|
||||||
|
|
||||||
|
// Too few args
|
||||||
|
model._set("A3", "=F.INV.RT(0.5, 6)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"15");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
}
|
||||||
35
base/src/test/statistical/test_fn_f_test.rs
Normal file
35
base/src/test/statistical/test_fn_f_test.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_f_test_sanity() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid call
|
||||||
|
model._set("A1", "=F.TEST(A2:A7, B2:B7)");
|
||||||
|
model._set("A2", "9");
|
||||||
|
model._set("A3", "12");
|
||||||
|
model._set("A4", "14");
|
||||||
|
model._set("A5", "16");
|
||||||
|
model._set("A6", "18");
|
||||||
|
model._set("A7", "20");
|
||||||
|
model._set("B2", "11");
|
||||||
|
model._set("B3", "10");
|
||||||
|
model._set("B4", "15");
|
||||||
|
model._set("B5", "17");
|
||||||
|
model._set("B6", "19");
|
||||||
|
model._set("B7", "21");
|
||||||
|
|
||||||
|
// Too few args
|
||||||
|
model._set("A8", "=F.TEST(A2:A7)");
|
||||||
|
|
||||||
|
// Too many args
|
||||||
|
model._set("A9", "=F.TEST(A2:A7, B2:B7, C2:C7)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.859284302");
|
||||||
|
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A9"), *"#ERROR!");
|
||||||
|
}
|
||||||
53
base/src/test/statistical/test_fn_fisher.rs
Normal file
53
base/src/test/statistical/test_fn_fisher.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
#[test]
|
||||||
|
fn test_fn_fisher_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid inputs
|
||||||
|
model._set("A1", "=FISHER(0.1)");
|
||||||
|
model._set("A2", "=FISHER(-0.5)");
|
||||||
|
model._set("A3", "=FISHER(0.8)");
|
||||||
|
|
||||||
|
// Domain errors: x <= -1 or x >= 1 -> #NUM!
|
||||||
|
model._set("A4", "=FISHER(1)");
|
||||||
|
model._set("A5", "=FISHER(-1)");
|
||||||
|
model._set("A6", "=FISHER(2)");
|
||||||
|
|
||||||
|
// Wrong number of arguments -> #ERROR!
|
||||||
|
model._set("A7", "=FISHER(0.1, 2)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.100335348");
|
||||||
|
assert_eq!(model._get_text("A2"), *"-0.549306144");
|
||||||
|
assert_eq!(model._get_text("A3"), *"1.098612289");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_fisher_inv_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid inputs
|
||||||
|
model._set("A1", "=FISHERINV(-1.5)");
|
||||||
|
model._set("A2", "=FISHERINV(0.5)");
|
||||||
|
model._set("A3", "=FISHERINV(2)");
|
||||||
|
|
||||||
|
// Wrong number of arguments -> #ERROR!
|
||||||
|
model._set("A4", "=FISHERINV(0.5, 1)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"-0.905148254");
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.462117157");
|
||||||
|
assert_eq!(model._get_text("A3"), *"0.96402758");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
}
|
||||||
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!");
|
||||||
|
}
|
||||||
42
base/src/test/statistical/test_fn_hyp_geom_dist.rs
Normal file
42
base/src/test/statistical/test_fn_hyp_geom_dist.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_hyp_geom_dist_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid: PDF (non-cumulative)
|
||||||
|
model._set("A1", "=HYPGEOM.DIST(1, 4, 12, 20, FALSE)");
|
||||||
|
|
||||||
|
// Valid: CDF (cumulative)
|
||||||
|
model._set("A2", "=HYPGEOM.DIST(1, 4, 12, 20, TRUE)");
|
||||||
|
|
||||||
|
// Wrong number of arguments -> #ERROR!
|
||||||
|
model._set("A3", "=HYPGEOM.DIST(1, 4, 12, 20)");
|
||||||
|
model._set("A4", "=HYPGEOM.DIST(1, 4, 12, 20, TRUE, FALSE)");
|
||||||
|
|
||||||
|
// Domain errors:
|
||||||
|
// sample_s > number_sample -> #NUM!
|
||||||
|
model._set("A5", "=HYPGEOM.DIST(5, 4, 12, 20, TRUE)");
|
||||||
|
|
||||||
|
// population_s > number_pop -> #NUM!
|
||||||
|
model._set("A6", "=HYPGEOM.DIST(1, 4, 25, 20, TRUE)");
|
||||||
|
|
||||||
|
// number_sample > number_pop -> #NUM!
|
||||||
|
model._set("A7", "=HYPGEOM.DIST(1, 25, 12, 20, TRUE)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// PDF: P(X = 1)
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.13869969");
|
||||||
|
|
||||||
|
// CDF: P(X <= 1)
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.153147575");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||||
|
}
|
||||||
61
base/src/test/statistical/test_fn_log_norm.rs
Normal file
61
base/src/test/statistical/test_fn_log_norm.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_log_norm_dist_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid: CDF and PDF
|
||||||
|
model._set("A1", "=LOGNORM.DIST(4, 3.5, 1.2, TRUE)");
|
||||||
|
model._set("A2", "=LOGNORM.DIST(4, 3.5, 1.2, FALSE)");
|
||||||
|
|
||||||
|
// Wrong number of arguments -> #ERROR!
|
||||||
|
model._set("A3", "=LOGNORM.DIST(4, 3.5, 1.2)");
|
||||||
|
model._set("A4", "=LOGNORM.DIST(4, 3.5, 1.2, TRUE, FALSE)");
|
||||||
|
|
||||||
|
// Domain errors:
|
||||||
|
// x <= 0 -> #NUM!
|
||||||
|
model._set("A5", "=LOGNORM.DIST(0, 3.5, 1.2, TRUE)");
|
||||||
|
// std_dev <= 0 -> #NUM!
|
||||||
|
model._set("A6", "=LOGNORM.DIST(4, 3.5, 0, TRUE)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.039083556");
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.017617597");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_log_norm_inv_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid call
|
||||||
|
model._set("A1", "=LOGNORM.INV(0.5, 3.5, 1.2)");
|
||||||
|
|
||||||
|
// Wrong number of arguments -> #ERROR!
|
||||||
|
model._set("A2", "=LOGNORM.INV(0.5, 3.5)");
|
||||||
|
model._set("A3", "=LOGNORM.INV(0.5, 3.5, 1.2, 0)");
|
||||||
|
|
||||||
|
// Domain errors:
|
||||||
|
// probability <= 0 or >= 1 -> #NUM!
|
||||||
|
model._set("A4", "=LOGNORM.INV(0, 3.5, 1.2)");
|
||||||
|
model._set("A5", "=LOGNORM.INV(1, 3.5, 1.2)");
|
||||||
|
// std_dev <= 0 -> #NUM!
|
||||||
|
model._set("A6", "=LOGNORM.INV(0.5, 3.5, 0)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"33.115451959");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
}
|
||||||
119
base/src/test/statistical/test_fn_norm_dist.rs
Normal file
119
base/src/test/statistical/test_fn_norm_dist.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_norm_dist_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid: standard normal as a special case
|
||||||
|
model._set("A1", "=NORM.DIST(1, 0, 1, TRUE)");
|
||||||
|
model._set("A2", "=NORM.DIST(1, 0, 1, FALSE)");
|
||||||
|
|
||||||
|
// Wrong number of arguments -> #ERROR!
|
||||||
|
model._set("A3", "=NORM.DIST(1, 0, 1)");
|
||||||
|
model._set("A4", "=NORM.DIST(1, 0, 1, TRUE, FALSE)");
|
||||||
|
|
||||||
|
// Domain errors: standard_dev <= 0 -> #NUM!
|
||||||
|
model._set("A5", "=NORM.DIST(1, 0, 0, TRUE)");
|
||||||
|
model._set("A6", "=NORM.DIST(1, 0, -1, TRUE)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.841344746");
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.241970725");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_norm_inv_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid: median of standard normal
|
||||||
|
model._set("A1", "=NORM.INV(0.5, 0, 1)");
|
||||||
|
|
||||||
|
// Wrong number of arguments -> #ERROR!
|
||||||
|
model._set("A2", "=NORM.INV(0.5, 0)");
|
||||||
|
model._set("A3", "=NORM.INV(0.5, 0, 1, 0)");
|
||||||
|
|
||||||
|
// Domain errors:
|
||||||
|
// probability <= 0 or >= 1 -> #NUM!
|
||||||
|
model._set("A4", "=NORM.INV(0, 0, 1)");
|
||||||
|
model._set("A5", "=NORM.INV(1, 0, 1)");
|
||||||
|
// standard_dev <= 0 -> #NUM!
|
||||||
|
model._set("A6", "=NORM.INV(0.5, 0, 0)");
|
||||||
|
|
||||||
|
model._set("A7", "=NORM.INV(0.7, 0.2, 1)");
|
||||||
|
model._set("A8", "=NORM.INV(0.7, 0.2, 5)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A7"), *"0.724400513");
|
||||||
|
assert_eq!(model._get_text("A8"), *"2.822002564");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_norm_s_dist_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid: CDF and PDF at z = 0
|
||||||
|
model._set("A1", "=NORM.S.DIST(0, TRUE)");
|
||||||
|
model._set("A2", "=NORM.S.DIST(0, FALSE)");
|
||||||
|
|
||||||
|
// Wrong number of arguments -> #ERROR!
|
||||||
|
model._set("A3", "=NORM.S.DIST(0)");
|
||||||
|
model._set("A4", "=NORM.S.DIST(0, TRUE, FALSE)");
|
||||||
|
|
||||||
|
model._set("A5", "=NORM.S.DIST(0.2, FALSE)");
|
||||||
|
model._set("A6", "=NORM.S.DIST(2.2, TRUE)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.5");
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.39894228");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A5"), *"0.391042694");
|
||||||
|
assert_eq!(model._get_text("A6"), *"0.986096552");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_norm_s_inv_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid: symmetric points
|
||||||
|
model._set("A1", "=NORM.S.INV(0.5)");
|
||||||
|
model._set("A2", "=NORM.S.INV(0.841344746)");
|
||||||
|
|
||||||
|
// Wrong number of arguments -> #ERROR!
|
||||||
|
model._set("A3", "=NORM.S.INV()");
|
||||||
|
model._set("A4", "=NORM.S.INV(0.5, 0)");
|
||||||
|
|
||||||
|
// Domain errors: probability <= 0 or >= 1 -> #NUM!
|
||||||
|
model._set("A5", "=NORM.S.INV(0)");
|
||||||
|
model._set("A6", "=NORM.S.INV(1)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0");
|
||||||
|
// Approximately 1
|
||||||
|
assert_eq!(model._get_text("A2"), *"1");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
}
|
||||||
31
base/src/test/statistical/test_fn_pearson.rs
Normal file
31
base/src/test/statistical/test_fn_pearson.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_chisq_test_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A2", "48");
|
||||||
|
model._set("A3", "32");
|
||||||
|
model._set("A4", "12");
|
||||||
|
model._set("A5", "1");
|
||||||
|
model._set("A6", "'13");
|
||||||
|
model._set("A7", "TRUE");
|
||||||
|
model._set("A8", "1");
|
||||||
|
model._set("A9", "13");
|
||||||
|
model._set("A10", "15");
|
||||||
|
|
||||||
|
model._set("B2", "55");
|
||||||
|
model._set("B3", "34");
|
||||||
|
model._set("B4", "13");
|
||||||
|
model._set("B5", "blah");
|
||||||
|
model._set("B6", "13");
|
||||||
|
model._set("B7", "1");
|
||||||
|
model._set("B8", "TRUE");
|
||||||
|
model._set("B9", "'14");
|
||||||
|
model._set("B10", "16");
|
||||||
|
|
||||||
|
model._set("C1", "=PEARSON(A2:A10, B2:B10)");
|
||||||
|
model.evaluate();
|
||||||
|
assert_eq!(model._get_text("C1"), *"0.998381439");
|
||||||
|
}
|
||||||
26
base/src/test/statistical/test_fn_phi.rs
Normal file
26
base/src/test/statistical/test_fn_phi.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_phi_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
model._set("A1", "=PHI(0)");
|
||||||
|
model._set("A2", "=PHI(1)");
|
||||||
|
model._set("A3", "=PHI(-1)");
|
||||||
|
|
||||||
|
// Wrong number of arguments -> #ERROR!
|
||||||
|
model._set("A4", "=PHI()");
|
||||||
|
model._set("A5", "=PHI(0, 1)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// Standard values
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.39894228");
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.241970725");
|
||||||
|
assert_eq!(model._get_text("A3"), *"0.241970725");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
|
}
|
||||||
41
base/src/test/statistical/test_fn_poisson.rs
Normal file
41
base/src/test/statistical/test_fn_poisson.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_poisson_dist_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// λ = 2, x = 3
|
||||||
|
// P(X = 3) ≈ 0.180447045
|
||||||
|
// P(X <= 3) ≈ 0.857123461
|
||||||
|
model._set("A1", "=POISSON.DIST(3, 2, FALSE)");
|
||||||
|
model._set("A2", "=POISSON.DIST(3, 2, TRUE)");
|
||||||
|
|
||||||
|
// Wrong arg count
|
||||||
|
model._set("A3", "=POISSON.DIST(3, 2)");
|
||||||
|
model._set("A4", "=POISSON.DIST(3, 2, TRUE, FALSE)");
|
||||||
|
|
||||||
|
// Domain errors
|
||||||
|
model._set("A5", "=POISSON.DIST(-1, 2, TRUE)"); // x < 0
|
||||||
|
model._set("A6", "=POISSON.DIST(3, -2, TRUE)"); // mean < 0
|
||||||
|
|
||||||
|
// λ = 0 special cases
|
||||||
|
model._set("A7", "=POISSON.DIST(0, 0, FALSE)"); // 1
|
||||||
|
model._set("A8", "=POISSON.DIST(1, 0, FALSE)"); // 0
|
||||||
|
model._set("A9", "=POISSON.DIST(5, 0, TRUE)"); // 1
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.180447044");
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.85712346");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A7"), *"1");
|
||||||
|
assert_eq!(model._get_text("A8"), *"0");
|
||||||
|
assert_eq!(model._get_text("A9"), *"1");
|
||||||
|
}
|
||||||
46
base/src/test/statistical/test_fn_stdev.rs
Normal file
46
base/src/test/statistical/test_fn_stdev.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn smoke_test() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=STDEV.P(10, 12, 23, 23, 16, 23, 21)");
|
||||||
|
model._set("A2", "=STDEV.S(10, 12, 23, 23, 16, 23, 21)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"5.174505793");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A2"), *"5.589105048");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn numbers() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
model._set("A2", "24");
|
||||||
|
model._set("A3", "25");
|
||||||
|
model._set("A4", "27");
|
||||||
|
model._set("A5", "23");
|
||||||
|
model._set("A6", "45");
|
||||||
|
model._set("A7", "23.5");
|
||||||
|
model._set("A8", "34");
|
||||||
|
model._set("A9", "23");
|
||||||
|
model._set("A10", "23");
|
||||||
|
model._set("A11", "TRUE");
|
||||||
|
model._set("A12", "'23");
|
||||||
|
model._set("A13", "Text");
|
||||||
|
model._set("A14", "FALSE");
|
||||||
|
model._set("A15", "45");
|
||||||
|
|
||||||
|
model._set("B1", "=STDEV.P(A2:A15)");
|
||||||
|
model._set("B2", "=STDEV.S(A2:A15)");
|
||||||
|
model._set("B3", "=STDEVA(A2:A15)");
|
||||||
|
model._set("B4", "=STDEVPA(A2:A15)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("B1"), *"8.483071378");
|
||||||
|
assert_eq!(model._get_text("B2"), *"8.941942369");
|
||||||
|
assert_eq!(model._get_text("B3"), *"15.499955689");
|
||||||
|
assert_eq!(model._get_text("B4"), *"14.936131032");
|
||||||
|
}
|
||||||
160
base/src/test/statistical/test_fn_t_dist.rs
Normal file
160
base/src/test/statistical/test_fn_t_dist.rs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_t_dist_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid: cumulative (left-tail CDF)
|
||||||
|
model._set("A1", "=T.DIST(2, 10, TRUE)");
|
||||||
|
// Valid: probability density function (PDF)
|
||||||
|
model._set("B1", "=T.DIST(2, 10, FALSE)");
|
||||||
|
|
||||||
|
// Wrong number of arguments
|
||||||
|
model._set("A2", "=T.DIST(2, 10)");
|
||||||
|
model._set("A3", "=T.DIST(2, 10, TRUE, FALSE)");
|
||||||
|
|
||||||
|
// Domain error: df < 1 -> #NUM!
|
||||||
|
model._set("A4", "=T.DIST(2, 0, TRUE)");
|
||||||
|
model._set("A5", "=T.DIST(2, -1, TRUE)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.963305983");
|
||||||
|
assert_eq!(model._get_text("B1"), *"0.061145766");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_t_dist_rt_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid: right tail probability
|
||||||
|
model._set("A1", "=T.DIST.RT(2, 10)");
|
||||||
|
|
||||||
|
// Wrong number of arguments
|
||||||
|
model._set("A2", "=T.DIST.RT(2)");
|
||||||
|
model._set("A3", "=T.DIST.RT(2, 10, TRUE)");
|
||||||
|
|
||||||
|
// Domain error: df < 1
|
||||||
|
model._set("A4", "=T.DIST.RT(2, 0)");
|
||||||
|
model._set("A5", "=T.DIST.RT(2, -1)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.036694017");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_t_dist_2t_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid: two-tailed probability
|
||||||
|
model._set("A1", "=T.DIST.2T(2, 10)");
|
||||||
|
|
||||||
|
// In the limit case of x = 0, the two-tailed probability is 1.0
|
||||||
|
model._set("A4", "=T.DIST.2T(0, 10)");
|
||||||
|
|
||||||
|
// Wrong number of arguments
|
||||||
|
model._set("A2", "=T.DIST.2T(2)");
|
||||||
|
model._set("A3", "=T.DIST.2T(2, 10, TRUE)");
|
||||||
|
|
||||||
|
// Domain errors:
|
||||||
|
// x < 0 -> #NUM!
|
||||||
|
model._set("A5", "=T.DIST.2T(-0.001, 10)");
|
||||||
|
// df < 1 -> #NUM!
|
||||||
|
model._set("A6", "=T.DIST.2T(2, 0)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.073388035");
|
||||||
|
assert_eq!(model._get_text("A4"), *"1");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_t_inv_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid: upper and lower tail
|
||||||
|
model._set("A1", "=T.INV(0.95, 10)");
|
||||||
|
model._set("A2", "=T.INV(0.05, 10)");
|
||||||
|
// limit case:
|
||||||
|
model._set("B2", "=T.INV(0.95, 1)");
|
||||||
|
|
||||||
|
// Wrong number of arguments
|
||||||
|
model._set("A3", "=T.INV(0.95)");
|
||||||
|
model._set("A4", "=T.INV(0.95, 10, 1)");
|
||||||
|
|
||||||
|
// Domain errors:
|
||||||
|
// p <= 0 or >= 1
|
||||||
|
model._set("A5", "=T.INV(0, 10)");
|
||||||
|
model._set("A6", "=T.INV(1, 10)");
|
||||||
|
// df < 1
|
||||||
|
model._set("A7", "=T.INV(0.95, 0)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"1.812461123");
|
||||||
|
assert_eq!(model._get_text("A2"), *"-1.812461123");
|
||||||
|
assert_eq!(model._get_text("B2"), *"6.313751515");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_t_inv_2t_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid: two-tailed critical values
|
||||||
|
model._set("A1", "=T.INV.2T(0.1, 10)");
|
||||||
|
model._set("A2", "=T.INV.2T(0.05, 10)");
|
||||||
|
|
||||||
|
// p = 1 should give t = 0 (both tails outside are 1.0, so cut at the mean)
|
||||||
|
model._set("A3", "=T.INV.2T(1, 10)");
|
||||||
|
|
||||||
|
model._set("A7", "=T.INV.2T(1.5, 10)");
|
||||||
|
|
||||||
|
// Wrong number of arguments
|
||||||
|
model._set("A4", "=T.INV.2T(0.1)");
|
||||||
|
model._set("A5", "=T.INV.2T(0.1, 10, 1)");
|
||||||
|
|
||||||
|
// Domain errors:
|
||||||
|
// p <= 0 or p > 1
|
||||||
|
model._set("A6", "=T.INV.2T(0, 10)");
|
||||||
|
// df < 1
|
||||||
|
model._set("A8", "=T.INV.2T(0.1, 0)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"1.812461123");
|
||||||
|
assert_eq!(model._get_text("A2"), *"2.228138852");
|
||||||
|
assert_eq!(model._get_text("A3"), *"0");
|
||||||
|
|
||||||
|
// NB: Excel returns -0.699812061 for T.INV.2T(1.5, 10)
|
||||||
|
// which seems inconsistent with its documented behavior
|
||||||
|
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A8"), *"#NUM!");
|
||||||
|
}
|
||||||
41
base/src/test/statistical/test_fn_t_test.rs
Normal file
41
base/src/test/statistical/test_fn_t_test.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
#[test]
|
||||||
|
fn test_fn_t_test_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A2", "3");
|
||||||
|
model._set("A3", "4");
|
||||||
|
model._set("A4", "5");
|
||||||
|
model._set("A5", "6");
|
||||||
|
model._set("A6", "10");
|
||||||
|
model._set("A7", "3");
|
||||||
|
model._set("A8", "2");
|
||||||
|
model._set("A9", "4");
|
||||||
|
model._set("A10", "7");
|
||||||
|
|
||||||
|
model._set("B2", "6");
|
||||||
|
model._set("B3", "19");
|
||||||
|
model._set("B4", "3");
|
||||||
|
model._set("B5", "2");
|
||||||
|
model._set("B6", "13");
|
||||||
|
model._set("B7", "4");
|
||||||
|
model._set("B8", "5");
|
||||||
|
model._set("B9", "17");
|
||||||
|
model._set("B10", "3");
|
||||||
|
|
||||||
|
model._set("C1", "=T.TEST(A2:A10, B2:B10, 1, 1)");
|
||||||
|
model._set("C2", "=T.TEST(A2:A10, B2:B10, 1, 2)");
|
||||||
|
model._set("C3", "=T.TEST(A2:A10, B2:B10, 1, 3)");
|
||||||
|
model._set("C4", "=T.TEST(A2:A10, B2:B10, 2, 1)");
|
||||||
|
model._set("C5", "=T.TEST(A2:A10, B2:B10, 2, 2)");
|
||||||
|
model._set("C6", "=T.TEST(A2:A10, B2:B10, 2, 3)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("C1"), *"0.103836888");
|
||||||
|
assert_eq!(model._get_text("C2"), *"0.100244599");
|
||||||
|
assert_eq!(model._get_text("C3"), *"0.105360319");
|
||||||
|
assert_eq!(model._get_text("C4"), *"0.207673777");
|
||||||
|
assert_eq!(model._get_text("C5"), *"0.200489197");
|
||||||
|
assert_eq!(model._get_text("C6"), *"0.210720639");
|
||||||
|
}
|
||||||
46
base/src/test/statistical/test_fn_var.rs
Normal file
46
base/src/test/statistical/test_fn_var.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn smoke_test() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=STDEV.P(10, 12, 23, 23, 16, 23, 21)");
|
||||||
|
model._set("A2", "=STDEV.S(10, 12, 23, 23, 16, 23, 21)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"5.174505793");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A2"), *"5.589105048");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn numbers() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
model._set("A2", "24");
|
||||||
|
model._set("A3", "25");
|
||||||
|
model._set("A4", "27");
|
||||||
|
model._set("A5", "23");
|
||||||
|
model._set("A6", "45");
|
||||||
|
model._set("A7", "23.5");
|
||||||
|
model._set("A8", "34");
|
||||||
|
model._set("A9", "23");
|
||||||
|
model._set("A10", "23");
|
||||||
|
model._set("A11", "TRUE");
|
||||||
|
model._set("A12", "'23");
|
||||||
|
model._set("A13", "Text");
|
||||||
|
model._set("A14", "FALSE");
|
||||||
|
model._set("A15", "45");
|
||||||
|
|
||||||
|
model._set("B1", "=VAR.P(A2:A15)");
|
||||||
|
model._set("B2", "=VAR.S(A2:A15)");
|
||||||
|
model._set("B3", "=VARA(A2:A15)");
|
||||||
|
model._set("B4", "=VARPA(A2:A15)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("B1"), *"71.9625");
|
||||||
|
assert_eq!(model._get_text("B2"), *"79.958333333");
|
||||||
|
assert_eq!(model._get_text("B3"), *"240.248626374");
|
||||||
|
assert_eq!(model._get_text("B4"), *"223.088010204");
|
||||||
|
}
|
||||||
41
base/src/test/statistical/test_fn_weibull.rs
Normal file
41
base/src/test/statistical/test_fn_weibull.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_weibull_dist_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid: CDF and PDF for x = 1, alpha = 2, beta = 1
|
||||||
|
model._set("A1", "=WEIBULL.DIST(1, 2, 1, TRUE)");
|
||||||
|
model._set("A2", "=WEIBULL.DIST(1, 2, 1, FALSE)");
|
||||||
|
|
||||||
|
// Wrong number of arguments -> #ERROR!
|
||||||
|
model._set("A3", "=WEIBULL.DIST(1, 2, 1)");
|
||||||
|
model._set("A4", "=WEIBULL.DIST(1, 2, 1, TRUE, FALSE)");
|
||||||
|
|
||||||
|
// Domain errors:
|
||||||
|
// x < 0 -> #NUM!
|
||||||
|
model._set("A5", "=WEIBULL.DIST(-1, 2, 1, TRUE)");
|
||||||
|
// alpha <= 0 -> #NUM!
|
||||||
|
model._set("A6", "=WEIBULL.DIST(1, 0, 1, TRUE)");
|
||||||
|
model._set("A7", "=WEIBULL.DIST(1, -1, 1, TRUE)");
|
||||||
|
// beta <= 0 -> #NUM!
|
||||||
|
model._set("A8", "=WEIBULL.DIST(1, 2, 0, TRUE)");
|
||||||
|
model._set("A9", "=WEIBULL.DIST(1, 2, -1, TRUE)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// 1 - e^-1
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.632120559");
|
||||||
|
// 2 * e^-1
|
||||||
|
assert_eq!(model._get_text("A2"), *"0.735758882");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A8"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A9"), *"#NUM!");
|
||||||
|
}
|
||||||
36
base/src/test/statistical/test_fn_z_test.rs
Normal file
36
base/src/test/statistical/test_fn_z_test.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_z_test_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A2", "3");
|
||||||
|
model._set("A3", "6");
|
||||||
|
model._set("A4", "7");
|
||||||
|
model._set("A5", "8");
|
||||||
|
model._set("A6", "6");
|
||||||
|
model._set("A7", "5");
|
||||||
|
model._set("A8", "4");
|
||||||
|
model._set("A9", "2");
|
||||||
|
model._set("A10", "1");
|
||||||
|
model._set("A11", "9");
|
||||||
|
|
||||||
|
model._set("G1", "=Z.TEST(A2:A11, 4)");
|
||||||
|
model._set("G2", "=Z.TEST(A2:A11, 6)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("G1"), *"0.090574197");
|
||||||
|
assert_eq!(model._get_text("G2"), *"0.863043389");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrays() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("D1", "=Z.TEST({5,2,3,4}, 4, 123)");
|
||||||
|
model._set("D2", "=Z.TEST({5,2,3,4}, 4)");
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("D1"), *"0.503243397");
|
||||||
|
assert_eq!(model._get_text("D2"), *"0.780710987");
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ use crate::test::util::new_empty_model;
|
|||||||
fn arguments() {
|
fn arguments() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
model._set("A1", "=ARABIC()");
|
model._set("A1", "=ARABIC()");
|
||||||
model._set("A2", "=ARABIC(V)");
|
model._set("A2", "=ARABIC(\"V\")");
|
||||||
model._set("A3", "=ARABIC(V, 2)");
|
model._set("A3", "=ARABIC(\"V\", 2)");
|
||||||
|
|
||||||
model._set("A4", "=ROMAN()");
|
model._set("A4", "=ROMAN()");
|
||||||
model._set("A5", "=ROMAN(5)");
|
model._set("A5", "=ROMAN(5)");
|
||||||
35
base/src/test/test_cell_info_n_sheets
Normal file
35
base/src/test/test_cell_info_n_sheets
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=CELL("address",A1)");
|
||||||
|
model._set("A2", "=CELL()");
|
||||||
|
|
||||||
|
model._set("A3", "=INFO("system")");
|
||||||
|
model._set("A4", "=INFO()");
|
||||||
|
|
||||||
|
model._set("A5", "=N(TRUE)");
|
||||||
|
model._set("A6", "=N()");
|
||||||
|
model._set("A7", "=N(1, 2)");
|
||||||
|
|
||||||
|
model._set("A8", "=SHEETS()");
|
||||||
|
model._set("A9", "=SHEETS(1)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"$A$1");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#N/IMPL!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A5"), *"1");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A8"), *"1");
|
||||||
|
assert_eq!(model._get_text("A9"), *"#N/IMPL!");
|
||||||
|
}
|
||||||
@@ -11,8 +11,8 @@ fn arguments() {
|
|||||||
model._set("A4", "=COMBINA()");
|
model._set("A4", "=COMBINA()");
|
||||||
model._set("A5", "=COMBIN(2)");
|
model._set("A5", "=COMBIN(2)");
|
||||||
model._set("A6", "=COMBINA(2)");
|
model._set("A6", "=COMBINA(2)");
|
||||||
model._set("A5", "=COMBIN(1, 2, 3)");
|
model._set("A7", "=COMBIN(1, 2, 3)");
|
||||||
model._set("A6", "=COMBINA(1, 2, 3)");
|
model._set("A8", "=COMBINA(1, 2, 3)");
|
||||||
|
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
123
base/src/test/test_floor.rs
Normal file
123
base/src/test/test_floor.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_floor_floating_point_precision() {
|
||||||
|
// This test specifically checks the floating-point precision bug fix
|
||||||
|
// Bug: FLOOR(7.1, 0.1) was returning 7.0 instead of 7.1
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// FLOOR tests
|
||||||
|
model._set("C5", "=FLOOR(7.1, 0.1)");
|
||||||
|
model._set("H7", "=FLOOR(-7.1, -0.1)");
|
||||||
|
|
||||||
|
// FLOOR.PRECISE tests
|
||||||
|
model._set("C53", "=FLOOR.PRECISE(7.1, 0.1)");
|
||||||
|
model._set("H53", "=FLOOR.PRECISE(7.1, -0.1)");
|
||||||
|
|
||||||
|
// FLOOR.MATH tests
|
||||||
|
model._set("C101", "=FLOOR.MATH(7.1, 0.1)");
|
||||||
|
model._set("H101", "=FLOOR.MATH(7.1, -0.1)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// All should return 7.1
|
||||||
|
assert_eq!(model._get_text("C5"), *"7.1");
|
||||||
|
assert_eq!(model._get_text("H7"), *"-7.1");
|
||||||
|
assert_eq!(model._get_text("C53"), *"7.1");
|
||||||
|
assert_eq!(model._get_text("H53"), *"7.1");
|
||||||
|
assert_eq!(model._get_text("C101"), *"7.1");
|
||||||
|
assert_eq!(model._get_text("H101"), *"7.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_floor_additional_precision_cases() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=FLOOR(7.9, 0.1)");
|
||||||
|
model._set("A2", "=FLOOR(2.6, 0.5)");
|
||||||
|
model._set("A3", "=FLOOR(0.3, 0.1)"); // 0.1 + 0.2 type scenario
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"7.9");
|
||||||
|
assert_eq!(model._get_text("A2"), *"2.5");
|
||||||
|
assert_eq!(model._get_text("A3"), *"0.3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_floor_basic_cases() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=FLOOR(3.7, 2)");
|
||||||
|
model._set("A2", "=FLOOR(3.2, 1)");
|
||||||
|
model._set("A3", "=FLOOR(10, 3)");
|
||||||
|
model._set("A4", "=FLOOR(7, 2)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"2");
|
||||||
|
assert_eq!(model._get_text("A2"), *"3");
|
||||||
|
assert_eq!(model._get_text("A3"), *"9");
|
||||||
|
assert_eq!(model._get_text("A4"), *"6");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_floor_negative_numbers() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
// Both negative: rounds toward zero
|
||||||
|
model._set("A1", "=FLOOR(-2.5, -2)");
|
||||||
|
model._set("A2", "=FLOOR(-11, -3)");
|
||||||
|
|
||||||
|
// Negative number, positive significance: rounds away from zero
|
||||||
|
model._set("A3", "=FLOOR(-11, 3)");
|
||||||
|
model._set("A4", "=FLOOR(-2.5, 2)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"-2");
|
||||||
|
assert_eq!(model._get_text("A2"), *"-9");
|
||||||
|
assert_eq!(model._get_text("A3"), *"-12");
|
||||||
|
assert_eq!(model._get_text("A4"), *"-4");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_floor_error_cases() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
// Positive number with negative significance should error
|
||||||
|
model._set("A1", "=FLOOR(2.5, -2)");
|
||||||
|
model._set("A2", "=FLOOR(10, -3)");
|
||||||
|
|
||||||
|
// Division by zero
|
||||||
|
model._set("A3", "=FLOOR(5, 0)");
|
||||||
|
|
||||||
|
// Wrong number of arguments
|
||||||
|
model._set("A4", "=FLOOR(5)");
|
||||||
|
model._set("A5", "=FLOOR(5, 1, 1)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#DIV/0!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_floor_edge_cases() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
// Zero value
|
||||||
|
model._set("A1", "=FLOOR(0, 5)");
|
||||||
|
model._set("A2", "=FLOOR(0, 0)");
|
||||||
|
|
||||||
|
// Exact multiples
|
||||||
|
model._set("A3", "=FLOOR(10, 5)");
|
||||||
|
model._set("A4", "=FLOOR(9, 3)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0");
|
||||||
|
assert_eq!(model._get_text("A2"), *"0");
|
||||||
|
assert_eq!(model._get_text("A3"), *"10");
|
||||||
|
assert_eq!(model._get_text("A4"), *"9");
|
||||||
|
}
|
||||||
@@ -7,8 +7,8 @@ fn datevalue_timevalue_arguments() {
|
|||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
model._set("A1", "=DATEVALUE()");
|
model._set("A1", "=DATEVALUE()");
|
||||||
model._set("A2", "=TIMEVALUE()");
|
model._set("A2", "=TIMEVALUE()");
|
||||||
model._set("A3", "=DATEVALUE("2000-01-01")")
|
model._set("A3", "=DATEVALUE(\"2000-01-01\")");
|
||||||
model._set("A4", "=TIMEVALUE("12:00:00")")
|
model._set("A4", "=TIMEVALUE(\"12:00:00\")");
|
||||||
model._set("A5", "=DATEVALUE(1,2)");
|
model._set("A5", "=DATEVALUE(1,2)");
|
||||||
model._set("A6", "=TIMEVALUE(1,2)");
|
model._set("A6", "=TIMEVALUE(1,2)");
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
@@ -20,5 +20,3 @@ fn datevalue_timevalue_arguments() {
|
|||||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ fn arguments() {
|
|||||||
model._set("A11", "=INT(10.22, 1)");
|
model._set("A11", "=INT(10.22, 1)");
|
||||||
model._set("A12", "=INT(10.22, 1, 2)");
|
model._set("A12", "=INT(10.22, 1, 2)");
|
||||||
|
|
||||||
|
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||||
@@ -29,7 +28,7 @@ fn arguments() {
|
|||||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
|
||||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
assert_eq!(model._get_text("A6"), *"10");
|
||||||
assert_eq!(model._get_text("A7"), *"10.2");
|
assert_eq!(model._get_text("A7"), *"10.2");
|
||||||
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||||
|
|
||||||
@@ -7,15 +7,24 @@ const TIMESTAMP_2023: i64 = 1679319865208;
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arguments() {
|
fn arguments() {
|
||||||
|
mock_time::set_mock_time(TIMESTAMP_2023);
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
model._set("A1", "=NOW(1)");
|
model._set("A1", "=NOW(1, 1)");
|
||||||
|
model._set("A2", "=NOW(\"Europe/Berlin\")");
|
||||||
|
model._set("A3", "=NOW(\"faketimezone\")");
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model._get_text("A1"),
|
model._get_text("A1"),
|
||||||
"#ERROR!",
|
"#ERROR!",
|
||||||
"NOW should not accept arguments"
|
"Wrong number of arguments"
|
||||||
|
);
|
||||||
|
assert_eq!(model._get_text("A2"), *"20/03/2023 14:44:25");
|
||||||
|
assert_eq!(
|
||||||
|
model._get_text("A3"),
|
||||||
|
"#VALUE!",
|
||||||
|
"Invalid timezone: faketimezone"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
base/src/test/test_sumsq.rs
Normal file
17
base/src/test/test_sumsq.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=SUMSQ()");
|
||||||
|
model._set("A2", "=SUMSQ(2)");
|
||||||
|
model._set("A3", "=SUMSQ(1, 2)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A2"), *"4");
|
||||||
|
assert_eq!(model._get_text("A3"), *"5");
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
|
|
||||||
| Function | Status | Documentation |
|
| Function | Status | Documentation |
|
||||||
| ---------- | ---------------------------------------------- | ------------- |
|
| ---------- | ---------------------------------------------- | ------------- |
|
||||||
| CELL | <Badge type="info" text="Not implemented yet" /> | – |
|
| CELL | <Badge type="tip" text="Available" /> | – |
|
||||||
| ERROR.TYPE | <Badge type="tip" text="Available" /> | – |
|
| ERROR.TYPE | <Badge type="tip" text="Available" /> | – |
|
||||||
| INFO | <Badge type="info" text="Not implemented yet" /> | – |
|
| INFO | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| ISBLANK | <Badge type="tip" text="Available" /> | – |
|
| ISBLANK | <Badge type="tip" text="Available" /> | – |
|
||||||
@@ -20,7 +20,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| ISEVEN | <Badge type="tip" text="Available" /> | – |
|
| ISEVEN | <Badge type="tip" text="Available" /> | – |
|
||||||
| ISFORMULA | <Badge type="tip" text="Available" /> | – |
|
| ISFORMULA | <Badge type="tip" text="Available" /> | – |
|
||||||
| ISLOGICAL | <Badge type="tip" text="Available" /> | – |
|
| ISLOGICAL | <Badge type="tip" text="Available" /> | – |
|
||||||
| ISNA | <Badge type="info" text="Not implemented yet" /> | – |
|
| ISNA | <Badge type="tip" text="Available" /> | – |
|
||||||
| ISNONTEXT | <Badge type="tip" text="Available" /> | – |
|
| ISNONTEXT | <Badge type="tip" text="Available" /> | – |
|
||||||
| ISNUMBER | <Badge type="tip" text="Available" /> | – |
|
| ISNUMBER | <Badge type="tip" text="Available" /> | – |
|
||||||
| ISODD | <Badge type="tip" text="Available" /> | – |
|
| ISODD | <Badge type="tip" text="Available" /> | – |
|
||||||
@@ -30,5 +30,5 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| N | <Badge type="info" text="Not implemented yet" /> | – |
|
| N | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| NA | <Badge type="tip" text="Available" /> | – |
|
| NA | <Badge type="tip" text="Available" /> | – |
|
||||||
| SHEET | <Badge type="tip" text="Available" /> | – |
|
| SHEET | <Badge type="tip" text="Available" /> | – |
|
||||||
| SHEETS | <Badge type="info" text="Not implemented yet" /> | – |
|
| SHEETS | <Badge type="tip" text="Available" /> | – |
|
||||||
| TYPE | <Badge type="tip" text="Available" /> | – |
|
| TYPE | <Badge type="tip" text="Available" /> | – |
|
||||||
|
|||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# CELL
|
# CELL
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# N
|
# N
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# SHEETS
|
# SHEETS
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -6,26 +6,33 @@ lang: en-US
|
|||||||
|
|
||||||
# COLUMN function
|
# COLUMN function
|
||||||
## Overview
|
## Overview
|
||||||
The COLUMN Function in IronCalc is a lookup & reference formula that is used to query and return the column number of a referenced Column or Cell.
|
The COLUMN Function in IronCalc is a lookup & reference formula that is used to query and return the column number of a referenced column or cell.
|
||||||
## Usage
|
## Usage
|
||||||
### Syntax
|
### Syntax
|
||||||
**COLUMN(<span title="Reference" style="color:#1E88E5">reference</span>) => <span title="Number" style="color:#1E88E5">column</span>**
|
**COLUMN(<span title="Reference" style="color:#1E88E5">reference</span>) => <span title="Number" style="color:#1E88E5">column</span>**
|
||||||
### Argument descriptions
|
### Argument descriptions
|
||||||
* *reference* ([cell](/features/value-types#references), [optional](/features/optional-arguments.md)). The number of the cell you wish to reference the column number of.
|
* *reference* ([cell](/features/value-types#references), [optional](/features/optional-arguments.md)). The cell, column, range, or [Named Range](/web-application/name-manager.html) for which you wish to find the column number.
|
||||||
### Additional guidance
|
### Additional guidance
|
||||||
* When referencing a range of cells, only the column number of the leftmost cell will be returned.
|
* When referencing a range of cells, only the column number of the leftmost cell will be returned.
|
||||||
* You are also able to reference complete columns instead of individual cells.
|
* You are also able to reference complete columns instead of individual cells.
|
||||||
|
* When using a Named Range as a reference, the reference is not case sensitive.
|
||||||
|
* IronCalc supports the use of both *Absolute* ($A$1) and *Relative* (A1) references.
|
||||||
|
* Cross-sheet references are also supported.
|
||||||
### Returned value
|
### Returned value
|
||||||
COLUMN returns the [number](/features/value-types#numbers) of the specific cell or column which is being referenced.
|
COLUMN returns the [number](/features/value-types#numbers) of the specific cell or column which is being referenced. If no reference is included, the column number of the cell where the formula is entered will be returned.
|
||||||
### Error conditions
|
### Error conditions
|
||||||
* IronCalc currently does not support the referencing of cells with names.
|
* A [#NAME?](/features/error-types.html#name) error is returned if a Named Range being referenced is deleted.
|
||||||
|
* A [#REF!](/features/error-types.html#ref) error is returned if a cell being referenced is deleted.
|
||||||
|
* A [#VALUE!](/features/error-types.html#value) error is returned if a column being referenced is deleted.
|
||||||
## Details
|
## Details
|
||||||
The COLUMN Function can only be used to display the correlating number of a single column within a Sheet. If you wish to show the number of columns used within a specific range, you can use the COLUMNS Function.
|
The COLUMN Function can only be used to display the correlating number of a single column within a Sheet. If you wish to show the number of columns used within a specific range, you can use the [COLUMNS](/functions/lookup_and_reference/columns) Function.
|
||||||
## Examples
|
## Examples
|
||||||
### No Cell Reference
|
### No Cell Reference
|
||||||
When no cell reference is made, the formula uses **=COLUMN()**. This will then output the column number of the cell where the formula is placed.<br><br>For example, if the formula is placed in cell A1, then "1" will be displayed.
|
When no cell reference is made, the formula uses **=COLUMN()**. This will output the column number of the cell where the formula is entered.<br><br>For example, if the formula is placed in cell A1, then "1" will be displayed.
|
||||||
### With Cell Reference
|
### With Cell Reference
|
||||||
When a cell reference is made, the formula uses **=COLUMN([Referenced Cell])**. This will then output the column number of the referenced cell, regardless of where the formula is placed in the sheet.<br><br>For example, if the cell B1 is the referenced cell, "2" will be the output of the formula no matter where it is placed in the sheet.<br><br>**Note:** references do not always have to be specific cells, you can also reference complete columns. For example, **=COLUMN(B:B)** would also result in an output of "2".
|
When a cell reference is made, the formula uses **=COLUMN(<span title="Reference" style="color:#1E88E5">Referenced Cell</span>)**. This will then output the column number of the referenced cell, regardless of where the formula is placed in the sheet.<br><br>For example, if B1 is the referenced cell, then "2" will be the output of the formula, regardless of where the formula is placed in the sheet.<br><br>**Note:** references do not have to be specific cells, you can also reference complete columns. For example, **=COLUMN(B:B)** would also result in an output of "2".
|
||||||
### Range References
|
### Range References
|
||||||
The COLUMN function can also be used to reference a range of Cells or Columns. In this case only the most left-hand column will be the resulting output.<br><br>For example, **=COLUMN(A1:J1)** will result in the ouput of "1".
|
The COLUMN function can also be used to reference a range of cells or columns. In this case only the leftmost column will be the resulting output.<br><br>For example, **=COLUMN(A1:J1)** will result in the output of "1".
|
||||||
## Links
|
## Links
|
||||||
|
* Visit Microsoft Excel's [Column function](https://support.microsoft.com/en-us/office/column-function-44e8c754-711c-4df3-9da4-47a55042554b) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093373) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/COLUMN) provide versions of the COLUMN function.
|
||||||
@@ -86,7 +86,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| SUMIF | <Badge type="tip" text="Available" /> | – |
|
| SUMIF | <Badge type="tip" text="Available" /> | – |
|
||||||
| SUMIFS | <Badge type="tip" text="Available" /> | – |
|
| SUMIFS | <Badge type="tip" text="Available" /> | – |
|
||||||
| SUMPRODUCT | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMPRODUCT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SUMSQ | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMSQ | <Badge type="tip" text="Available" /> | – |
|
||||||
| SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SUMX2PY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMX2PY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
|
|||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# SUMSQ
|
# SUMSQ
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { Model } from "@ironcalc/wasm";
|
import type { Model } from "@ironcalc/wasm";
|
||||||
import { styled } from "@mui/material";
|
import { styled } from "@mui/material";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { Fx } from "../../icons";
|
import { Fx } from "../../icons";
|
||||||
import { theme } from "../../theme";
|
import { theme } from "../../theme";
|
||||||
import { FORMULA_BAR_HEIGHT } from "../constants";
|
import { FORMULA_BAR_HEIGHT } from "../constants";
|
||||||
@@ -9,6 +11,7 @@ import {
|
|||||||
ROW_HEIGH_SCALE,
|
ROW_HEIGH_SCALE,
|
||||||
} from "../WorksheetCanvas/constants";
|
} from "../WorksheetCanvas/constants";
|
||||||
import type { WorkbookState } from "../workbookState";
|
import type { WorkbookState } from "../workbookState";
|
||||||
|
import FormulaBarMenu from "./FormulaBarMenu";
|
||||||
|
|
||||||
type FormulaBarProps = {
|
type FormulaBarProps = {
|
||||||
cellAddress: string;
|
cellAddress: string;
|
||||||
@@ -17,6 +20,8 @@ type FormulaBarProps = {
|
|||||||
workbookState: WorkbookState;
|
workbookState: WorkbookState;
|
||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
onTextUpdated: () => void;
|
onTextUpdated: () => void;
|
||||||
|
openDrawer: () => void;
|
||||||
|
canEdit: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function FormulaBar(properties: FormulaBarProps) {
|
function FormulaBar(properties: FormulaBarProps) {
|
||||||
@@ -28,10 +33,27 @@ function FormulaBar(properties: FormulaBarProps) {
|
|||||||
onTextUpdated,
|
onTextUpdated,
|
||||||
workbookState,
|
workbookState,
|
||||||
} = properties;
|
} = properties;
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleMenuOpenChange = (isOpen: boolean): void => {
|
||||||
|
setIsMenuOpen(isOpen);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<AddressContainer>
|
<AddressContainer $active={isMenuOpen}>
|
||||||
|
<FormulaBarMenu
|
||||||
|
onMenuOpenChange={handleMenuOpenChange}
|
||||||
|
openDrawer={properties.openDrawer}
|
||||||
|
canEdit={properties.canEdit}
|
||||||
|
model={model}
|
||||||
|
onUpdate={onChange}
|
||||||
|
>
|
||||||
<CellBarAddress>{cellAddress}</CellBarAddress>
|
<CellBarAddress>{cellAddress}</CellBarAddress>
|
||||||
|
<StyledIcon>
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</StyledIcon>
|
||||||
|
</FormulaBarMenu>
|
||||||
</AddressContainer>
|
</AddressContainer>
|
||||||
<Divider />
|
<Divider />
|
||||||
<FormulaContainer>
|
<FormulaContainer>
|
||||||
@@ -101,7 +123,7 @@ const Divider = styled("div")`
|
|||||||
background-color: ${theme.palette.grey["300"]};
|
background-color: ${theme.palette.grey["300"]};
|
||||||
min-width: 1px;
|
min-width: 1px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
margin: 0px 16px;
|
margin: 0px 16px 0px 8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FormulaContainer = styled("div")`
|
const FormulaContainer = styled("div")`
|
||||||
@@ -121,22 +143,46 @@ const Container = styled("div")`
|
|||||||
background: ${(properties): string =>
|
background: ${(properties): string =>
|
||||||
properties.theme.palette.background.default};
|
properties.theme.palette.background.default};
|
||||||
height: ${FORMULA_BAR_HEIGHT}px;
|
height: ${FORMULA_BAR_HEIGHT}px;
|
||||||
|
border-top: 1px solid ${theme.palette.grey["300"]};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const AddressContainer = styled("div")`
|
const AddressContainer = styled("div")<{ $active?: boolean }>`
|
||||||
padding-left: 16px;
|
|
||||||
color: ${theme.palette.common.black};
|
color: ${theme.palette.common.black};
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: normal;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
flex-grow: row;
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: ${(props) =>
|
||||||
|
props.$active ? theme.palette.action.selected : "transparent"};
|
||||||
|
&:hover {
|
||||||
|
background-color: ${(props) =>
|
||||||
|
props.$active ? theme.palette.action.selected : theme.palette.grey["100"]};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CellBarAddress = styled("div")`
|
const CellBarAddress = styled("div")`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: "center";
|
box-sizing: border-box;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding-left: 8px;
|
||||||
|
background-color: transparent;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIcon = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px 2px;
|
||||||
|
background-color: transparent;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EditorWrapper = styled("div")`
|
const EditorWrapper = styled("div")`
|
||||||
|
|||||||
170
webapp/IronCalc/src/components/FormulaBar/FormulaBarMenu.tsx
Normal file
170
webapp/IronCalc/src/components/FormulaBar/FormulaBarMenu.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import type { Model } from "@ironcalc/wasm";
|
||||||
|
import { Menu, MenuItem, styled } from "@mui/material";
|
||||||
|
import { Tag } from "lucide-react";
|
||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { theme } from "../../theme";
|
||||||
|
import { parseRangeInSheet } from "../Editor/util";
|
||||||
|
|
||||||
|
type FormulaBarMenuProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onMenuOpenChange: (isOpen: boolean) => void;
|
||||||
|
openDrawer: () => void;
|
||||||
|
canEdit: boolean;
|
||||||
|
model: Model;
|
||||||
|
onUpdate: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormulaBarMenu = (properties: FormulaBarMenuProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isMenuOpen, setMenuOpen] = useState(false);
|
||||||
|
const anchorElement = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleMenuOpen = useCallback((): void => {
|
||||||
|
setMenuOpen(true);
|
||||||
|
properties.onMenuOpenChange(true);
|
||||||
|
}, [properties.onMenuOpenChange]);
|
||||||
|
|
||||||
|
const handleMenuClose = useCallback((): void => {
|
||||||
|
setMenuOpen(false);
|
||||||
|
properties.onMenuOpenChange(false);
|
||||||
|
}, [properties.onMenuOpenChange]);
|
||||||
|
|
||||||
|
const definedNameList = properties.model.getDefinedNameList();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ChildrenWrapper onClick={handleMenuOpen} ref={anchorElement}>
|
||||||
|
{properties.children}
|
||||||
|
</ChildrenWrapper>
|
||||||
|
<StyledMenu
|
||||||
|
open={isMenuOpen}
|
||||||
|
onClose={handleMenuClose}
|
||||||
|
anchorEl={anchorElement.current}
|
||||||
|
marginThreshold={0}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "left",
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "left",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{definedNameList.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{definedNameList.map((definedName) => {
|
||||||
|
return (
|
||||||
|
<MenuItemWrapper
|
||||||
|
key={`${definedName.name}-${definedName.scope}`}
|
||||||
|
disableRipple
|
||||||
|
onClick={() => {
|
||||||
|
// select the area corresponding to the defined name
|
||||||
|
const formula = definedName.formula;
|
||||||
|
const range = parseRangeInSheet(properties.model, formula);
|
||||||
|
if (range) {
|
||||||
|
const [
|
||||||
|
sheetIndex,
|
||||||
|
rowStart,
|
||||||
|
columnStart,
|
||||||
|
rowEnd,
|
||||||
|
columnEnd,
|
||||||
|
] = range;
|
||||||
|
properties.model.setSelectedSheet(sheetIndex);
|
||||||
|
properties.model.setSelectedCell(rowStart, columnStart);
|
||||||
|
properties.model.setSelectedRange(
|
||||||
|
rowStart,
|
||||||
|
columnStart,
|
||||||
|
rowEnd,
|
||||||
|
columnEnd,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
properties.onUpdate();
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tag />
|
||||||
|
<MenuItemText>{definedName.name}</MenuItemText>
|
||||||
|
<MenuItemExample>{definedName.formula}</MenuItemExample>
|
||||||
|
</MenuItemWrapper>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<MenuDivider />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<MenuItemWrapper
|
||||||
|
onClick={() => {
|
||||||
|
properties.openDrawer();
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
disabled={!properties.canEdit}
|
||||||
|
disableRipple
|
||||||
|
>
|
||||||
|
<MenuItemText>{t("formula_bar.manage_named_ranges")}</MenuItemText>
|
||||||
|
</MenuItemWrapper>
|
||||||
|
</StyledMenu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledMenu = styled(Menu)`
|
||||||
|
top: 4px;
|
||||||
|
min-width: 260px;
|
||||||
|
max-width: 460px;
|
||||||
|
& .MuiPaper-root {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 0px;
|
||||||
|
margin-left: -4px;
|
||||||
|
}
|
||||||
|
& .MuiList-root {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuItemWrapper = styled(MenuItem)`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
gap: 8px;
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
min-width: 172px;
|
||||||
|
margin: 0px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
height: 32px;
|
||||||
|
& svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: ${theme.palette.grey[600]};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ChildrenWrapper = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuDivider = styled("div")`
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-top: 1px solid ${theme.palette.grey[200]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuItemText = styled("div")`
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: ${theme.palette.common.black};
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuItemExample = styled("div")`
|
||||||
|
color: ${theme.palette.grey[400]};
|
||||||
|
margin-left: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default FormulaBarMenu;
|
||||||
@@ -398,7 +398,9 @@ const ListItem = styled("div")<{ $isSelected: boolean }>(({ $isSelected }) => ({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
|
gap: "8px",
|
||||||
padding: "8px 12px",
|
padding: "8px 12px",
|
||||||
|
cursor: "pointer",
|
||||||
minHeight: "40px",
|
minHeight: "40px",
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
borderBottom: `1px solid ${theme.palette.grey[200]}`,
|
borderBottom: `1px solid ${theme.palette.grey[200]}`,
|
||||||
@@ -438,6 +440,8 @@ const NameText = styled("span")({
|
|||||||
fontSize: "12px",
|
fontSize: "12px",
|
||||||
color: theme.palette.common.black,
|
color: theme.palette.common.black,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
|
wordBreak: "break-all",
|
||||||
|
overflowWrap: "break-word",
|
||||||
});
|
});
|
||||||
|
|
||||||
const IconsWrapper = styled("div")({
|
const IconsWrapper = styled("div")({
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import {
|
|||||||
Redo2,
|
Redo2,
|
||||||
RemoveFormatting,
|
RemoveFormatting,
|
||||||
Strikethrough,
|
Strikethrough,
|
||||||
Tags,
|
|
||||||
Type,
|
Type,
|
||||||
Underline,
|
Underline,
|
||||||
Undo2,
|
Undo2,
|
||||||
@@ -87,7 +86,6 @@ type ToolbarProperties = {
|
|||||||
numFmt: string;
|
numFmt: string;
|
||||||
showGridLines: boolean;
|
showGridLines: boolean;
|
||||||
onToggleShowGridLines: (show: boolean) => void;
|
onToggleShowGridLines: (show: boolean) => void;
|
||||||
openDrawer: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function Toolbar(properties: ToolbarProperties) {
|
function Toolbar(properties: ToolbarProperties) {
|
||||||
@@ -514,18 +512,6 @@ function Toolbar(properties: ToolbarProperties) {
|
|||||||
{properties.showGridLines ? <Grid2x2Check /> : <Grid2x2X />}
|
{properties.showGridLines ? <Grid2x2Check /> : <Grid2x2X />}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("toolbar.named_ranges")}>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
onClick={() => {
|
|
||||||
properties.openDrawer();
|
|
||||||
}}
|
|
||||||
disabled={!canEdit}
|
|
||||||
>
|
|
||||||
<Tags />
|
|
||||||
</StyledButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={t("toolbar.selected_png")}>
|
<Tooltip title={t("toolbar.selected_png")}>
|
||||||
<StyledButton
|
<StyledButton
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -665,9 +665,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
model.setShowGridLines(sheet, show);
|
model.setShowGridLines(sheet, show);
|
||||||
setRedrawId((id) => id + 1);
|
setRedrawId((id) => id + 1);
|
||||||
}}
|
}}
|
||||||
openDrawer={() => {
|
|
||||||
setDrawerOpen(true);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<WorksheetAreaLeft $drawerWidth={isDrawerOpen ? drawerWidth : 0}>
|
<WorksheetAreaLeft $drawerWidth={isDrawerOpen ? drawerWidth : 0}>
|
||||||
<FormulaBar
|
<FormulaBar
|
||||||
@@ -682,6 +679,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
}}
|
}}
|
||||||
model={model}
|
model={model}
|
||||||
workbookState={workbookState}
|
workbookState={workbookState}
|
||||||
|
openDrawer={() => {
|
||||||
|
setDrawerOpen(true);
|
||||||
|
}}
|
||||||
|
canEdit={true}
|
||||||
/>
|
/>
|
||||||
<Worksheet
|
<Worksheet
|
||||||
model={model}
|
model={model}
|
||||||
@@ -762,9 +763,9 @@ type WorksheetAreaLeftProps = { $drawerWidth: number };
|
|||||||
const WorksheetAreaLeft = styled("div")<WorksheetAreaLeftProps>(
|
const WorksheetAreaLeft = styled("div")<WorksheetAreaLeftProps>(
|
||||||
({ $drawerWidth }) => ({
|
({ $drawerWidth }) => ({
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: `${TOOLBAR_HEIGHT + 1}px`,
|
top: `${TOOLBAR_HEIGHT}px`,
|
||||||
width: `calc(100% - ${$drawerWidth}px)`,
|
width: `calc(100% - ${$drawerWidth}px)`,
|
||||||
height: `calc(100% - ${TOOLBAR_HEIGHT + 1}px)`,
|
height: `calc(100% - ${TOOLBAR_HEIGHT}px)`,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { CellStyle, Model } from "@ironcalc/wasm";
|
import type { CellStyle, Model } from "@ironcalc/wasm";
|
||||||
import { columnNameFromNumber } from "@ironcalc/wasm";
|
import { columnNameFromNumber } from "@ironcalc/wasm";
|
||||||
|
import { theme } from "../../theme";
|
||||||
import { getColor } from "../Editor/util";
|
import { getColor } from "../Editor/util";
|
||||||
import type { Cell } from "../types";
|
import type { Cell } from "../types";
|
||||||
import type { WorkbookState } from "../workbookState";
|
import type { WorkbookState } from "../workbookState";
|
||||||
@@ -721,7 +722,7 @@ export default class WorksheetCanvas {
|
|||||||
const style = this.model.getCellStyle(selectedSheet, row, column);
|
const style = this.model.getCellStyle(selectedSheet, row, column);
|
||||||
|
|
||||||
// first the background
|
// first the background
|
||||||
let backgroundColor = "#FFFFFF";
|
let backgroundColor = theme.palette.common.white;
|
||||||
if (style.fill.fg_color) {
|
if (style.fill.fg_color) {
|
||||||
backgroundColor = style.fill.fg_color;
|
backgroundColor = style.fill.fg_color;
|
||||||
}
|
}
|
||||||
@@ -1036,14 +1037,21 @@ export default class WorksheetCanvas {
|
|||||||
width: number,
|
width: number,
|
||||||
div: HTMLDivElement,
|
div: HTMLDivElement,
|
||||||
selected: boolean,
|
selected: boolean,
|
||||||
|
isFullColumnSelected: boolean,
|
||||||
): void {
|
): void {
|
||||||
div.style.boxSizing = "border-box";
|
div.style.boxSizing = "border-box";
|
||||||
div.style.width = `${width}px`;
|
div.style.width = `${width}px`;
|
||||||
div.style.height = `${headerRowHeight}px`;
|
div.style.height = `${headerRowHeight}px`;
|
||||||
div.style.backgroundColor = selected
|
div.style.backgroundColor = selected
|
||||||
? headerSelectedBackground
|
? isFullColumnSelected
|
||||||
|
? theme.palette.primary.main
|
||||||
|
: headerSelectedBackground
|
||||||
: headerBackground;
|
: headerBackground;
|
||||||
div.style.color = selected ? headerSelectedColor : headerTextColor;
|
div.style.color = selected
|
||||||
|
? isFullColumnSelected
|
||||||
|
? theme.palette.common.white
|
||||||
|
: headerSelectedColor
|
||||||
|
: headerTextColor;
|
||||||
div.style.fontWeight = "bold";
|
div.style.fontWeight = "bold";
|
||||||
div.style.borderLeft = `1px solid ${headerBorderColor}`;
|
div.style.borderLeft = `1px solid ${headerBorderColor}`;
|
||||||
div.style.borderTop = `1px solid ${headerBorderColor}`;
|
div.style.borderTop = `1px solid ${headerBorderColor}`;
|
||||||
@@ -1071,9 +1079,15 @@ export default class WorksheetCanvas {
|
|||||||
const { sheet: selectedSheet, range } = this.model.getSelectedView();
|
const { sheet: selectedSheet, range } = this.model.getSelectedView();
|
||||||
let rowStart = range[0];
|
let rowStart = range[0];
|
||||||
let rowEnd = range[2];
|
let rowEnd = range[2];
|
||||||
|
let columnStart = range[1];
|
||||||
|
let columnEnd = range[3];
|
||||||
if (rowStart > rowEnd) {
|
if (rowStart > rowEnd) {
|
||||||
[rowStart, rowEnd] = [rowEnd, rowStart];
|
[rowStart, rowEnd] = [rowEnd, rowStart];
|
||||||
}
|
}
|
||||||
|
if (columnStart > columnEnd) {
|
||||||
|
[columnStart, columnEnd] = [columnEnd, columnStart];
|
||||||
|
}
|
||||||
|
const isFullRowSelected = columnStart === 1 && columnEnd === LAST_COLUMN;
|
||||||
const context = this.ctx;
|
const context = this.ctx;
|
||||||
|
|
||||||
let topLeftCornerY = headerRowHeight + 0.5;
|
let topLeftCornerY = headerRowHeight + 0.5;
|
||||||
@@ -1085,7 +1099,9 @@ export default class WorksheetCanvas {
|
|||||||
context.fillStyle = headerBorderColor;
|
context.fillStyle = headerBorderColor;
|
||||||
context.fillRect(0.5, topLeftCornerY, headerColumnWidth, rowHeight);
|
context.fillRect(0.5, topLeftCornerY, headerColumnWidth, rowHeight);
|
||||||
context.fillStyle = selected
|
context.fillStyle = selected
|
||||||
? headerSelectedBackground
|
? isFullRowSelected
|
||||||
|
? theme.palette.primary.main
|
||||||
|
: headerSelectedBackground
|
||||||
: headerBackground;
|
: headerBackground;
|
||||||
context.fillRect(
|
context.fillRect(
|
||||||
0.5,
|
0.5,
|
||||||
@@ -1097,7 +1113,11 @@ export default class WorksheetCanvas {
|
|||||||
context.fillStyle = outlineColor;
|
context.fillStyle = outlineColor;
|
||||||
context.fillRect(headerColumnWidth - 1, topLeftCornerY, 1, rowHeight);
|
context.fillRect(headerColumnWidth - 1, topLeftCornerY, 1, rowHeight);
|
||||||
}
|
}
|
||||||
context.fillStyle = selected ? headerSelectedColor : headerTextColor;
|
context.fillStyle = selected
|
||||||
|
? isFullRowSelected
|
||||||
|
? theme.palette.common.white
|
||||||
|
: headerSelectedColor
|
||||||
|
: headerTextColor;
|
||||||
context.font = `bold 12px ${defaultCellFontFamily}`;
|
context.font = `bold 12px ${defaultCellFontFamily}`;
|
||||||
context.fillText(
|
context.fillText(
|
||||||
`${row}`,
|
`${row}`,
|
||||||
@@ -1122,11 +1142,17 @@ export default class WorksheetCanvas {
|
|||||||
const { columnHeaders } = this;
|
const { columnHeaders } = this;
|
||||||
let deltaX = 0;
|
let deltaX = 0;
|
||||||
const { range } = this.model.getSelectedView();
|
const { range } = this.model.getSelectedView();
|
||||||
|
let rowStart = range[0];
|
||||||
|
let rowEnd = range[2];
|
||||||
let columnStart = range[1];
|
let columnStart = range[1];
|
||||||
let columnEnd = range[3];
|
let columnEnd = range[3];
|
||||||
if (columnStart > columnEnd) {
|
if (columnStart > columnEnd) {
|
||||||
[columnStart, columnEnd] = [columnEnd, columnStart];
|
[columnStart, columnEnd] = [columnEnd, columnStart];
|
||||||
}
|
}
|
||||||
|
if (rowStart > rowEnd) {
|
||||||
|
[rowStart, rowEnd] = [rowEnd, rowStart];
|
||||||
|
}
|
||||||
|
const isFullColumnSelected = rowStart === 1 && rowEnd === LAST_ROW;
|
||||||
for (const header of columnHeaders.querySelectorAll(".column-header"))
|
for (const header of columnHeaders.querySelectorAll(".column-header"))
|
||||||
header.remove();
|
header.remove();
|
||||||
for (const handle of columnHeaders.querySelectorAll(
|
for (const handle of columnHeaders.querySelectorAll(
|
||||||
@@ -1146,7 +1172,12 @@ export default class WorksheetCanvas {
|
|||||||
// Frozen headers
|
// Frozen headers
|
||||||
for (let column = 1; column <= frozenColumns; column += 1) {
|
for (let column = 1; column <= frozenColumns; column += 1) {
|
||||||
const selected = column >= columnStart && column <= columnEnd;
|
const selected = column >= columnStart && column <= columnEnd;
|
||||||
deltaX += this.addColumnHeader(deltaX, column, selected);
|
deltaX += this.addColumnHeader(
|
||||||
|
deltaX,
|
||||||
|
column,
|
||||||
|
selected,
|
||||||
|
isFullColumnSelected,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frozenColumns !== 0) {
|
if (frozenColumns !== 0) {
|
||||||
@@ -1162,7 +1193,12 @@ export default class WorksheetCanvas {
|
|||||||
|
|
||||||
for (let column = firstColumn; column <= lastColumn; column += 1) {
|
for (let column = firstColumn; column <= lastColumn; column += 1) {
|
||||||
const selected = column >= columnStart && column <= columnEnd;
|
const selected = column >= columnStart && column <= columnEnd;
|
||||||
deltaX += this.addColumnHeader(deltaX, column, selected);
|
deltaX += this.addColumnHeader(
|
||||||
|
deltaX,
|
||||||
|
column,
|
||||||
|
selected,
|
||||||
|
isFullColumnSelected,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
columnHeaders.style.width = `${deltaX}px`;
|
columnHeaders.style.width = `${deltaX}px`;
|
||||||
@@ -1172,6 +1208,7 @@ export default class WorksheetCanvas {
|
|||||||
deltaX: number,
|
deltaX: number,
|
||||||
column: number,
|
column: number,
|
||||||
selected: boolean,
|
selected: boolean,
|
||||||
|
isFullColumnSelected: boolean,
|
||||||
): number {
|
): number {
|
||||||
const columnWidth = this.getColumnWidth(
|
const columnWidth = this.getColumnWidth(
|
||||||
this.model.getSelectedSheet(),
|
this.model.getSelectedSheet(),
|
||||||
@@ -1182,7 +1219,7 @@ export default class WorksheetCanvas {
|
|||||||
div.textContent = columnNameFromNumber(column);
|
div.textContent = columnNameFromNumber(column);
|
||||||
this.columnHeaders.insertBefore(div, null);
|
this.columnHeaders.insertBefore(div, null);
|
||||||
|
|
||||||
this.styleColumnHeader(columnWidth, div, selected);
|
this.styleColumnHeader(columnWidth, div, selected, isFullColumnSelected);
|
||||||
this.addColumnResizeHandle(deltaX + columnWidth, column, columnWidth);
|
this.addColumnResizeHandle(deltaX + columnWidth, column, columnWidth);
|
||||||
return columnWidth;
|
return columnWidth;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,9 @@
|
|||||||
"label": "Formula",
|
"label": "Formula",
|
||||||
"title": "Update formula"
|
"title": "Update formula"
|
||||||
},
|
},
|
||||||
|
"formula_bar": {
|
||||||
|
"manage_named_ranges": "Manage Named Ranges"
|
||||||
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"add_sheet": "Add sheet",
|
"add_sheet": "Add sheet",
|
||||||
"sheet_list": "Sheet list",
|
"sheet_list": "Sheet list",
|
||||||
|
|||||||
25
webapp/app.ironcalc.com/frontend/package-lock.json
generated
25
webapp/app.ironcalc.com/frontend/package-lock.json
generated
@@ -91,6 +91,7 @@
|
|||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -570,6 +571,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||||
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
@@ -613,6 +615,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
|
||||||
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
|
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
@@ -2026,6 +2029,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.4.tgz",
|
||||||
"integrity": "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==",
|
"integrity": "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -2122,6 +2126,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.8.25",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001754",
|
||||||
@@ -2691,6 +2696,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -2758,6 +2764,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -2767,6 +2774,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -2978,6 +2986,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -3023,6 +3032,7 @@
|
|||||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -3113,21 +3123,6 @@
|
|||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
|
||||||
"node_modules/yaml": {
|
|
||||||
"version": "2.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
|
|
||||||
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"yaml": "bin.mjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.6"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,16 +227,19 @@ const Wrapper = styled("div")`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const DRAWER_WIDTH = 264;
|
const DRAWER_WIDTH = 264;
|
||||||
const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 440;
|
export const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 768;
|
||||||
|
|
||||||
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
|
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
|
||||||
margin-left: ${({ isDrawerOpen }) => (isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`)};
|
margin-left: ${({ isDrawerOpen }) =>
|
||||||
width: ${({ isDrawerOpen }) => (isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : "100%")};
|
isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`};
|
||||||
|
width: ${({ isDrawerOpen }) =>
|
||||||
|
isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : "100%"};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
@media (max-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px) {
|
@media (max-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px) {
|
||||||
${({ isDrawerOpen }) => isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`}
|
${({ isDrawerOpen }) =>
|
||||||
|
isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -247,7 +250,7 @@ const MobileOverlay = styled("div")`
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
z-index: 1;
|
z-index: 100;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
@media (min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE + 1}px) {
|
@media (min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE + 1}px) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Model } from "@ironcalc/workbook";
|
|||||||
import { IconButton, Tooltip } from "@mui/material";
|
import { IconButton, Tooltip } from "@mui/material";
|
||||||
import { CloudOff, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
import { CloudOff, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
||||||
import { useLayoutEffect, useRef, useState } from "react";
|
import { useLayoutEffect, useRef, useState } from "react";
|
||||||
|
import { MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE } from "../App";
|
||||||
import { FileMenu } from "./FileMenu";
|
import { FileMenu } from "./FileMenu";
|
||||||
import { HelpMenu } from "./HelpMenu";
|
import { HelpMenu } from "./HelpMenu";
|
||||||
import { downloadModel } from "./rpc";
|
import { downloadModel } from "./rpc";
|
||||||
@@ -77,7 +78,7 @@ export function FileBar(properties: {
|
|||||||
{properties.isDrawerOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
|
{properties.isDrawerOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
|
||||||
</DrawerButton>
|
</DrawerButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{width > 440 && (
|
{width > MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE && (
|
||||||
<FileMenu
|
<FileMenu
|
||||||
newModel={properties.newModel}
|
newModel={properties.newModel}
|
||||||
newModelFromTemplate={properties.newModelFromTemplate}
|
newModelFromTemplate={properties.newModelFromTemplate}
|
||||||
@@ -92,7 +93,7 @@ export function FileBar(properties: {
|
|||||||
onDelete={properties.onDelete}
|
onDelete={properties.onDelete}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{width > 440 && <HelpMenu />}
|
{width > MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE && <HelpMenu />}
|
||||||
<WorkbookTitleWrapper>
|
<WorkbookTitleWrapper>
|
||||||
<WorkbookTitle
|
<WorkbookTitle
|
||||||
name={properties.model.getName()}
|
name={properties.model.getName()}
|
||||||
@@ -112,7 +113,8 @@ export function FileBar(properties: {
|
|||||||
<div style={{ fontWeight: "bold" }}>{cloudWarningText2}</div>
|
<div style={{ fontWeight: "bold" }}>{cloudWarningText2}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
placement="bottom-start"
|
placement="bottom"
|
||||||
|
enterTouchDelay={0}
|
||||||
enterDelay={500}
|
enterDelay={500}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
popper: {
|
popper: {
|
||||||
|
|||||||
BIN
xlsx/tests/calc_tests/BETA_GAMMA.xlsx
Normal file
BIN
xlsx/tests/calc_tests/BETA_GAMMA.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/CELL.xlsx
Normal file
BIN
xlsx/tests/calc_tests/CELL.xlsx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/calc_tests/MINA_MAXA.xlsx
Normal file
BIN
xlsx/tests/calc_tests/MINA_MAXA.xlsx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/calc_tests/N.xlsx
Normal file
BIN
xlsx/tests/calc_tests/N.xlsx
Normal file
Binary file not shown.
Binary file not shown.
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.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user