Compare commits
5 Commits
main
...
dani/widge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ce34045d6 | ||
|
|
bcd1f66c9c | ||
|
|
5a891483b6 | ||
|
|
0eafc9b599 | ||
|
|
e48e539bd6 |
@@ -12,9 +12,6 @@ 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,20 +471,6 @@ 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 {
|
||||||
@@ -504,20 +490,6 @@ 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,7 +711,6 @@ 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],
|
||||||
@@ -872,10 +871,12 @@ 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],
|
||||||
@@ -888,125 +889,6 @@ 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],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1032,7 +914,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),
|
Function::Choose => scalar_arguments(args), // static_analysis_choose(args, cell),
|
||||||
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),
|
||||||
@@ -1079,6 +961,7 @@ 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),
|
||||||
@@ -1107,7 +990,6 @@ 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),
|
||||||
@@ -1270,6 +1152,7 @@ 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),
|
||||||
@@ -1282,81 +1165,5 @@ 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,7 +4,6 @@ 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;
|
||||||
@@ -771,12 +770,12 @@ impl Model {
|
|||||||
Ok(values)
|
Ok(values)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the current date/time as an Excel serial number in the given timezone.
|
// Returns the current date/time as an Excel serial number in the model's configured timezone.
|
||||||
// Used by TODAY() and NOW().
|
// Used by TODAY() and NOW().
|
||||||
pub(crate) fn current_excel_serial_with_timezone(&self, tz: Tz) -> Option<f64> {
|
fn current_excel_serial(&self) -> 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(&tz);
|
let local_time = dt.with_timezone(&self.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
|
||||||
@@ -979,7 +978,7 @@ impl Model {
|
|||||||
message: "Wrong number of arguments".to_string(),
|
message: "Wrong number of arguments".to_string(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
match self.current_excel_serial_with_timezone(self.tz) {
|
match self.current_excel_serial() {
|
||||||
Some(serial) => CalcResult::Number(serial.floor()),
|
Some(serial) => CalcResult::Number(serial.floor()),
|
||||||
None => CalcResult::Error {
|
None => CalcResult::Error {
|
||||||
error: Error::ERROR,
|
error: Error::ERROR,
|
||||||
@@ -990,35 +989,14 @@ 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.len() > 1 {
|
if !args.is_empty() {
|
||||||
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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let tz = match args.first() {
|
match self.current_excel_serial() {
|
||||||
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::{EXCEL_PRECISION, LAST_COLUMN, LAST_ROW};
|
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||||
use crate::expressions::parser::ArrayNode;
|
use crate::expressions::parser::ArrayNode;
|
||||||
use crate::expressions::types::CellReferenceIndex;
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
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_excel_precision, to_precision};
|
use crate::number_format::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,9 +984,7 @@ impl Model {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply Excel precision to the ratio to handle floating-point rounding errors
|
let result = f64::floor(value / significance) * significance;
|
||||||
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
|
||||||
let result = f64::floor(ratio) * significance;
|
|
||||||
CalcResult::Number(result)
|
CalcResult::Number(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1024,7 +1022,7 @@ impl Model {
|
|||||||
cell: CellReferenceIndex,
|
cell: CellReferenceIndex,
|
||||||
) -> CalcResult {
|
) -> CalcResult {
|
||||||
let arg_count = args.len();
|
let arg_count = args.len();
|
||||||
if !(1..=3).contains(&arg_count) {
|
if arg_count > 3 {
|
||||||
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) {
|
||||||
@@ -1065,7 +1063,7 @@ impl Model {
|
|||||||
cell: CellReferenceIndex,
|
cell: CellReferenceIndex,
|
||||||
) -> CalcResult {
|
) -> CalcResult {
|
||||||
let arg_count = args.len();
|
let arg_count = args.len();
|
||||||
if !(1..=2).contains(&arg_count) {
|
if arg_count > 2 {
|
||||||
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) {
|
||||||
@@ -1095,7 +1093,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 !(1..=3).contains(&arg_count) {
|
if arg_count > 3 {
|
||||||
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) {
|
||||||
@@ -1123,14 +1121,10 @@ impl Model {
|
|||||||
}
|
}
|
||||||
let significance = significance.abs();
|
let significance = significance.abs();
|
||||||
if value < 0.0 && mode != 0.0 {
|
if value < 0.0 && mode != 0.0 {
|
||||||
// Apply Excel precision to handle floating-point rounding errors
|
let result = f64::ceil(value / significance) * significance;
|
||||||
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
|
||||||
let result = f64::ceil(ratio) * significance;
|
|
||||||
CalcResult::Number(result)
|
CalcResult::Number(result)
|
||||||
} else {
|
} else {
|
||||||
// Apply Excel precision to handle floating-point rounding errors
|
let result = f64::floor(value / significance) * significance;
|
||||||
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
|
||||||
let result = f64::floor(ratio) * significance;
|
|
||||||
CalcResult::Number(result)
|
CalcResult::Number(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1141,7 +1135,7 @@ impl Model {
|
|||||||
cell: CellReferenceIndex,
|
cell: CellReferenceIndex,
|
||||||
) -> CalcResult {
|
) -> CalcResult {
|
||||||
let arg_count = args.len();
|
let arg_count = args.len();
|
||||||
if !(1..=2).contains(&arg_count) {
|
if arg_count > 2 {
|
||||||
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) {
|
||||||
@@ -1160,9 +1154,7 @@ impl Model {
|
|||||||
return CalcResult::Number(0.0);
|
return CalcResult::Number(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply Excel precision to handle floating-point rounding errors
|
let result = f64::floor(value / significance) * significance;
|
||||||
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
|
||||||
let result = f64::floor(ratio) * significance;
|
|
||||||
CalcResult::Number(result)
|
CalcResult::Number(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1217,7 +1209,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 !(1..=2).contains(&args.len()) {
|
if args.len() > 2 {
|
||||||
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) {
|
||||||
|
|||||||
@@ -1,230 +0,0 @@
|
|||||||
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,7 +19,6 @@ 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;
|
||||||
@@ -77,9 +76,6 @@ pub enum Function {
|
|||||||
Sum,
|
Sum,
|
||||||
Sumif,
|
Sumif,
|
||||||
Sumifs,
|
Sumifs,
|
||||||
Sumx2my2,
|
|
||||||
Sumx2py2,
|
|
||||||
Sumxmy2,
|
|
||||||
Tan,
|
Tan,
|
||||||
Tanh,
|
Tanh,
|
||||||
Acot,
|
Acot,
|
||||||
@@ -194,92 +190,6 @@ 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,
|
||||||
@@ -415,16 +325,10 @@ pub enum Function {
|
|||||||
Dvar,
|
Dvar,
|
||||||
Dvarp,
|
Dvarp,
|
||||||
Dstdevp,
|
Dstdevp,
|
||||||
|
|
||||||
Correl,
|
|
||||||
Rsq,
|
|
||||||
Intercept,
|
|
||||||
Slope,
|
|
||||||
Steyx,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Function {
|
impl Function {
|
||||||
pub fn into_iter() -> IntoIter<Function, 345> {
|
pub fn into_iter() -> IntoIter<Function, 268> {
|
||||||
[
|
[
|
||||||
Function::And,
|
Function::And,
|
||||||
Function::False,
|
Function::False,
|
||||||
@@ -501,9 +405,6 @@ 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,
|
||||||
@@ -552,7 +453,6 @@ 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,
|
||||||
@@ -698,79 +598,6 @@ 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()
|
||||||
}
|
}
|
||||||
@@ -832,70 +659,6 @@ 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(),
|
||||||
}
|
}
|
||||||
@@ -1048,7 +811,6 @@ 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),
|
||||||
@@ -1195,85 +957,6 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1382,7 +1065,6 @@ 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"),
|
||||||
@@ -1535,6 +1217,7 @@ 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"),
|
||||||
@@ -1551,83 +1234,6 @@ impl fmt::Display for Function {
|
|||||||
Function::Dvar => write!(f, "DVAR"),
|
Function::Dvar => write!(f, "DVAR"),
|
||||||
Function::Dvarp => write!(f, "DVARP"),
|
Function::Dvarp => write!(f, "DVARP"),
|
||||||
Function::Dstdevp => write!(f, "DSTDEVP"),
|
Function::Dstdevp => write!(f, "DSTDEVP"),
|
||||||
Function::BetaDist => write!(f, "BETA.DIST"),
|
|
||||||
Function::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"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1748,7 +1354,6 @@ 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),
|
||||||
@@ -1925,82 +1530,6 @@ 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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
733
base/src/functions/statistical.rs
Normal file
733
base/src/functions/statistical.rs
Normal file
@@ -0,0 +1,733 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,397 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,264 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,418 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,519 +0,0 @@
|
|||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,588 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,518 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
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,5 +1,4 @@
|
|||||||
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;
|
||||||
@@ -40,14 +39,12 @@ 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;
|
||||||
@@ -58,18 +55,12 @@ 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;
|
||||||
@@ -89,6 +80,5 @@ 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;
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
#![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");
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
#![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!");
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
#![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!");
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
#![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");
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
#![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!");
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
#![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");
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#![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");
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
#![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!");
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
#![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!");
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
#![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!");
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#![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!");
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
#![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!");
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#![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!");
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
#![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!");
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
#![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!");
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#![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");
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#![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!");
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#![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");
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#![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");
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
#![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!");
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#![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");
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#![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");
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#![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!");
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#![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)");
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
#![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("A7", "=COMBIN(1, 2, 3)");
|
model._set("A5", "=COMBIN(1, 2, 3)");
|
||||||
model._set("A8", "=COMBINA(1, 2, 3)");
|
model._set("A6", "=COMBINA(1, 2, 3)");
|
||||||
|
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
@@ -24,4 +24,4 @@ fn arguments() {
|
|||||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||||
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||||
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||||
}
|
}
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
#![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,3 +20,5 @@ 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,6 +20,7 @@ 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!");
|
||||||
@@ -28,7 +29,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"), *"10");
|
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||||
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,24 +7,15 @@ 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, 1)");
|
model._set("A1", "=NOW(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!",
|
||||||
"Wrong number of arguments"
|
"NOW should not accept arguments"
|
||||||
);
|
|
||||||
assert_eq!(model._get_text("A2"), *"20/03/2023 14:44:25");
|
|
||||||
assert_eq!(
|
|
||||||
model._get_text("A3"),
|
|
||||||
"#VALUE!",
|
|
||||||
"Invalid timezone: faketimezone"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
#![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="tip" text="Available" /> | – |
|
| CELL | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| 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="tip" text="Available" /> | – |
|
| ISNA | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| 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="tip" text="Available" /> | – |
|
| SHEETS | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| TYPE | <Badge type="tip" text="Available" /> | – |
|
| TYPE | <Badge type="tip" text="Available" /> | – |
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# CELL
|
# CELL
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 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).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# N
|
# N
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 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).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -7,5 +7,6 @@ lang: en-US
|
|||||||
# SHEETS
|
# SHEETS
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 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).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -6,33 +6,26 @@ 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 cell, column, range, or [Named Range](/web-application/name-manager.html) for which you wish to find the column number.
|
* *reference* ([cell](/features/value-types#references), [optional](/features/optional-arguments.md)). The number of the cell you wish to reference the column number of.
|
||||||
### 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 left most 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. If no reference is included, the column number of the cell where the formula is entered will be returned.
|
COLUMN returns the [number](/features/value-types#numbers) of the specific cell or column which is being referenced.
|
||||||
### Error conditions
|
### Error conditions
|
||||||
* A [#NAME?](/features/error-types.html#name) error is returned if a Named Range being referenced is deleted.
|
* IronCalc currently does not support the referencing of cells with names.
|
||||||
* 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](/functions/lookup_and_reference/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 Function.
|
||||||
## Examples
|
## Examples
|
||||||
### No Cell Reference
|
### No Cell Reference
|
||||||
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.
|
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.
|
||||||
### With Cell Reference
|
### With Cell Reference
|
||||||
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".
|
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".
|
||||||
### Range References
|
### Range References
|
||||||
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".
|
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".
|
||||||
## 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="tip" text="Available" /> | – |
|
| SUMSQ | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| 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,5 +7,6 @@ lang: en-US
|
|||||||
# SUMSQ
|
# SUMSQ
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 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).
|
🚧 This function is not yet available in IronCalc.
|
||||||
|
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||||
:::
|
:::
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
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";
|
||||||
@@ -11,7 +9,6 @@ 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;
|
||||||
@@ -20,8 +17,6 @@ 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) {
|
||||||
@@ -33,27 +28,10 @@ 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 $active={isMenuOpen}>
|
<AddressContainer>
|
||||||
<FormulaBarMenu
|
<CellBarAddress>{cellAddress}</CellBarAddress>
|
||||||
onMenuOpenChange={handleMenuOpenChange}
|
|
||||||
openDrawer={properties.openDrawer}
|
|
||||||
canEdit={properties.canEdit}
|
|
||||||
model={model}
|
|
||||||
onUpdate={onChange}
|
|
||||||
>
|
|
||||||
<CellBarAddress>{cellAddress}</CellBarAddress>
|
|
||||||
<StyledIcon>
|
|
||||||
<ChevronDown size={16} />
|
|
||||||
</StyledIcon>
|
|
||||||
</FormulaBarMenu>
|
|
||||||
</AddressContainer>
|
</AddressContainer>
|
||||||
<Divider />
|
<Divider />
|
||||||
<FormulaContainer>
|
<FormulaContainer>
|
||||||
@@ -123,7 +101,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 0px 8px;
|
margin: 0px 16px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FormulaContainer = styled("div")`
|
const FormulaContainer = styled("div")`
|
||||||
@@ -143,46 +121,22 @@ 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")<{ $active?: boolean }>`
|
const AddressContainer = styled("div")`
|
||||||
|
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;
|
||||||
align-items: center;
|
flex-grow: row;
|
||||||
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%;
|
||||||
box-sizing: border-box;
|
text-align: "center";
|
||||||
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")`
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
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,9 +398,7 @@ 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]}`,
|
||||||
@@ -440,8 +438,6 @@ 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")({
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { styled, Tooltip } from "@mui/material";
|
import { styled, Tooltip } from "@mui/material";
|
||||||
import { Menu, Plus } from "lucide-react";
|
import { EllipsisVertical, Menu, Plus } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { IronCalcLogo } from "../../icons";
|
import { IronCalcLogo } from "../../icons";
|
||||||
import { theme } from "../../theme";
|
import { theme } from "../../theme";
|
||||||
import { NAVIGATION_HEIGHT } from "../constants";
|
import { NAVIGATION_HEIGHT } from "../constants";
|
||||||
import { StyledButton } from "../Toolbar/Toolbar";
|
import { StyledButton } from "../Toolbar/Toolbar";
|
||||||
|
import WorkbookSettingsDialog from "../WorkbookSettings/WorkbookSettingsDialog";
|
||||||
import type { WorkbookState } from "../workbookState";
|
import type { WorkbookState } from "../workbookState";
|
||||||
import SheetListMenu from "./SheetListMenu";
|
import SheetListMenu from "./SheetListMenu";
|
||||||
import SheetTab from "./SheetTab";
|
import SheetTab from "./SheetTab";
|
||||||
@@ -21,12 +22,16 @@ export interface SheetTabBarProps {
|
|||||||
onSheetRenamed: (name: string) => void;
|
onSheetRenamed: (name: string) => void;
|
||||||
onSheetDeleted: () => void;
|
onSheetDeleted: () => void;
|
||||||
onHideSheet: () => void;
|
onHideSheet: () => void;
|
||||||
|
onOpenWorkbookSettings: () => void;
|
||||||
|
initialLocale: string;
|
||||||
|
initialTimezone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetTabBar(props: SheetTabBarProps) {
|
function SheetTabBar(props: SheetTabBarProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { workbookState, onSheetSelected, sheets, selectedIndex } = props;
|
const { workbookState, onSheetSelected, sheets, selectedIndex } = props;
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
|
||||||
|
const [workbookSettingsOpen, setWorkbookSettingsOpen] = useState(false);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
@@ -95,6 +100,17 @@ function SheetTabBar(props: SheetTabBarProps) {
|
|||||||
<IronCalcLogo />
|
<IronCalcLogo />
|
||||||
</LogoLink>
|
</LogoLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip title={t("workbook_settings.open_settings")}>
|
||||||
|
<StyledButton
|
||||||
|
$pressed={false}
|
||||||
|
onClick={() => {
|
||||||
|
setWorkbookSettingsOpen(true);
|
||||||
|
props.onOpenWorkbookSettings();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EllipsisVertical />
|
||||||
|
</StyledButton>
|
||||||
|
</Tooltip>
|
||||||
</RightContainer>
|
</RightContainer>
|
||||||
<SheetListMenu
|
<SheetListMenu
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
@@ -107,6 +123,12 @@ function SheetTabBar(props: SheetTabBarProps) {
|
|||||||
}}
|
}}
|
||||||
selectedIndex={selectedIndex}
|
selectedIndex={selectedIndex}
|
||||||
/>
|
/>
|
||||||
|
<WorkbookSettingsDialog
|
||||||
|
open={workbookSettingsOpen}
|
||||||
|
onClose={() => setWorkbookSettingsOpen(false)}
|
||||||
|
initialLocale={props.initialLocale}
|
||||||
|
initialTimezone={props.initialTimezone}
|
||||||
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -170,22 +192,28 @@ const RightContainer = styled("a")`
|
|||||||
color: ${theme.palette.primary.main};
|
color: ${theme.palette.primary.main};
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0px 8px;
|
padding: 0px 8px;
|
||||||
@media (max-width: 769px) {
|
gap: 4px;
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const LogoLink = styled("div")`
|
const LogoLink = styled("div")`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px;
|
padding: 0px 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
max-height: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
cursor: pointer;
|
||||||
svg {
|
svg {
|
||||||
height: 14px;
|
height: 14px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: ${theme.palette.grey["100"]};
|
background-color: ${theme.palette.grey["100"]};
|
||||||
|
transition: "all 0.2s";
|
||||||
|
outline: 1px solid ${theme.palette.grey["200"]};
|
||||||
|
}
|
||||||
|
@media (max-width: 769px) {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
Redo2,
|
Redo2,
|
||||||
RemoveFormatting,
|
RemoveFormatting,
|
||||||
Strikethrough,
|
Strikethrough,
|
||||||
|
Tags,
|
||||||
Type,
|
Type,
|
||||||
Underline,
|
Underline,
|
||||||
Undo2,
|
Undo2,
|
||||||
@@ -86,6 +87,7 @@ 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) {
|
||||||
@@ -512,6 +514,18 @@ 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,6 +665,9 @@ 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
|
||||||
@@ -679,10 +682,6 @@ 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}
|
||||||
@@ -763,9 +762,9 @@ type WorksheetAreaLeftProps = { $drawerWidth: number };
|
|||||||
const WorksheetAreaLeft = styled("div")<WorksheetAreaLeftProps>(
|
const WorksheetAreaLeft = styled("div")<WorksheetAreaLeftProps>(
|
||||||
({ $drawerWidth }) => ({
|
({ $drawerWidth }) => ({
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: `${TOOLBAR_HEIGHT}px`,
|
top: `${TOOLBAR_HEIGHT + 1}px`,
|
||||||
width: `calc(100% - ${$drawerWidth}px)`,
|
width: `calc(100% - ${$drawerWidth}px)`,
|
||||||
height: `calc(100% - ${TOOLBAR_HEIGHT}px)`,
|
height: `calc(100% - ${TOOLBAR_HEIGHT + 1}px)`,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,429 @@
|
|||||||
|
import styled from "@emotion/styled";
|
||||||
|
import {
|
||||||
|
Autocomplete,
|
||||||
|
Box,
|
||||||
|
Dialog,
|
||||||
|
FormControl,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Check, X } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { theme } from "../../theme";
|
||||||
|
|
||||||
|
type WorkbookSettingsDialogProps = {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
initialLocale: string;
|
||||||
|
initialTimezone: string;
|
||||||
|
onSave?: (locale: string, timezone: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkbookSettingsDialog = (properties: WorkbookSettingsDialogProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const locales = ["en-US", "en-GB", "de-DE", "fr-FR", "es-ES"];
|
||||||
|
const timezones = [
|
||||||
|
"Berlin, Germany (GMT+1)",
|
||||||
|
"New York, USA (GMT-5)",
|
||||||
|
"Tokyo, Japan (GMT+9)",
|
||||||
|
"London, UK (GMT+0)",
|
||||||
|
"Sydney, Australia (GMT+10)",
|
||||||
|
];
|
||||||
|
const [selectedLocale, setSelectedLocale] = useState<string>(
|
||||||
|
properties.initialLocale && locales.includes(properties.initialLocale)
|
||||||
|
? properties.initialLocale
|
||||||
|
: locales[0],
|
||||||
|
);
|
||||||
|
const [selectedTimezone, setSelectedTimezone] = useState<string>(
|
||||||
|
properties.initialTimezone && timezones.includes(properties.initialTimezone)
|
||||||
|
? properties.initialTimezone
|
||||||
|
: timezones[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (properties.onSave && selectedLocale && selectedTimezone) {
|
||||||
|
properties.onSave(selectedLocale, selectedTimezone);
|
||||||
|
}
|
||||||
|
properties.onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure selectedLocale is always a valid locale
|
||||||
|
const validSelectedLocale =
|
||||||
|
selectedLocale && locales.includes(selectedLocale)
|
||||||
|
? selectedLocale
|
||||||
|
: locales[0];
|
||||||
|
|
||||||
|
// Ensure selectedTimezone is always a valid timezone
|
||||||
|
const validSelectedTimezone =
|
||||||
|
selectedTimezone && timezones.includes(selectedTimezone)
|
||||||
|
? selectedTimezone
|
||||||
|
: timezones[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledDialog
|
||||||
|
open={properties.open}
|
||||||
|
onClose={(_event, reason) => {
|
||||||
|
if (reason === "backdropClick" || reason === "escapeKeyDown") {
|
||||||
|
properties.onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledDialogTitle>
|
||||||
|
{t("workbook_settings.title")}
|
||||||
|
<Cross
|
||||||
|
onClick={properties.onClose}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
properties.onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X />
|
||||||
|
</Cross>
|
||||||
|
</StyledDialogTitle>
|
||||||
|
|
||||||
|
<StyledDialogContent
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<StyledSectionTitle>
|
||||||
|
{t("workbook_settings.locale_and_timezone.title")}
|
||||||
|
</StyledSectionTitle>
|
||||||
|
<FieldWrapper>
|
||||||
|
<StyledLabel htmlFor="locale">
|
||||||
|
{t("workbook_settings.locale_and_timezone.locale_label")}
|
||||||
|
</StyledLabel>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<StyledSelect
|
||||||
|
id="locale"
|
||||||
|
value={validSelectedLocale}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSelectedLocale(event.target.value as string);
|
||||||
|
}}
|
||||||
|
MenuProps={{
|
||||||
|
PaperProps: {
|
||||||
|
sx: menuPaperStyles,
|
||||||
|
},
|
||||||
|
TransitionProps: {
|
||||||
|
timeout: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{locales.map((locale) => (
|
||||||
|
<StyledMenuItem
|
||||||
|
key={locale}
|
||||||
|
value={locale}
|
||||||
|
$isSelected={locale === selectedLocale}
|
||||||
|
>
|
||||||
|
{locale}
|
||||||
|
</StyledMenuItem>
|
||||||
|
))}
|
||||||
|
</StyledSelect>
|
||||||
|
<HelperBox>
|
||||||
|
<Row>
|
||||||
|
{t("workbook_settings.locale_and_timezone.locale_example1")}
|
||||||
|
<RowValue>1,234.56</RowValue>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
{t("workbook_settings.locale_and_timezone.locale_example2")}
|
||||||
|
<RowValue>12/31/2025</RowValue>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
{t("workbook_settings.locale_and_timezone.locale_example3")}
|
||||||
|
<RowValue>11/23/2025 09:21:06 PM</RowValue>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
{t("workbook_settings.locale_and_timezone.locale_example4")}
|
||||||
|
<RowValue>Monday</RowValue>
|
||||||
|
</Row>
|
||||||
|
</HelperBox>
|
||||||
|
</FormControl>
|
||||||
|
</FieldWrapper>
|
||||||
|
<FieldWrapper>
|
||||||
|
<StyledLabel htmlFor="timezone">
|
||||||
|
{t("workbook_settings.locale_and_timezone.timezone_label")}
|
||||||
|
</StyledLabel>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<StyledAutocomplete
|
||||||
|
id="timezone"
|
||||||
|
value={validSelectedTimezone}
|
||||||
|
onChange={(_event, newValue) => {
|
||||||
|
setSelectedTimezone((newValue as string) || "");
|
||||||
|
}}
|
||||||
|
options={timezones}
|
||||||
|
renderInput={(params) => <TextField {...params} />}
|
||||||
|
renderOption={(props, option) => (
|
||||||
|
<StyledMenuItem
|
||||||
|
{...props}
|
||||||
|
key={option as string}
|
||||||
|
$isSelected={option === validSelectedTimezone}
|
||||||
|
>
|
||||||
|
{option as string}
|
||||||
|
</StyledMenuItem>
|
||||||
|
)}
|
||||||
|
disableClearable
|
||||||
|
slotProps={{
|
||||||
|
paper: {
|
||||||
|
sx: menuPaperStyles,
|
||||||
|
},
|
||||||
|
popper: {
|
||||||
|
sx: {
|
||||||
|
"& .MuiAutocomplete-paper": {
|
||||||
|
transition: "none !important",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
popupIndicator: {
|
||||||
|
disableRipple: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<HelperBox>
|
||||||
|
<Row>
|
||||||
|
{t("workbook_settings.locale_and_timezone.timezone_example1")}
|
||||||
|
<RowValue>23/11/2025</RowValue>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
{t("workbook_settings.locale_and_timezone.timezone_example2")}
|
||||||
|
<RowValue>11/23/2025 09:21:06 PM</RowValue>
|
||||||
|
</Row>
|
||||||
|
</HelperBox>
|
||||||
|
</FormControl>
|
||||||
|
</FieldWrapper>
|
||||||
|
</StyledDialogContent>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<StyledButton onClick={handleSave} tabIndex={0}>
|
||||||
|
<Check
|
||||||
|
style={{ width: "16px", height: "16px", marginRight: "8px" }}
|
||||||
|
/>
|
||||||
|
{t("num_fmt.save")}
|
||||||
|
</StyledButton>
|
||||||
|
</DialogFooter>
|
||||||
|
</StyledDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledDialog = styled(Dialog)`
|
||||||
|
& .MuiPaper-root {
|
||||||
|
max-width: 320px;
|
||||||
|
width: 320px;
|
||||||
|
min-width: 280px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDialogTitle = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: Inter;
|
||||||
|
padding: 0px 12px;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid ${theme.palette.grey["300"]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Cross = styled("div")`
|
||||||
|
&:hover {
|
||||||
|
background-color: ${theme.palette.grey["50"]};
|
||||||
|
}
|
||||||
|
display: flex;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDialogContent = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSectionTitle = styled("h1")`
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: Inter;
|
||||||
|
margin: 0px;
|
||||||
|
color: ${theme.palette.text.primary};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSelect = styled(Select)`
|
||||||
|
font-size: 12px;
|
||||||
|
height: 32px;
|
||||||
|
& .MuiInputBase-root {
|
||||||
|
padding: 0px !important;
|
||||||
|
}
|
||||||
|
& .MuiInputBase-input {
|
||||||
|
font-size: 12px;
|
||||||
|
height: 20px;
|
||||||
|
padding-right: 0px !important;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
& .MuiSelect-select {
|
||||||
|
padding: 8px 32px 8px 8px !important;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
& .MuiSvgIcon-root {
|
||||||
|
right: 4px !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HelperBox = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid ${theme.palette.grey["300"]};
|
||||||
|
font-family: Inter;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
background-color: ${theme.palette.grey["100"]};
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Row = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: ${theme.palette.grey[700]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RowValue = styled("span")`
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: Inter;
|
||||||
|
font-weight: normal;
|
||||||
|
color: ${theme.palette.grey[500]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledAutocomplete = styled(Autocomplete)`
|
||||||
|
& .MuiInputBase-root {
|
||||||
|
padding: 0px !important;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
& .MuiInputBase-input {
|
||||||
|
font-size: 12px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0px;
|
||||||
|
padding-right: 0px !important;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
& .MuiAutocomplete-popupIndicator:hover {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
& .MuiAutocomplete-popupIndicator {
|
||||||
|
& .MuiTouchRipple-root {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& .MuiOutlinedInput-root .MuiAutocomplete-endAdornment {
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
|
& .MuiOutlinedInput-root .MuiAutocomplete-input {
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const menuPaperStyles = {
|
||||||
|
boxSizing: "border-box",
|
||||||
|
marginTop: "4px",
|
||||||
|
padding: "4px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
transition: "none !important",
|
||||||
|
"& .MuiList-padding": {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
"& .MuiList-root": {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
"& .MuiAutocomplete-noOptions": {
|
||||||
|
padding: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontFamily: "Inter",
|
||||||
|
},
|
||||||
|
"& .MuiMenuItem-root": {
|
||||||
|
height: "32px !important",
|
||||||
|
padding: "8px !important",
|
||||||
|
minHeight: "32px !important",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledMenuItem = styled(MenuItem)<{ $isSelected?: boolean }>`
|
||||||
|
padding: 8px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
min-height: 32px !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: ${({ $isSelected }) =>
|
||||||
|
$isSelected ? theme.palette.grey[50] : "transparent"} !important;
|
||||||
|
&:hover {
|
||||||
|
background-color: ${theme.palette.grey[50]} !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FieldWrapper = styled(Box)`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
gap: 6px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLabel = styled("label")`
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: "Inter";
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${theme.palette.text.primary};
|
||||||
|
display: block;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DialogFooter = styled("div")`
|
||||||
|
color: ${theme.palette.grey[700]};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1px solid ${theme.palette.grey["300"]};
|
||||||
|
font-family: Inter;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledButton = styled("div")`
|
||||||
|
cursor: pointer;
|
||||||
|
color: ${theme.palette.common.white};
|
||||||
|
background: ${theme.palette.primary.main};
|
||||||
|
padding: 0px 10px;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-family: "Inter";
|
||||||
|
font-size: 14px;
|
||||||
|
&:hover {
|
||||||
|
background: ${theme.palette.primary.dark};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default WorkbookSettingsDialog;
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
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";
|
||||||
@@ -722,7 +721,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 = theme.palette.common.white;
|
let backgroundColor = "#FFFFFF";
|
||||||
if (style.fill.fg_color) {
|
if (style.fill.fg_color) {
|
||||||
backgroundColor = style.fill.fg_color;
|
backgroundColor = style.fill.fg_color;
|
||||||
}
|
}
|
||||||
@@ -1037,21 +1036,14 @@ 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
|
||||||
? isFullColumnSelected
|
? headerSelectedBackground
|
||||||
? theme.palette.primary.main
|
|
||||||
: headerSelectedBackground
|
|
||||||
: headerBackground;
|
: headerBackground;
|
||||||
div.style.color = selected
|
div.style.color = selected ? headerSelectedColor : headerTextColor;
|
||||||
? 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}`;
|
||||||
@@ -1079,15 +1071,9 @@ 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;
|
||||||
@@ -1099,9 +1085,7 @@ 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
|
||||||
? isFullRowSelected
|
? headerSelectedBackground
|
||||||
? theme.palette.primary.main
|
|
||||||
: headerSelectedBackground
|
|
||||||
: headerBackground;
|
: headerBackground;
|
||||||
context.fillRect(
|
context.fillRect(
|
||||||
0.5,
|
0.5,
|
||||||
@@ -1113,11 +1097,7 @@ 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
|
context.fillStyle = selected ? headerSelectedColor : headerTextColor;
|
||||||
? isFullRowSelected
|
|
||||||
? theme.palette.common.white
|
|
||||||
: headerSelectedColor
|
|
||||||
: headerTextColor;
|
|
||||||
context.font = `bold 12px ${defaultCellFontFamily}`;
|
context.font = `bold 12px ${defaultCellFontFamily}`;
|
||||||
context.fillText(
|
context.fillText(
|
||||||
`${row}`,
|
`${row}`,
|
||||||
@@ -1142,17 +1122,11 @@ 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(
|
||||||
@@ -1172,12 +1146,7 @@ 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 += this.addColumnHeader(deltaX, column, selected);
|
||||||
deltaX,
|
|
||||||
column,
|
|
||||||
selected,
|
|
||||||
isFullColumnSelected,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frozenColumns !== 0) {
|
if (frozenColumns !== 0) {
|
||||||
@@ -1193,12 +1162,7 @@ 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 += this.addColumnHeader(deltaX, column, selected);
|
||||||
deltaX,
|
|
||||||
column,
|
|
||||||
selected,
|
|
||||||
isFullColumnSelected,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
columnHeaders.style.width = `${deltaX}px`;
|
columnHeaders.style.width = `${deltaX}px`;
|
||||||
@@ -1208,7 +1172,6 @@ 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(),
|
||||||
@@ -1219,7 +1182,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, isFullColumnSelected);
|
this.styleColumnHeader(columnWidth, div, selected);
|
||||||
this.addColumnResizeHandle(deltaX + columnWidth, column, columnWidth);
|
this.addColumnResizeHandle(deltaX + columnWidth, column, columnWidth);
|
||||||
return columnWidth;
|
return columnWidth;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,9 +92,6 @@
|
|||||||
"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",
|
||||||
@@ -167,5 +164,21 @@
|
|||||||
"right_drawer": {
|
"right_drawer": {
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"resize_drawer": "Resize drawer"
|
"resize_drawer": "Resize drawer"
|
||||||
|
},
|
||||||
|
"workbook_settings": {
|
||||||
|
"open_settings": "Open settings",
|
||||||
|
"title": "Workbook Settings",
|
||||||
|
"close": "Close dialog",
|
||||||
|
"locale_and_timezone": {
|
||||||
|
"title": "Locale & Timezone",
|
||||||
|
"locale_label": "Locale",
|
||||||
|
"locale_example1": "Number",
|
||||||
|
"locale_example2": "Date",
|
||||||
|
"locale_example3": "Date and Time",
|
||||||
|
"locale_example4": "First day of the week",
|
||||||
|
"timezone_label": "Timezone",
|
||||||
|
"timezone_example1": "TODAY()",
|
||||||
|
"timezone_example2": "NOW()"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
webapp/app.ironcalc.com/frontend/package-lock.json
generated
25
webapp/app.ironcalc.com/frontend/package-lock.json
generated
@@ -91,7 +91,6 @@
|
|||||||
"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",
|
||||||
@@ -571,7 +570,6 @@
|
|||||||
"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",
|
||||||
@@ -615,7 +613,6 @@
|
|||||||
"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",
|
||||||
@@ -2029,7 +2026,6 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -2126,7 +2122,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"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",
|
||||||
@@ -2696,7 +2691,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -2764,7 +2758,6 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -2774,7 +2767,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -2986,7 +2978,6 @@
|
|||||||
"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"
|
||||||
@@ -3032,7 +3023,6 @@
|
|||||||
"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",
|
||||||
@@ -3123,6 +3113,21 @@
|
|||||||
"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,19 +227,16 @@ const Wrapper = styled("div")`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const DRAWER_WIDTH = 264;
|
const DRAWER_WIDTH = 264;
|
||||||
export const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 768;
|
const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 440;
|
||||||
|
|
||||||
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
|
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
|
||||||
margin-left: ${({ isDrawerOpen }) =>
|
margin-left: ${({ isDrawerOpen }) => (isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`)};
|
||||||
isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`};
|
width: ${({ isDrawerOpen }) => (isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : "100%")};
|
||||||
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 }) => isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`}
|
||||||
isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`}
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -250,7 +247,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: 100;
|
z-index: 1;
|
||||||
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,7 +3,6 @@ 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";
|
||||||
@@ -78,7 +77,7 @@ export function FileBar(properties: {
|
|||||||
{properties.isDrawerOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
|
{properties.isDrawerOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
|
||||||
</DrawerButton>
|
</DrawerButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{width > MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE && (
|
{width > 440 && (
|
||||||
<FileMenu
|
<FileMenu
|
||||||
newModel={properties.newModel}
|
newModel={properties.newModel}
|
||||||
newModelFromTemplate={properties.newModelFromTemplate}
|
newModelFromTemplate={properties.newModelFromTemplate}
|
||||||
@@ -93,7 +92,7 @@ export function FileBar(properties: {
|
|||||||
onDelete={properties.onDelete}
|
onDelete={properties.onDelete}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{width > MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE && <HelpMenu />}
|
{width > 440 && <HelpMenu />}
|
||||||
<WorkbookTitleWrapper>
|
<WorkbookTitleWrapper>
|
||||||
<WorkbookTitle
|
<WorkbookTitle
|
||||||
name={properties.model.getName()}
|
name={properties.model.getName()}
|
||||||
@@ -113,8 +112,7 @@ export function FileBar(properties: {
|
|||||||
<div style={{ fontWeight: "bold" }}>{cloudWarningText2}</div>
|
<div style={{ fontWeight: "bold" }}>{cloudWarningText2}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
placement="bottom"
|
placement="bottom-start"
|
||||||
enterTouchDelay={0}
|
|
||||||
enterDelay={500}
|
enterDelay={500}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
popper: {
|
popper: {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/calc_tests/COMBIN_COMBINA_detailed.xlsx
Normal file
BIN
xlsx/tests/calc_tests/COMBIN_COMBINA_detailed.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/DMIN_DMAX_DAVERAGE_DSUM_DCOUNT_DGET 1.xlsx
Normal file
BIN
xlsx/tests/calc_tests/DMIN_DMAX_DAVERAGE_DSUM_DCOUNT_DGET 1.xlsx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/calc_tests/MROUND_edgecases.xlsx
Normal file
BIN
xlsx/tests/calc_tests/MROUND_edgecases.xlsx
Normal file
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/calc_tests/N_CELL_INFO_SHEETS.xlsx
Normal file
BIN
xlsx/tests/calc_tests/N_CELL_INFO_SHEETS.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