Compare commits
125 Commits
llm_test/a
...
dani/widge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d1d338c51 | ||
|
|
a78920216a | ||
|
|
b96c10ab8d | ||
|
|
ab8aaea2bf | ||
|
|
f96612cf23 | ||
|
|
745435b950 | ||
|
|
4ca996cd3f | ||
|
|
3fbb91c414 | ||
|
|
93c9c42607 | ||
|
|
11edc2378e | ||
|
|
962e70c834 | ||
|
|
f803dad0a3 | ||
|
|
19580fc1ad | ||
|
|
e760b2d08e | ||
|
|
0e6ded7154 | ||
|
|
db26403432 | ||
|
|
9193479cce | ||
|
|
f814a75ae5 | ||
|
|
c8da5efb5f | ||
|
|
522e734395 | ||
|
|
2a7d59e512 | ||
|
|
c4142d4bf8 | ||
|
|
885d344b5b | ||
|
|
bed6f007cd | ||
|
|
dbd1b2df60 | ||
|
|
8597d14a4e | ||
|
|
01b19b9c35 | ||
|
|
4649a0c78c | ||
|
|
cd0baf5ba7 | ||
|
|
167d169f1a | ||
|
|
080574b112 | ||
|
|
6056b8f122 | ||
|
|
e61b15655a | ||
|
|
6822505602 | ||
|
|
25f7891343 | ||
|
|
bdd0af0a39 | ||
|
|
261924396d | ||
|
|
67ef3bcf87 | ||
|
|
9aac285964 | ||
|
|
ba40c3c673 | ||
|
|
cc01556387 | ||
|
|
35323df20e | ||
|
|
19c115b32f | ||
|
|
6b60b339d6 | ||
|
|
41c8d88b80 | ||
|
|
73e5c305cc | ||
|
|
774b447c84 | ||
|
|
23b7333572 | ||
|
|
ef47c26c50 | ||
|
|
5cc61b0de4 | ||
|
|
42e8d44454 | ||
|
|
f840806f94 | ||
|
|
4a21d4b03a | ||
|
|
4cf162eb82 | ||
|
|
2cab93be18 | ||
|
|
fd34e46689 | ||
|
|
3bb49d1e8f | ||
|
|
1391f196b5 | ||
|
|
3db094c956 | ||
|
|
50941cb6ef | ||
|
|
150b516863 | ||
|
|
dc49afa2c3 | ||
|
|
acb90fbb9d | ||
|
|
7676efca44 | ||
|
|
8e15c623dd | ||
|
|
eb76d8dd23 | ||
|
|
1053d00d22 | ||
|
|
5ff4774c5a | ||
|
|
7e966baa0d | ||
|
|
c52c05aa8e | ||
|
|
129959137d | ||
|
|
4d5af45711 | ||
|
|
471f32f92a | ||
|
|
7b5427196d | ||
|
|
66b7586730 | ||
|
|
630f0e1baf | ||
|
|
bc9fefcb70 | ||
|
|
3d970acc34 | ||
|
|
e0e566db76 | ||
|
|
e3fc1d229a | ||
|
|
78d1f6b4a4 | ||
|
|
45ee1c35fe | ||
|
|
671cfff619 | ||
|
|
7e2fcec4a3 | ||
|
|
12342da649 | ||
|
|
4e9d7611a8 | ||
|
|
e0339f641b | ||
|
|
aa953e1ece | ||
|
|
cbf75c059b | ||
|
|
b2744efeb5 | ||
|
|
ef6849e822 | ||
|
|
aa4dd598b1 | ||
|
|
8b3bd7943e | ||
|
|
a1d1b64b76 | ||
|
|
5094a7fe4d | ||
|
|
c283fd7b60 | ||
|
|
36beccd4ae | ||
|
|
a252f9c626 | ||
|
|
f8bd03d92c | ||
|
|
e44a2e8c3e | ||
|
|
4217c1455b | ||
|
|
d8b3ba0dae | ||
|
|
95a7782f22 | ||
|
|
087211ebc3 | ||
|
|
46d766c85c | ||
|
|
2a14ee73c4 | ||
|
|
401c7c4289 | ||
|
|
3246137545 | ||
|
|
b1f45511d0 | ||
|
|
4b93174261 | ||
|
|
3111a74530 | ||
|
|
ae3fcaf9e9 | ||
|
|
dd78db3d2b | ||
|
|
acf334074f | ||
|
|
e48810d91b | ||
|
|
18db1cf052 | ||
|
|
ed40f79324 | ||
|
|
10ee95c48f | ||
|
|
741a223f3d | ||
|
|
ba139d1b6c | ||
|
|
e0306cb161 | ||
|
|
cea1f67cd0 | ||
|
|
4a3eef5a81 | ||
|
|
91299e3c0b | ||
|
|
1b38d79b81 |
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -43,6 +43,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
@@ -443,6 +452,7 @@ dependencies = [
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"statrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -965,6 +975,16 @@ version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||
|
||||
[[package]]
|
||||
name = "statrs"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
|
||||
@@ -19,6 +19,7 @@ regex = { version = "1.0", optional = true}
|
||||
regex-lite = { version = "0.1.6", optional = true}
|
||||
bitcode = "0.6.3"
|
||||
csv = "1.3.0"
|
||||
statrs = { version = "0.18.0", default-features = false, features = [] }
|
||||
|
||||
[features]
|
||||
default = ["use_regex_full"]
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::{
|
||||
token::Error,
|
||||
types::CellReferenceIndex,
|
||||
},
|
||||
formatter::format::parse_formatted_number,
|
||||
model::Model,
|
||||
};
|
||||
|
||||
@@ -14,6 +15,23 @@ pub(crate) enum NumberOrArray {
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn cast_number(&self, s: &str) -> Option<f64> {
|
||||
match s.trim().parse::<f64>() {
|
||||
Ok(f) => Some(f),
|
||||
_ => {
|
||||
let currency = &self.locale.currency.symbol;
|
||||
let mut currencies = vec!["$", "€"];
|
||||
if !currencies.iter().any(|e| *e == currency) {
|
||||
currencies.push(currency);
|
||||
}
|
||||
// Try to parse as a formatted number (e.g., dates, currencies, percentages)
|
||||
if let Ok((v, _number_format)) = parse_formatted_number(s, ¤cies) {
|
||||
return Some(v);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) fn get_number_or_array(
|
||||
&mut self,
|
||||
node: &Node,
|
||||
@@ -21,9 +39,9 @@ impl Model {
|
||||
) -> Result<NumberOrArray, CalcResult> {
|
||||
match self.evaluate_node_in_context(node, cell) {
|
||||
CalcResult::Number(f) => Ok(NumberOrArray::Number(f)),
|
||||
CalcResult::String(s) => match s.parse::<f64>() {
|
||||
Ok(f) => Ok(NumberOrArray::Number(f)),
|
||||
_ => Err(CalcResult::new_error(
|
||||
CalcResult::String(s) => match self.cast_number(&s) {
|
||||
Some(f) => Ok(NumberOrArray::Number(f)),
|
||||
None => Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Expecting number".to_string(),
|
||||
@@ -89,16 +107,16 @@ impl Model {
|
||||
self.cast_to_number(result, cell)
|
||||
}
|
||||
|
||||
fn cast_to_number(
|
||||
pub(crate) fn cast_to_number(
|
||||
&mut self,
|
||||
result: CalcResult,
|
||||
cell: CellReferenceIndex,
|
||||
) -> Result<f64, CalcResult> {
|
||||
match result {
|
||||
CalcResult::Number(f) => Ok(f),
|
||||
CalcResult::String(s) => match s.parse::<f64>() {
|
||||
Ok(f) => Ok(f),
|
||||
_ => Err(CalcResult::new_error(
|
||||
CalcResult::String(s) => match self.cast_number(&s) {
|
||||
Some(f) => Ok(f),
|
||||
None => Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Expecting number".to_string(),
|
||||
|
||||
@@ -12,6 +12,9 @@ pub(crate) const DEFAULT_WINDOW_WIDTH: i64 = 800;
|
||||
pub(crate) const LAST_COLUMN: i32 = 16_384;
|
||||
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:
|
||||
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
||||
// The 2 days offset is because of Excel 1900 bug
|
||||
|
||||
@@ -471,6 +471,20 @@ impl Parser {
|
||||
Node::NumberKind(s) => ArrayNode::Number(s),
|
||||
Node::StringKind(s) => ArrayNode::String(s),
|
||||
Node::ErrorKind(kind) => ArrayNode::Error(kind),
|
||||
Node::UnaryKind {
|
||||
kind: OpUnary::Minus,
|
||||
right,
|
||||
} => {
|
||||
if let Node::NumberKind(n) = *right {
|
||||
ArrayNode::Number(-n)
|
||||
} else {
|
||||
return Err(Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
message: "Invalid value in array".to_string(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
});
|
||||
}
|
||||
}
|
||||
error @ Node::ParseErrorKind { .. } => return Err(error),
|
||||
_ => {
|
||||
return Err(Node::ParseErrorKind {
|
||||
@@ -490,6 +504,20 @@ impl Parser {
|
||||
Node::NumberKind(s) => ArrayNode::Number(s),
|
||||
Node::StringKind(s) => ArrayNode::String(s),
|
||||
Node::ErrorKind(kind) => ArrayNode::Error(kind),
|
||||
Node::UnaryKind {
|
||||
kind: OpUnary::Minus,
|
||||
right,
|
||||
} => {
|
||||
if let Node::NumberKind(n) = *right {
|
||||
ArrayNode::Number(-n)
|
||||
} else {
|
||||
return Err(Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
message: "Invalid value in array".to_string(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
});
|
||||
}
|
||||
}
|
||||
error @ Node::ParseErrorKind { .. } => return Err(error),
|
||||
_ => {
|
||||
return Err(Node::ParseErrorKind {
|
||||
|
||||
@@ -711,6 +711,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
||||
Function::Value => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Valuetotext => args_signature_scalars(arg_count, 1, 1),
|
||||
Function::Average => vec![Signature::Vector; arg_count],
|
||||
Function::Avedev => vec![Signature::Vector; arg_count],
|
||||
Function::Averagea => vec![Signature::Vector; arg_count],
|
||||
Function::Averageif => args_signature_sumif(arg_count),
|
||||
Function::Averageifs => vec![Signature::Vector; arg_count],
|
||||
@@ -871,11 +872,141 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
||||
Function::Combin => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Combina => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Sumsq => vec![Signature::Vector; arg_count],
|
||||
|
||||
Function::N => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Sheets => args_signature_scalars(arg_count, 0, 1),
|
||||
Function::Cell => 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::Dcount => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||
Function::Dget => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||
Function::Dmax => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||
Function::Dmin => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||
Function::Dsum => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||
Function::Dcounta => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||
Function::Dproduct => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||
Function::Dstdev => 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::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],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -901,7 +1032,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
||||
Function::Atan => scalar_arguments(args),
|
||||
Function::Atan2 => scalar_arguments(args),
|
||||
Function::Atanh => scalar_arguments(args),
|
||||
Function::Choose => scalar_arguments(args), // static_analysis_choose(args, cell),
|
||||
Function::Choose => scalar_arguments(args),
|
||||
Function::Column => not_implemented(args),
|
||||
Function::Columns => not_implemented(args),
|
||||
Function::Cos => scalar_arguments(args),
|
||||
@@ -948,7 +1079,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
||||
Function::Lookup => not_implemented(args),
|
||||
Function::Match => not_implemented(args),
|
||||
Function::Offset => static_analysis_offset(args),
|
||||
// FIXME: Row could return an array
|
||||
Function::Row => StaticResult::Scalar,
|
||||
Function::Rows => not_implemented(args),
|
||||
Function::Vlookup => not_implemented(args),
|
||||
@@ -977,6 +1107,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
||||
Function::Valuetotext => not_implemented(args),
|
||||
Function::Average => not_implemented(args),
|
||||
Function::Averagea => not_implemented(args),
|
||||
Function::Avedev => not_implemented(args),
|
||||
Function::Averageif => not_implemented(args),
|
||||
Function::Averageifs => not_implemented(args),
|
||||
Function::Count => not_implemented(args),
|
||||
@@ -1139,5 +1270,93 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
||||
Function::Sheets => scalar_arguments(args),
|
||||
Function::Cell => scalar_arguments(args),
|
||||
Function::Info => scalar_arguments(args),
|
||||
Function::Dget => not_implemented(args),
|
||||
Function::Dmax => not_implemented(args),
|
||||
Function::Dmin => not_implemented(args),
|
||||
Function::Dcount => not_implemented(args),
|
||||
Function::Daverage => not_implemented(args),
|
||||
Function::Dsum => not_implemented(args),
|
||||
Function::Dcounta => not_implemented(args),
|
||||
Function::Dproduct => not_implemented(args),
|
||||
Function::Dstdev => not_implemented(args),
|
||||
Function::Dvar => not_implemented(args),
|
||||
Function::Dvarp => 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,15 +211,19 @@ pub fn parse_reference_a1(r: &str) -> Option<ParsedReference> {
|
||||
pub fn is_valid_identifier(name: &str) -> bool {
|
||||
// https://support.microsoft.com/en-us/office/names-in-formulas-fc2935f9-115d-4bef-a370-3aa8bb4c91f1
|
||||
// https://github.com/MartinTrummer/excel-names/
|
||||
// NOTE: We are being much more restrictive than Excel.
|
||||
// In particular we do not support non ascii characters.
|
||||
let upper = name.to_ascii_uppercase();
|
||||
let bytes = upper.as_bytes();
|
||||
let len = bytes.len();
|
||||
// length of chars
|
||||
let len = upper.chars().count();
|
||||
|
||||
let mut chars = upper.chars();
|
||||
|
||||
if len > 255 || len == 0 {
|
||||
return false;
|
||||
}
|
||||
let first = bytes[0] as char;
|
||||
let first = match chars.next() {
|
||||
Some(ch) => ch,
|
||||
None => return false,
|
||||
};
|
||||
// The first character of a name must be a letter, an underscore character (_), or a backslash (\).
|
||||
if !(first.is_ascii_alphabetic() || first == '_' || first == '\\') {
|
||||
return false;
|
||||
@@ -237,20 +241,10 @@ pub fn is_valid_identifier(name: &str) -> bool {
|
||||
if parse_reference_r1c1(name).is_some() {
|
||||
return false;
|
||||
}
|
||||
let mut i = 1;
|
||||
while i < len {
|
||||
let ch = bytes[i] as char;
|
||||
match ch {
|
||||
'a'..='z' => {}
|
||||
'A'..='Z' => {}
|
||||
'0'..='9' => {}
|
||||
'_' => {}
|
||||
'.' => {}
|
||||
_ => {
|
||||
return false;
|
||||
}
|
||||
for ch in chars {
|
||||
if !(ch.is_alphanumeric() || ch == '_' || ch == '.') {
|
||||
return false;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
true
|
||||
|
||||
@@ -196,6 +196,7 @@ fn test_names() {
|
||||
assert!(is_valid_identifier("_."));
|
||||
assert!(is_valid_identifier("_1"));
|
||||
assert!(is_valid_identifier("\\."));
|
||||
assert!(is_valid_identifier("truñe"));
|
||||
|
||||
// invalid
|
||||
assert!(!is_valid_identifier("true"));
|
||||
@@ -209,7 +210,6 @@ fn test_names() {
|
||||
assert!(!is_valid_identifier("1true"));
|
||||
|
||||
assert!(!is_valid_identifier("test€"));
|
||||
assert!(!is_valid_identifier("truñe"));
|
||||
assert!(!is_valid_identifier("tr&ue"));
|
||||
|
||||
assert!(!is_valid_identifier("LOG10"));
|
||||
|
||||
@@ -15,7 +15,7 @@ pub struct Formatted {
|
||||
|
||||
/// Returns the vector of chars of the fractional part of a *positive* number:
|
||||
/// 3.1415926 ==> ['1', '4', '1', '5', '9', '2', '6']
|
||||
fn get_fract_part(value: f64, precision: i32) -> Vec<char> {
|
||||
fn get_fract_part(value: f64, precision: i32, int_len: usize) -> Vec<char> {
|
||||
let b = format!("{:.1$}", value.fract(), precision as usize)
|
||||
.chars()
|
||||
.collect::<Vec<char>>();
|
||||
@@ -30,6 +30,12 @@ fn get_fract_part(value: f64, precision: i32) -> Vec<char> {
|
||||
if last_non_zero < 2 {
|
||||
return vec![];
|
||||
}
|
||||
let max_len = if int_len > 15 {
|
||||
2_usize
|
||||
} else {
|
||||
15_usize - int_len + 1
|
||||
};
|
||||
let last_non_zero = usize::min(last_non_zero, max_len + 1);
|
||||
b[2..last_non_zero].to_vec()
|
||||
}
|
||||
|
||||
@@ -423,7 +429,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
||||
if value_abs as i64 == 0 {
|
||||
int_part = vec![];
|
||||
}
|
||||
let fract_part = get_fract_part(value_abs, p.precision);
|
||||
let fract_part = get_fract_part(value_abs, p.precision, int_part.len());
|
||||
// ln is the number of digits of the integer part of the value
|
||||
let ln = int_part.len() as i32;
|
||||
// digit count is the number of digit tokens ('0', '?' and '#') to the left of the decimal point
|
||||
@@ -744,10 +750,10 @@ fn parse_date(value: &str) -> Result<(i32, String), String> {
|
||||
/// "30.34%" => (0.3034, "0.00%")
|
||||
/// 100€ => (100, "100€")
|
||||
pub(crate) fn parse_formatted_number(
|
||||
value: &str,
|
||||
original: &str,
|
||||
currencies: &[&str],
|
||||
) -> Result<(f64, Option<String>), String> {
|
||||
let value = value.trim();
|
||||
let value = original.trim();
|
||||
let scientific_format = "0.00E+00";
|
||||
|
||||
// Check if it is a percentage
|
||||
@@ -799,7 +805,8 @@ pub(crate) fn parse_formatted_number(
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok((serial_number, format)) = parse_date(value) {
|
||||
// check if it is a date. NOTE: we don't trim the original here
|
||||
if let Ok((serial_number, format)) = parse_date(original) {
|
||||
return Ok((serial_number as f64, Some(format)));
|
||||
}
|
||||
|
||||
|
||||
946
base/src/functions/database.rs
Normal file
946
base/src/functions/database.rs
Normal file
@@ -0,0 +1,946 @@
|
||||
use chrono::Datelike;
|
||||
|
||||
use crate::{
|
||||
calc_result::CalcResult,
|
||||
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
|
||||
formatter::dates::date_to_serial_number,
|
||||
Model,
|
||||
};
|
||||
|
||||
use super::util::{compare_values, from_wildcard_to_regex, result_matches_regex};
|
||||
|
||||
impl Model {
|
||||
// =DAVERAGE(database, field, criteria)
|
||||
pub(crate) fn fn_daverage(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let criteria = match self.get_reference(&args[2], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if db_right.row <= db_left.row {
|
||||
// no data rows
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "No data rows in database".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut sum = 0.0f64;
|
||||
let mut count = 0usize;
|
||||
|
||||
let mut row = db_left.row + 1; // skip header
|
||||
while row <= db_right.row {
|
||||
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||
let v = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: db_left.sheet,
|
||||
row,
|
||||
column: field_col,
|
||||
});
|
||||
if let CalcResult::Number(n) = v {
|
||||
if n.is_finite() {
|
||||
sum += n;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
row += 1;
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "No numeric values matched criteria".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(sum / count as f64)
|
||||
}
|
||||
|
||||
// =DCOUNT(database, field, criteria)
|
||||
// Counts numeric entries in the field for rows that match criteria
|
||||
pub(crate) fn fn_dcount(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let criteria = match self.get_reference(&args[2], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if db_right.row <= db_left.row {
|
||||
// no data rows
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "No data rows in database".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut count = 0usize;
|
||||
let mut row = db_left.row + 1; // skip header
|
||||
while row <= db_right.row {
|
||||
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||
let v = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: db_left.sheet,
|
||||
row,
|
||||
column: field_col,
|
||||
});
|
||||
if matches!(v, CalcResult::Number(_)) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
row += 1;
|
||||
}
|
||||
|
||||
CalcResult::Number(count as f64)
|
||||
}
|
||||
|
||||
// =DGET(database, field, criteria)
|
||||
// Returns the (single) field value for the unique matching row
|
||||
pub(crate) fn fn_dget(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let criteria = match self.get_reference(&args[2], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if db_right.row <= db_left.row {
|
||||
// no data rows
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "No data rows in database".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut result: Option<CalcResult> = None;
|
||||
let mut matches = 0usize;
|
||||
|
||||
let mut row = db_left.row + 1;
|
||||
while row <= db_right.row {
|
||||
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||
matches += 1;
|
||||
if matches > 1 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "More than one matching record".to_string(),
|
||||
};
|
||||
}
|
||||
result = Some(self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: db_left.sheet,
|
||||
row,
|
||||
column: field_col,
|
||||
}));
|
||||
}
|
||||
row += 1;
|
||||
}
|
||||
|
||||
match (matches, result) {
|
||||
(0, _) | (_, None) => CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "No matching record".to_string(),
|
||||
},
|
||||
(_, Some(v)) => v,
|
||||
}
|
||||
}
|
||||
|
||||
// =DMAX(database, field, criteria)
|
||||
pub(crate) fn fn_dmax(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
self.db_extreme(args, cell, true)
|
||||
}
|
||||
|
||||
// =DMIN(database, field, criteria)
|
||||
pub(crate) fn fn_dmin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
self.db_extreme(args, cell, false)
|
||||
}
|
||||
|
||||
// =DSUM(database, field, criteria)
|
||||
pub(crate) fn fn_dsum(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let criteria = match self.get_reference(&args[2], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if db_right.row <= db_left.row {
|
||||
// no data rows
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "No data rows in database".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut sum = 0.0;
|
||||
|
||||
// skip header
|
||||
let mut row = db_left.row + 1;
|
||||
while row <= db_right.row {
|
||||
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||
let v = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: db_left.sheet,
|
||||
row,
|
||||
column: field_col,
|
||||
});
|
||||
if let CalcResult::Number(n) = v {
|
||||
if n.is_finite() {
|
||||
sum += n;
|
||||
}
|
||||
}
|
||||
}
|
||||
row += 1;
|
||||
}
|
||||
|
||||
CalcResult::Number(sum)
|
||||
}
|
||||
|
||||
// =DCOUNTA(database, field, criteria)
|
||||
// Counts non-empty entries (any type) in the field for rows that match criteria
|
||||
pub(crate) fn fn_dcounta(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let criteria = match self.get_reference(&args[2], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if db_right.row <= db_left.row {
|
||||
// no data rows
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "No data rows in database".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut count = 0;
|
||||
for row in (db_left.row + 1)..=db_right.row {
|
||||
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||
let v = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: db_left.sheet,
|
||||
row,
|
||||
column: field_col,
|
||||
});
|
||||
if !matches!(v, CalcResult::EmptyCell | CalcResult::EmptyArg) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CalcResult::Number(count as f64)
|
||||
}
|
||||
|
||||
// =DPRODUCT(database, field, criteria)
|
||||
pub(crate) fn fn_dproduct(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let criteria = match self.get_reference(&args[2], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if db_right.row <= db_left.row {
|
||||
// no data rows
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "No data rows in database".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut product = 1.0f64;
|
||||
let mut has_numeric = false;
|
||||
|
||||
let mut row = db_left.row + 1; // skip header
|
||||
while row <= db_right.row {
|
||||
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||
let v = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: db_left.sheet,
|
||||
row,
|
||||
column: field_col,
|
||||
});
|
||||
if let CalcResult::Number(n) = v {
|
||||
if n.is_finite() {
|
||||
product *= n;
|
||||
has_numeric = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
row += 1;
|
||||
}
|
||||
|
||||
// Excel returns 0 when no rows / no numeric values match for DPRODUCT
|
||||
if has_numeric {
|
||||
CalcResult::Number(product)
|
||||
} else {
|
||||
CalcResult::Number(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
// Small internal helper for DSTDEV / DVAR
|
||||
// Collects sum, sum of squares, and count of numeric values in the field
|
||||
// for rows that match the criteria.
|
||||
fn db_numeric_stats(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> Result<(f64, f64, usize), CalcResult> {
|
||||
if args.len() != 3 {
|
||||
return Err(CalcResult::new_args_number_error(cell));
|
||||
}
|
||||
|
||||
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let field_col = self.resolve_db_field_column(db_left, db_right, &args[1], cell)?;
|
||||
|
||||
let criteria = match self.get_reference(&args[2], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
if db_right.row <= db_left.row {
|
||||
// no data rows
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "No data rows in database".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut sum = 0.0f64;
|
||||
let mut sumsq = 0.0f64;
|
||||
let mut count = 0usize;
|
||||
|
||||
let mut row = db_left.row + 1; // skip header
|
||||
while row <= db_right.row {
|
||||
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||
let v = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: db_left.sheet,
|
||||
row,
|
||||
column: field_col,
|
||||
});
|
||||
if let CalcResult::Number(n) = v {
|
||||
if n.is_finite() {
|
||||
sum += n;
|
||||
sumsq += n * n;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
row += 1;
|
||||
}
|
||||
|
||||
Ok((sum, sumsq, count))
|
||||
}
|
||||
|
||||
// =DSTDEV(database, field, criteria)
|
||||
// Sample standard deviation of matching numeric values
|
||||
pub(crate) fn fn_dstdev(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
|
||||
Ok(stats) => stats,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// Excel behaviour: #DIV/0! if 0 or 1 numeric values match
|
||||
if count < 2 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Not enough numeric values matched criteria".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let n = count as f64;
|
||||
let var = (sumsq - (sum * sum) / n) / (n - 1.0);
|
||||
let var = if var < 0.0 { 0.0 } else { var };
|
||||
CalcResult::Number(var.sqrt())
|
||||
}
|
||||
|
||||
// =DVAR(database, field, criteria)
|
||||
// Sample variance of matching numeric values
|
||||
pub(crate) fn fn_dvar(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
|
||||
Ok(stats) => stats,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// Excel behaviour: #DIV/0! if 0 or 1 numeric values match
|
||||
if count < 2 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Not enough numeric values matched criteria".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let n = count as f64;
|
||||
let var = (sumsq - (sum * sum) / n) / (n - 1.0);
|
||||
let var = if var < 0.0 { 0.0 } else { var };
|
||||
CalcResult::Number(var)
|
||||
}
|
||||
|
||||
// =DSTDEVP(database, field, criteria)
|
||||
// Population standard deviation of matching numeric values
|
||||
pub(crate) fn fn_dstdevp(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
|
||||
Ok(stats) => stats,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// Excel behaviour: #DIV/0! if no numeric values match
|
||||
if count == 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "No numeric values matched criteria".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let n = count as f64;
|
||||
let var = (sumsq - (sum * sum) / n) / n;
|
||||
let var = if var < 0.0 { 0.0 } else { var };
|
||||
CalcResult::Number(var.sqrt())
|
||||
}
|
||||
|
||||
// =DVARP(database, field, criteria)
|
||||
// Population variance of matching numeric values
|
||||
pub(crate) fn fn_dvarp(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
|
||||
Ok(stats) => stats,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// Excel behaviour: #DIV/0! if no numeric values match
|
||||
if count == 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "No numeric values matched criteria".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let n = count as f64;
|
||||
let var = (sumsq - (sum * sum) / n) / n;
|
||||
let var = if var < 0.0 { 0.0 } else { var };
|
||||
CalcResult::Number(var)
|
||||
}
|
||||
|
||||
/// Resolve the "field" (2nd arg) to an absolute column index (i32) within the sheet.
|
||||
/// Field can be a number (1-based index) or a header name (case-insensitive).
|
||||
/// Returns the absolute column index, not a 1-based offset within the database range.
|
||||
fn resolve_db_field_column(
|
||||
&mut self,
|
||||
db_left: CellReferenceIndex,
|
||||
db_right: CellReferenceIndex,
|
||||
field_arg: &Node,
|
||||
cell: CellReferenceIndex,
|
||||
) -> Result<i32, CalcResult> {
|
||||
let field_column_name = match self.evaluate_node_in_context(field_arg, cell) {
|
||||
CalcResult::String(s) => s.to_lowercase(),
|
||||
CalcResult::Number(index) => {
|
||||
let index = index.floor() as i32;
|
||||
if index < 1 || db_left.column + index - 1 > db_right.column {
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Field index out of range".to_string(),
|
||||
});
|
||||
}
|
||||
return Ok(db_left.column + index - 1);
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
return if b {
|
||||
Ok(db_left.column)
|
||||
} else {
|
||||
// Index 0 is out of range
|
||||
Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Invalid field specifier".to_string(),
|
||||
})
|
||||
};
|
||||
}
|
||||
error @ CalcResult::Error { .. } => {
|
||||
return Err(error);
|
||||
}
|
||||
CalcResult::Range { .. } => {
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
})
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
||||
CalcResult::Array(_) => {
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
// We search in the database a column whose header matches field_column_name
|
||||
for column in db_left.column..=db_right.column {
|
||||
let v = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: db_left.sheet,
|
||||
row: db_left.row,
|
||||
column,
|
||||
});
|
||||
match &v {
|
||||
CalcResult::String(s) => {
|
||||
if s.to_lowercase() == field_column_name {
|
||||
return Ok(column);
|
||||
}
|
||||
}
|
||||
CalcResult::Number(n) => {
|
||||
if field_column_name == n.to_string() {
|
||||
return Ok(column);
|
||||
}
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
if field_column_name == b.to_string() {
|
||||
return Ok(column);
|
||||
}
|
||||
}
|
||||
CalcResult::Error { .. }
|
||||
| CalcResult::Range { .. }
|
||||
| CalcResult::EmptyCell
|
||||
| CalcResult::EmptyArg
|
||||
| CalcResult::Array(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Field header not found".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Check whether a database row matches the criteria range.
|
||||
/// Criteria logic: OR across criteria rows; AND across columns within a row.
|
||||
fn db_row_matches_criteria(
|
||||
&mut self,
|
||||
db_left: CellReferenceIndex,
|
||||
db_right: CellReferenceIndex,
|
||||
row: i32,
|
||||
criteria: (CellReferenceIndex, CellReferenceIndex),
|
||||
) -> bool {
|
||||
let (c_left, c_right) = criteria;
|
||||
|
||||
// Read criteria headers (first row of criteria range)
|
||||
// Map header name (lowercased) -> db column (if exists)
|
||||
let mut crit_cols: Vec<i32> = Vec::new();
|
||||
let mut header_count = 0;
|
||||
// We cover the criteria table:
|
||||
// headerA | headerB | ...
|
||||
// critA1 | critA2 | ...
|
||||
// critB1 | critB2 | ...
|
||||
// ...
|
||||
for column in c_left.column..=c_right.column {
|
||||
let cell = CellReferenceIndex {
|
||||
sheet: c_left.sheet,
|
||||
row: c_left.row,
|
||||
column,
|
||||
};
|
||||
let criteria_header = self.evaluate_cell(cell);
|
||||
if let Ok(s) = self.cast_to_string(criteria_header, cell) {
|
||||
// Non-empty string header. If the header is non string we skip it
|
||||
header_count += 1;
|
||||
let wanted = s.to_lowercase();
|
||||
|
||||
// Find corresponding Database column
|
||||
let mut found = false;
|
||||
for db_column in db_left.column..=db_right.column {
|
||||
let db_header = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: db_left.sheet,
|
||||
row: db_left.row,
|
||||
column: db_column,
|
||||
});
|
||||
if let Ok(hs) = self.cast_to_string(db_header, cell) {
|
||||
if hs.to_lowercase() == wanted {
|
||||
crit_cols.push(db_column);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
// that means the criteria column has no matching DB column
|
||||
// If the criteria condition is empty then we remove this condition
|
||||
// otherwise this condition can never be satisfied
|
||||
// We evaluate all criteria rows to see if any is non-empty
|
||||
let mut has_non_empty = false;
|
||||
for r in (c_left.row + 1)..=c_right.row {
|
||||
let ccell = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: c_left.sheet,
|
||||
row: r,
|
||||
column,
|
||||
});
|
||||
if !matches!(ccell, CalcResult::EmptyCell | CalcResult::EmptyArg) {
|
||||
has_non_empty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if has_non_empty {
|
||||
// This criteria column can never be satisfied
|
||||
header_count -= 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if c_right.row <= c_left.row {
|
||||
// If no criteria rows (only headers), everything matches
|
||||
return true;
|
||||
}
|
||||
|
||||
if header_count == 0 {
|
||||
// If there are not "String" headers, nothing matches
|
||||
// NB: There might be String headers that do not match any DB columns,
|
||||
// in that case everything matches.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Evaluate each criteria row (OR)
|
||||
for r in (c_left.row + 1)..=c_right.row {
|
||||
// AND across columns for this criteria row
|
||||
let mut and_ok = true;
|
||||
|
||||
for (offset, db_col) in crit_cols.iter().enumerate() {
|
||||
// Criteria cell
|
||||
let ccell = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: c_left.sheet,
|
||||
row: r,
|
||||
column: c_left.column + offset as i32,
|
||||
});
|
||||
|
||||
// Empty criteria cell -> ignored
|
||||
if matches!(ccell, CalcResult::EmptyCell | CalcResult::EmptyArg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Database value for this row/column
|
||||
let db_val = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: db_left.sheet,
|
||||
row,
|
||||
column: *db_col,
|
||||
});
|
||||
|
||||
if !self.criteria_cell_matches(&db_val, &ccell) {
|
||||
and_ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if and_ok {
|
||||
// This criteria row satisfied (OR)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// none matched
|
||||
false
|
||||
}
|
||||
|
||||
/// Implements Excel-like criteria matching for a single value.
|
||||
/// Supports prefixes: <>, >=, <=, >, <, = ; wildcards * and ? for string equals.
|
||||
fn criteria_cell_matches(&self, db_val: &CalcResult, crit_cell: &CalcResult) -> bool {
|
||||
// Convert the criteria cell to a string for operator parsing if possible,
|
||||
// otherwise fall back to equality via compare_values.
|
||||
|
||||
let mut criteria = match crit_cell {
|
||||
CalcResult::String(s) => s.trim().to_string(),
|
||||
CalcResult::Number(n) => {
|
||||
// treat as equality with number
|
||||
return match db_val {
|
||||
CalcResult::Number(v) => (*v - *n).abs() <= f64::EPSILON,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
// check equality with boolean
|
||||
return match db_val {
|
||||
CalcResult::Boolean(v) => *v == *b,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
||||
CalcResult::Error { .. } => return false,
|
||||
CalcResult::Range { .. } | CalcResult::Array(_) => return false,
|
||||
};
|
||||
|
||||
// Detect operator prefix
|
||||
let mut op = "="; // default equality (with wildcard semantics for strings)
|
||||
let prefixes = ["<>", ">=", "<=", ">", "<", "="];
|
||||
for p in prefixes.iter() {
|
||||
if criteria.starts_with(p) {
|
||||
op = p;
|
||||
criteria = criteria[p.len()..].trim().to_string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Is it a number?
|
||||
let rhs_num = criteria.parse::<f64>().ok();
|
||||
|
||||
// Is it a date?
|
||||
// FIXME: We should parse dates according to locale settings
|
||||
let rhs_date = criteria.parse::<chrono::NaiveDate>().ok();
|
||||
|
||||
match op {
|
||||
">" | ">=" | "<" | "<=" => {
|
||||
if let Some(d) = rhs_date {
|
||||
// date comparison
|
||||
let serial = match date_to_serial_number(d.day(), d.month(), d.year()) {
|
||||
Ok(sn) => sn as f64,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
if let CalcResult::Number(n) = db_val {
|
||||
match op {
|
||||
">" => *n > serial,
|
||||
">=" => *n >= serial,
|
||||
"<" => *n < serial,
|
||||
"<=" => *n <= serial,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else if let Some(t) = rhs_num {
|
||||
// numeric comparison
|
||||
if let CalcResult::Number(n) = db_val {
|
||||
match op {
|
||||
">" => *n > t,
|
||||
">=" => *n >= t,
|
||||
"<" => *n < t,
|
||||
"<=" => *n <= t,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
// string comparison (case-insensitive) using compare_values semantics
|
||||
let rhs = CalcResult::String(criteria.to_lowercase());
|
||||
let lhs = match db_val {
|
||||
CalcResult::String(s) => CalcResult::String(s.to_lowercase()),
|
||||
x => x.clone(),
|
||||
};
|
||||
let c = compare_values(&lhs, &rhs);
|
||||
match op {
|
||||
">" => c > 0,
|
||||
">=" => c >= 0,
|
||||
"<" => c < 0,
|
||||
"<=" => c <= 0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
"<>" => {
|
||||
// not equal (with wildcard semantics for strings)
|
||||
// If rhs has wildcards and db_val is string, do regex; else use compare_values != 0
|
||||
if let CalcResult::String(s) = db_val {
|
||||
if criteria.contains('*') || criteria.contains('?') {
|
||||
if let Ok(re) = from_wildcard_to_regex(&criteria.to_lowercase(), true) {
|
||||
return !result_matches_regex(
|
||||
&CalcResult::String(s.to_lowercase()),
|
||||
&re,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let rhs = if let Some(n) = rhs_num {
|
||||
CalcResult::Number(n)
|
||||
} else {
|
||||
CalcResult::String(criteria.to_lowercase())
|
||||
};
|
||||
let lhs = match db_val {
|
||||
CalcResult::String(s) => CalcResult::String(s.to_lowercase()),
|
||||
x => x.clone(),
|
||||
};
|
||||
compare_values(&lhs, &rhs) != 0
|
||||
}
|
||||
_ => {
|
||||
// equality. For strings, support wildcards (*, ?)
|
||||
if let Some(n) = rhs_num {
|
||||
// numeric equals
|
||||
if let CalcResult::Number(m) = db_val {
|
||||
(*m - n).abs() <= f64::EPSILON
|
||||
} else {
|
||||
compare_values(db_val, &CalcResult::Number(n)) == 0
|
||||
}
|
||||
} else {
|
||||
// textual/boolean equals (case-insensitive), wildcard-enabled for strings
|
||||
if let CalcResult::String(s) = db_val {
|
||||
if criteria.contains('*') || criteria.contains('?') {
|
||||
if let Ok(re) = from_wildcard_to_regex(&criteria.to_lowercase(), true) {
|
||||
return result_matches_regex(
|
||||
&CalcResult::String(s.to_lowercase()),
|
||||
&re,
|
||||
);
|
||||
}
|
||||
}
|
||||
// This is weird but we only need to check if "starts with" for equality
|
||||
return s.to_lowercase().starts_with(&criteria.to_lowercase());
|
||||
}
|
||||
// Fallback: compare_values equality
|
||||
compare_values(db_val, &CalcResult::String(criteria.to_lowercase())) == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared implementation for DMAX/DMIN
|
||||
fn db_extreme(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
want_max: bool,
|
||||
) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let criteria = match self.get_reference(&args[2], cell) {
|
||||
Ok(r) => (r.left, r.right),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if db_right.row <= db_left.row {
|
||||
// no data rows
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "No data rows in database".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut best: Option<f64> = None;
|
||||
|
||||
let mut row = db_left.row + 1;
|
||||
while row <= db_right.row {
|
||||
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
|
||||
let v = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: db_left.sheet,
|
||||
row,
|
||||
column: field_col,
|
||||
});
|
||||
if let CalcResult::Number(value) = v {
|
||||
if value.is_finite() {
|
||||
best = Some(match best {
|
||||
None => value,
|
||||
Some(cur) => {
|
||||
if want_max {
|
||||
value.max(cur)
|
||||
} else {
|
||||
value.min(cur)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
row += 1;
|
||||
}
|
||||
|
||||
match best {
|
||||
Some(v) => CalcResult::Number(v),
|
||||
None => CalcResult::Number(0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,31 @@ use chrono::Months;
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::NaiveTime;
|
||||
use chrono::Timelike;
|
||||
use chrono_tz::Tz;
|
||||
|
||||
const SECONDS_PER_DAY: i32 = 86_400;
|
||||
const SECONDS_PER_DAY_F64: f64 = SECONDS_PER_DAY as f64;
|
||||
|
||||
fn is_leap_year(year: i32) -> bool {
|
||||
(year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)
|
||||
}
|
||||
|
||||
fn is_feb_29_between_dates(start: chrono::NaiveDate, end: chrono::NaiveDate) -> bool {
|
||||
let start_year = start.year();
|
||||
let end_year = end.year();
|
||||
|
||||
for year in start_year..=end_year {
|
||||
if is_leap_year(year)
|
||||
&& (year < end_year
|
||||
|| (year == end_year && end.month() > 2)
|
||||
&& (year > start_year || (year == start_year && start.month() <= 2)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper macros to eliminate boilerplate in date/time component extraction
|
||||
// functions (DAY, MONTH, YEAR, HOUR, MINUTE, SECOND).
|
||||
@@ -750,12 +771,12 @@ impl Model {
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
// Returns the current date/time as an Excel serial number in the model's configured timezone.
|
||||
// Returns the current date/time as an Excel serial number in the given timezone.
|
||||
// Used by TODAY() and NOW().
|
||||
fn current_excel_serial(&self) -> Option<f64> {
|
||||
pub(crate) fn current_excel_serial_with_timezone(&self, tz: Tz) -> Option<f64> {
|
||||
let seconds = get_milliseconds_since_epoch() / 1000;
|
||||
DateTime::from_timestamp(seconds, 0).map(|dt| {
|
||||
let local_time = dt.with_timezone(&self.tz);
|
||||
let local_time = dt.with_timezone(&tz);
|
||||
let days_from_1900 = local_time.num_days_from_ce() - EXCEL_DATE_BASE;
|
||||
let fraction = (local_time.num_seconds_from_midnight() as f64) / (60.0 * 60.0 * 24.0);
|
||||
days_from_1900 as f64 + fraction
|
||||
@@ -958,7 +979,7 @@ impl Model {
|
||||
message: "Wrong number of arguments".to_string(),
|
||||
};
|
||||
}
|
||||
match self.current_excel_serial() {
|
||||
match self.current_excel_serial_with_timezone(self.tz) {
|
||||
Some(serial) => CalcResult::Number(serial.floor()),
|
||||
None => CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
@@ -969,14 +990,35 @@ impl Model {
|
||||
}
|
||||
|
||||
pub(crate) fn fn_now(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if !args.is_empty() {
|
||||
if args.len() > 1 {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Wrong number of arguments".to_string(),
|
||||
};
|
||||
}
|
||||
match self.current_excel_serial() {
|
||||
let tz = match args.first() {
|
||||
Some(arg0) => {
|
||||
// Parse timezone argument
|
||||
let tz_str = match self.get_string(arg0, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let tz: Tz = match tz_str.parse() {
|
||||
Ok(tz) => tz,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: format!("Invalid timezone: {}", &tz_str),
|
||||
}
|
||||
}
|
||||
};
|
||||
tz
|
||||
}
|
||||
None => self.tz,
|
||||
};
|
||||
match self.current_excel_serial_with_timezone(tz) {
|
||||
Some(serial) => CalcResult::Number(serial),
|
||||
None => CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
@@ -1567,18 +1609,44 @@ impl Model {
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
let year_days = if start_date.year() == end_date.year() {
|
||||
if (start_date.year() % 4 == 0 && start_date.year() % 100 != 0)
|
||||
|| start_date.year() % 400 == 0
|
||||
{
|
||||
366.0
|
||||
} else {
|
||||
365.0
|
||||
// Procedure E
|
||||
|
||||
let start_year = start_date.year();
|
||||
let end_year = end_date.year();
|
||||
|
||||
let step_a = start_year != end_year;
|
||||
let step_b = start_year + 1 != end_year;
|
||||
let step_c = start_date.month() < end_date.month();
|
||||
let step_d = start_date.month() == end_date.month();
|
||||
let step_e = start_date.day() <= end_date.day();
|
||||
let step_f = step_a && (step_b || step_c || (step_d && step_e));
|
||||
if step_f {
|
||||
// 7.
|
||||
// return average of days in year between start_year and end_year, inclusive
|
||||
let mut total_days = 0;
|
||||
for year in start_year..=end_year {
|
||||
if is_leap_year(year) {
|
||||
total_days += 366;
|
||||
} else {
|
||||
total_days += 365;
|
||||
}
|
||||
}
|
||||
days / (total_days as f64 / (end_year - start_year + 1) as f64)
|
||||
} else if step_a && is_leap_year(start_year) {
|
||||
// 8.
|
||||
days / 366.0
|
||||
} else if is_feb_29_between_dates(start_date, end_date) {
|
||||
// 9. If a February 29 occurs between date1 and date2 then return 366
|
||||
days / 366.0
|
||||
} else if end_date.month() == 2 && end_date.day() == 29 {
|
||||
// 10. If date2 is February 29 then return 366
|
||||
days / 366.0
|
||||
} else if !step_a && is_leap_year(start_year) {
|
||||
days / 366.0
|
||||
} else {
|
||||
365.0
|
||||
};
|
||||
days / year_days
|
||||
// 11.
|
||||
days / 365.0
|
||||
}
|
||||
}
|
||||
2 => days / 360.0,
|
||||
3 => days / 365.0,
|
||||
@@ -1595,6 +1663,34 @@ impl Model {
|
||||
}
|
||||
_ => return CalcResult::new_error(Error::NUM, cell, "Invalid basis".to_string()),
|
||||
};
|
||||
CalcResult::Number(result)
|
||||
CalcResult::Number(result.abs())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_leap_year() {
|
||||
assert!(is_leap_year(2000));
|
||||
assert!(!is_leap_year(1900));
|
||||
assert!(is_leap_year(2004));
|
||||
assert!(!is_leap_year(2001));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_feb_29_between_dates() {
|
||||
let d1 = chrono::NaiveDate::from_ymd_opt(2020, 2, 28).unwrap();
|
||||
let d2 = chrono::NaiveDate::from_ymd_opt(2020, 3, 1).unwrap();
|
||||
assert!(is_feb_29_between_dates(d1, d2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_feb_29_between_dates_false() {
|
||||
let d1 = chrono::NaiveDate::from_ymd_opt(2021, 2, 28).unwrap();
|
||||
let d2 = chrono::NaiveDate::from_ymd_opt(2021, 3, 1).unwrap();
|
||||
assert!(!is_feb_29_between_dates(d1, d2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use statrs::function::erf::{erf, erfc};
|
||||
|
||||
use crate::{
|
||||
calc_result::CalcResult,
|
||||
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
|
||||
model::Model,
|
||||
};
|
||||
|
||||
use super::transcendental::{bessel_i, bessel_j, bessel_k, bessel_y, erf};
|
||||
use super::transcendental::{bessel_i, bessel_j, bessel_k, bessel_y};
|
||||
// https://root.cern/doc/v610/TMath_8cxx_source.html
|
||||
|
||||
// Notice that the parameters for Bessel functions in Excel and here have inverted order
|
||||
@@ -160,7 +162,7 @@ impl Model {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
CalcResult::Number(1.0 - erf(x))
|
||||
CalcResult::Number(erfc(x))
|
||||
}
|
||||
|
||||
pub(crate) fn fn_erfcprecise(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
@@ -171,6 +173,6 @@ impl Model {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
CalcResult::Number(1.0 - erf(x))
|
||||
CalcResult::Number(erfc(x))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
pub(crate) fn erf(x: f64) -> f64 {
|
||||
let cof = vec![
|
||||
-1.3026537197817094,
|
||||
6.419_697_923_564_902e-1,
|
||||
1.9476473204185836e-2,
|
||||
-9.561_514_786_808_63e-3,
|
||||
-9.46595344482036e-4,
|
||||
3.66839497852761e-4,
|
||||
4.2523324806907e-5,
|
||||
-2.0278578112534e-5,
|
||||
-1.624290004647e-6,
|
||||
1.303655835580e-6,
|
||||
1.5626441722e-8,
|
||||
-8.5238095915e-8,
|
||||
6.529054439e-9,
|
||||
5.059343495e-9,
|
||||
-9.91364156e-10,
|
||||
-2.27365122e-10,
|
||||
9.6467911e-11,
|
||||
2.394038e-12,
|
||||
-6.886027e-12,
|
||||
8.94487e-13,
|
||||
3.13092e-13,
|
||||
-1.12708e-13,
|
||||
3.81e-16,
|
||||
7.106e-15,
|
||||
-1.523e-15,
|
||||
-9.4e-17,
|
||||
1.21e-16,
|
||||
-2.8e-17,
|
||||
];
|
||||
|
||||
let mut d = 0.0;
|
||||
let mut dd = 0.0;
|
||||
|
||||
let x_abs = x.abs();
|
||||
|
||||
let t = 2.0 / (2.0 + x_abs);
|
||||
let ty = 4.0 * t - 2.0;
|
||||
|
||||
for j in (1..=cof.len() - 1).rev() {
|
||||
let tmp = d;
|
||||
d = ty * d - dd + cof[j];
|
||||
dd = tmp;
|
||||
}
|
||||
|
||||
let res = t * f64::exp(-x_abs * x_abs + 0.5 * (cof[0] + ty * d) - dd);
|
||||
if x < 0.0 {
|
||||
res - 1.0
|
||||
} else {
|
||||
1.0 - res
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ mod bessel_j1_y1;
|
||||
mod bessel_jn_yn;
|
||||
mod bessel_k;
|
||||
mod bessel_util;
|
||||
mod erf;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_bessel;
|
||||
@@ -13,4 +12,3 @@ pub(crate) use bessel_i::bessel_i;
|
||||
pub(crate) use bessel_jn_yn::jn as bessel_j;
|
||||
pub(crate) use bessel_jn_yn::yn as bessel_y;
|
||||
pub(crate) use bessel_k::bessel_k;
|
||||
pub(crate) use erf::erf;
|
||||
|
||||
@@ -68,14 +68,14 @@ macro_rules! single_number_fn {
|
||||
},
|
||||
// If String, parse to f64 then apply or #VALUE! error
|
||||
ArrayNode::String(s) => {
|
||||
let node = match s.parse::<f64>() {
|
||||
Ok(f) => match $op(f) {
|
||||
let node = match self.cast_number(&s) {
|
||||
Some(f) => match $op(f) {
|
||||
Ok(x) => ArrayNode::Number(x),
|
||||
Err(Error::DIV) => ArrayNode::Error(Error::DIV),
|
||||
Err(Error::VALUE) => ArrayNode::Error(Error::VALUE),
|
||||
Err(e) => ArrayNode::Error(e),
|
||||
},
|
||||
Err(_) => ArrayNode::Error(Error::VALUE),
|
||||
None => ArrayNode::Error(Error::VALUE),
|
||||
};
|
||||
data_row.push(node);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::cast::NumberOrArray;
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::constants::{EXCEL_PRECISION, LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::parser::ArrayNode;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::functions::math_util::{from_roman, to_roman_with_form};
|
||||
use crate::number_format::to_precision;
|
||||
use crate::number_format::{to_excel_precision, to_precision};
|
||||
use crate::single_number_fn;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
@@ -934,11 +934,11 @@ impl Model {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
let value = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let divisor = match self.get_number(&args[1], cell) {
|
||||
let divisor = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
@@ -984,7 +984,9 @@ impl Model {
|
||||
};
|
||||
}
|
||||
|
||||
let result = f64::floor(value / significance) * significance;
|
||||
// Apply Excel precision to the ratio to handle floating-point rounding errors
|
||||
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||
let result = f64::floor(ratio) * significance;
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
@@ -1022,7 +1024,7 @@ impl Model {
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
let arg_count = args.len();
|
||||
if arg_count > 3 {
|
||||
if !(1..=3).contains(&arg_count) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
@@ -1063,7 +1065,7 @@ impl Model {
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
let arg_count = args.len();
|
||||
if arg_count > 2 {
|
||||
if !(1..=2).contains(&arg_count) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
@@ -1093,7 +1095,7 @@ impl Model {
|
||||
|
||||
pub(crate) fn fn_floor_math(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let arg_count = args.len();
|
||||
if arg_count > 3 {
|
||||
if !(1..=3).contains(&arg_count) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
@@ -1121,10 +1123,14 @@ impl Model {
|
||||
}
|
||||
let significance = significance.abs();
|
||||
if value < 0.0 && mode != 0.0 {
|
||||
let result = f64::ceil(value / significance) * significance;
|
||||
// Apply Excel precision to handle floating-point rounding errors
|
||||
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||
let result = f64::ceil(ratio) * significance;
|
||||
CalcResult::Number(result)
|
||||
} else {
|
||||
let result = f64::floor(value / significance) * significance;
|
||||
// Apply Excel precision to handle floating-point rounding errors
|
||||
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||
let result = f64::floor(ratio) * significance;
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
}
|
||||
@@ -1135,7 +1141,7 @@ impl Model {
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
let arg_count = args.len();
|
||||
if arg_count > 2 {
|
||||
if !(1..=2).contains(&arg_count) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
@@ -1154,7 +1160,9 @@ impl Model {
|
||||
return CalcResult::Number(0.0);
|
||||
}
|
||||
|
||||
let result = f64::floor(value / significance) * significance;
|
||||
// Apply Excel precision to handle floating-point rounding errors
|
||||
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||
let result = f64::floor(ratio) * significance;
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
@@ -1163,11 +1171,34 @@ impl Model {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
|
||||
let value = self.evaluate_node_in_context(&args[0], cell);
|
||||
|
||||
let multiple = self.evaluate_node_in_context(&args[1], cell);
|
||||
|
||||
// if either is empty => #N/A
|
||||
if matches!(value, CalcResult::EmptyArg) || matches!(multiple, CalcResult::EmptyArg) {
|
||||
return CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Bad argument for MROUND".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// Booleans are not cast
|
||||
if matches!(value, CalcResult::Boolean(_)) {
|
||||
return CalcResult::new_error(Error::VALUE, cell, "Expecting number".to_string());
|
||||
}
|
||||
|
||||
if matches!(multiple, CalcResult::Boolean(_)) {
|
||||
return CalcResult::new_error(Error::VALUE, cell, "Expecting number".to_string());
|
||||
}
|
||||
|
||||
let value = match self.cast_to_number(value, cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let multiple = match self.get_number(&args[1], cell) {
|
||||
let multiple = match self.cast_to_number(multiple, cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
@@ -1186,7 +1217,7 @@ impl Model {
|
||||
}
|
||||
|
||||
pub(crate) fn fn_trunc(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() > 2 {
|
||||
if !(1..=2).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
@@ -1210,11 +1241,15 @@ impl Model {
|
||||
if !(-15.0..=15.0).contains(&num_digits) {
|
||||
return CalcResult::Number(value);
|
||||
}
|
||||
CalcResult::Number(if value >= 0.0 {
|
||||
let v = if value >= 0.0 {
|
||||
f64::floor(value * 10f64.powf(num_digits)) / 10f64.powf(num_digits)
|
||||
} else {
|
||||
f64::ceil(value * 10f64.powf(num_digits)) / 10f64.powf(num_digits)
|
||||
})
|
||||
};
|
||||
if value.is_finite() && v.is_infinite() {
|
||||
return CalcResult::Number(value);
|
||||
}
|
||||
CalcResult::Number(v)
|
||||
}
|
||||
|
||||
single_number_fn!(fn_log10, |f| if f <= 0.0 {
|
||||
@@ -1250,13 +1285,19 @@ impl Model {
|
||||
} else {
|
||||
Ok((f * PI).sqrt())
|
||||
});
|
||||
single_number_fn!(fn_acot, |f| if f == 0.0 {
|
||||
Err(Error::DIV)
|
||||
} else {
|
||||
Ok(f64::atan(1.0 / f))
|
||||
|
||||
single_number_fn!(fn_acot, |f| {
|
||||
let v = f64::atan(1.0 / f);
|
||||
if f >= 0.0 {
|
||||
Ok(v)
|
||||
} else {
|
||||
// To be compatible with Excel we need a different branch
|
||||
// when f < 0
|
||||
Ok(v + PI)
|
||||
}
|
||||
});
|
||||
single_number_fn!(fn_acoth, |f: f64| if f.abs() == 1.0 {
|
||||
Err(Error::DIV)
|
||||
Err(Error::NUM)
|
||||
} else {
|
||||
Ok(0.5 * (f64::ln((f + 1.0) / (f - 1.0))))
|
||||
});
|
||||
@@ -1265,8 +1306,11 @@ impl Model {
|
||||
} else {
|
||||
Ok(f64::cos(f) / f64::sin(f))
|
||||
});
|
||||
single_number_fn!(fn_coth, |f| if f == 0.0 {
|
||||
single_number_fn!(fn_coth, |f: f64| if f == 0.0 {
|
||||
Err(Error::DIV)
|
||||
} else if f.abs() > 20.0 {
|
||||
// for values > 20.0 this is exact in f64
|
||||
Ok(f.signum())
|
||||
} else {
|
||||
Ok(f64::cosh(f) / f64::sinh(f))
|
||||
});
|
||||
@@ -1280,16 +1324,8 @@ impl Model {
|
||||
} else {
|
||||
Ok(1.0 / f64::sinh(f))
|
||||
});
|
||||
single_number_fn!(fn_sec, |f| if f == 0.0 {
|
||||
Err(Error::DIV)
|
||||
} else {
|
||||
Ok(1.0 / f64::cos(f))
|
||||
});
|
||||
single_number_fn!(fn_sech, |f| if f == 0.0 {
|
||||
Err(Error::DIV)
|
||||
} else {
|
||||
Ok(1.0 / f64::cosh(f))
|
||||
});
|
||||
single_number_fn!(fn_sec, |f| Ok(1.0 / f64::cos(f)));
|
||||
single_number_fn!(fn_sech, |f| Ok(1.0 / f64::cosh(f)));
|
||||
single_number_fn!(fn_exp, |f: f64| Ok(f64::exp(f)));
|
||||
single_number_fn!(fn_fact, |x: f64| {
|
||||
let x = x.floor();
|
||||
@@ -1320,7 +1356,13 @@ impl Model {
|
||||
}
|
||||
Ok(acc)
|
||||
});
|
||||
single_number_fn!(fn_sign, |f| Ok(f64::signum(f)));
|
||||
single_number_fn!(fn_sign, |f| {
|
||||
if f == 0.0 {
|
||||
Ok(0.0)
|
||||
} else {
|
||||
Ok(f64::signum(f))
|
||||
}
|
||||
});
|
||||
single_number_fn!(fn_degrees, |f| Ok(f * (180.0 / PI)));
|
||||
single_number_fn!(fn_radians, |f| Ok(f * (PI / 180.0)));
|
||||
single_number_fn!(fn_odd, |f| {
|
||||
|
||||
230
base/src/functions/mathematical_sum.rs
Normal file
230
base/src/functions/mathematical_sum.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
type TwoMatricesResult = (i32, i32, Vec<Option<f64>>, Vec<Option<f64>>);
|
||||
|
||||
// Helper to check if two shapes are the same or compatible 1D shapes
|
||||
fn is_same_shape_or_1d(rows1: i32, cols1: i32, rows2: i32, cols2: i32) -> bool {
|
||||
(rows1 == rows2 && cols1 == cols2)
|
||||
|| (rows1 == 1 && cols2 == 1 && cols1 == rows2)
|
||||
|| (rows2 == 1 && cols1 == 1 && cols2 == rows1)
|
||||
}
|
||||
|
||||
impl Model {
|
||||
// SUMX2MY2(array_x, array_y) - Returns the sum of the difference of squares
|
||||
pub(crate) fn fn_sumx2my2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let result = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let (_, _, values_left, values_right) = result;
|
||||
|
||||
let mut sum = 0.0;
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
let x = x_opt.unwrap_or(0.0);
|
||||
let y = y_opt.unwrap_or(0.0);
|
||||
sum += x * x - y * y;
|
||||
}
|
||||
|
||||
CalcResult::Number(sum)
|
||||
}
|
||||
|
||||
// SUMX2PY2(array_x, array_y) - Returns the sum of the sum of squares
|
||||
pub(crate) fn fn_sumx2py2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let result = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let (_rows, _cols, values_left, values_right) = result;
|
||||
|
||||
let mut sum = 0.0;
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
let x = x_opt.unwrap_or(0.0);
|
||||
let y = y_opt.unwrap_or(0.0);
|
||||
sum += x * x + y * y;
|
||||
}
|
||||
|
||||
CalcResult::Number(sum)
|
||||
}
|
||||
|
||||
// SUMXMY2(array_x, array_y) - Returns the sum of squares of differences
|
||||
pub(crate) fn fn_sumxmy2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let result = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let (_, _, values_left, values_right) = result;
|
||||
|
||||
let mut sum = 0.0;
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
let x = x_opt.unwrap_or(0.0);
|
||||
let y = y_opt.unwrap_or(0.0);
|
||||
let diff = x - y;
|
||||
sum += diff * diff;
|
||||
}
|
||||
|
||||
CalcResult::Number(sum)
|
||||
}
|
||||
|
||||
// Helper function to extract and validate two matrices (ranges or arrays) with compatible shapes.
|
||||
// Returns (rows, cols, values_left, values_right) or an error.
|
||||
pub(crate) fn fn_get_two_matrices(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> Result<TwoMatricesResult, CalcResult> {
|
||||
if args.len() != 2 {
|
||||
return Err(CalcResult::new_args_number_error(cell));
|
||||
}
|
||||
let x_range = self.evaluate_node_in_context(&args[0], cell);
|
||||
let y_range = self.evaluate_node_in_context(&args[1], cell);
|
||||
|
||||
let result = match (x_range, y_range) {
|
||||
(
|
||||
CalcResult::Range {
|
||||
left: l1,
|
||||
right: r1,
|
||||
},
|
||||
CalcResult::Range {
|
||||
left: l2,
|
||||
right: r2,
|
||||
},
|
||||
) => {
|
||||
if l1.sheet != l2.sheet {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
));
|
||||
}
|
||||
let rows1 = r1.row - l1.row + 1;
|
||||
let cols1 = r1.column - l1.column + 1;
|
||||
let rows2 = r2.row - l2.row + 1;
|
||||
let cols2 = r2.column - l2.column + 1;
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = self.values_from_range(l1, r1)?;
|
||||
let values_right = self.values_from_range(l2, r2)?;
|
||||
(rows1, cols1, values_left, values_right)
|
||||
}
|
||||
(
|
||||
CalcResult::Array(left),
|
||||
CalcResult::Range {
|
||||
left: l2,
|
||||
right: r2,
|
||||
},
|
||||
) => {
|
||||
let rows2 = r2.row - l2.row + 1;
|
||||
let cols2 = r2.column - l2.column + 1;
|
||||
|
||||
let rows1 = left.len() as i32;
|
||||
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Array and range must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = match self.values_from_array(left) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in first array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
let values_right = self.values_from_range(l2, r2)?;
|
||||
(rows2, cols2, values_left, values_right)
|
||||
}
|
||||
(
|
||||
CalcResult::Range {
|
||||
left: l1,
|
||||
right: r1,
|
||||
},
|
||||
CalcResult::Array(right),
|
||||
) => {
|
||||
let rows1 = r1.row - l1.row + 1;
|
||||
let cols1 = r1.column - l1.column + 1;
|
||||
|
||||
let rows2 = right.len() as i32;
|
||||
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Range and array must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = self.values_from_range(l1, r1)?;
|
||||
let values_right = match self.values_from_array(right) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
(rows1, cols1, values_left, values_right)
|
||||
}
|
||||
(CalcResult::Array(left), CalcResult::Array(right)) => {
|
||||
let rows1 = left.len() as i32;
|
||||
let rows2 = right.len() as i32;
|
||||
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
|
||||
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
|
||||
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Arrays must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = match self.values_from_array(left) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in first array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
let values_right = match self.values_from_array(right) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
(rows1, cols1, values_left, values_right)
|
||||
}
|
||||
_ => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Both arguments must be ranges or arrays".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub(crate) mod binary_search;
|
||||
mod database;
|
||||
mod date_and_time;
|
||||
mod engineering;
|
||||
mod financial;
|
||||
@@ -18,6 +19,7 @@ mod lookup_and_reference;
|
||||
mod macros;
|
||||
mod math_util;
|
||||
mod mathematical;
|
||||
mod mathematical_sum;
|
||||
mod statistical;
|
||||
mod subtotal;
|
||||
mod text;
|
||||
@@ -75,6 +77,9 @@ pub enum Function {
|
||||
Sum,
|
||||
Sumif,
|
||||
Sumifs,
|
||||
Sumx2my2,
|
||||
Sumx2py2,
|
||||
Sumxmy2,
|
||||
Tan,
|
||||
Tanh,
|
||||
Acot,
|
||||
@@ -189,6 +194,92 @@ pub enum Function {
|
||||
Minifs,
|
||||
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,
|
||||
Datedif,
|
||||
@@ -310,10 +401,30 @@ pub enum Function {
|
||||
Delta,
|
||||
Gestep,
|
||||
Subtotal,
|
||||
|
||||
// Database
|
||||
Daverage,
|
||||
Dcount,
|
||||
Dget,
|
||||
Dmax,
|
||||
Dmin,
|
||||
Dsum,
|
||||
Dcounta,
|
||||
Dproduct,
|
||||
Dstdev,
|
||||
Dvar,
|
||||
Dvarp,
|
||||
Dstdevp,
|
||||
|
||||
Correl,
|
||||
Rsq,
|
||||
Intercept,
|
||||
Slope,
|
||||
Steyx,
|
||||
}
|
||||
|
||||
impl Function {
|
||||
pub fn into_iter() -> IntoIter<Function, 256> {
|
||||
pub fn into_iter() -> IntoIter<Function, 345> {
|
||||
[
|
||||
Function::And,
|
||||
Function::False,
|
||||
@@ -390,6 +501,9 @@ impl Function {
|
||||
Function::Sum,
|
||||
Function::Sumif,
|
||||
Function::Sumifs,
|
||||
Function::Sumx2my2,
|
||||
Function::Sumx2py2,
|
||||
Function::Sumxmy2,
|
||||
Function::Choose,
|
||||
Function::Column,
|
||||
Function::Columns,
|
||||
@@ -438,6 +552,7 @@ impl Function {
|
||||
Function::Type,
|
||||
Function::Sheet,
|
||||
Function::Average,
|
||||
Function::Avedev,
|
||||
Function::Averagea,
|
||||
Function::Averageif,
|
||||
Function::Averageifs,
|
||||
@@ -571,6 +686,91 @@ impl Function {
|
||||
Function::Cell,
|
||||
Function::Info,
|
||||
Function::Sheets,
|
||||
Function::Daverage,
|
||||
Function::Dcount,
|
||||
Function::Dget,
|
||||
Function::Dmax,
|
||||
Function::Dmin,
|
||||
Function::Dsum,
|
||||
Function::Dcounta,
|
||||
Function::Dproduct,
|
||||
Function::Dstdev,
|
||||
Function::Dvar,
|
||||
Function::Dvarp,
|
||||
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()
|
||||
}
|
||||
@@ -624,6 +824,78 @@ impl Function {
|
||||
Function::Arabic => "_xlfn.ARABIC".to_string(),
|
||||
Function::Combina => "_xlfn.COMBINA".to_string(),
|
||||
Function::Sheets => "_xlfn.SHEETS".to_string(),
|
||||
Function::Acoth => "_xlfn.ACOTH".to_string(),
|
||||
Function::Cot => "_xlfn.COT".to_string(),
|
||||
Function::Coth => "_xlfn.COTH".to_string(),
|
||||
Function::Csc => "_xlfn.CSC".to_string(),
|
||||
Function::Csch => "_xlfn.CSCH".to_string(),
|
||||
Function::Sec => "_xlfn.SEC".to_string(),
|
||||
Function::Sech => "_xlfn.SECH".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(),
|
||||
}
|
||||
@@ -659,14 +931,14 @@ impl Function {
|
||||
"ASINH" => Some(Function::Asinh),
|
||||
"ACOSH" => Some(Function::Acosh),
|
||||
"ATANH" => Some(Function::Atanh),
|
||||
"ACOT" => Some(Function::Acot),
|
||||
"COTH" => Some(Function::Coth),
|
||||
"COT" => Some(Function::Cot),
|
||||
"CSC" => Some(Function::Csc),
|
||||
"CSCH" => Some(Function::Csch),
|
||||
"SEC" => Some(Function::Sec),
|
||||
"SECH" => Some(Function::Sech),
|
||||
"ACOTH" => Some(Function::Acoth),
|
||||
"ACOT" | "_XLFN.ACOT" => Some(Function::Acot),
|
||||
"COTH" | "_XLFN.COTH" => Some(Function::Coth),
|
||||
"COT" | "_XLFN.COT" => Some(Function::Cot),
|
||||
"CSC" | "_XLFN.CSC" => Some(Function::Csc),
|
||||
"CSCH" | "_XLFN.CSCH" => Some(Function::Csch),
|
||||
"SEC" | "_XLFN.SEC" => Some(Function::Sec),
|
||||
"SECH" | "_XLFN.SECH" => Some(Function::Sech),
|
||||
"ACOTH" | "_XLFN.ACOTH" => Some(Function::Acoth),
|
||||
"FACT" => Some(Function::Fact),
|
||||
"FACTDOUBLE" => Some(Function::Factdouble),
|
||||
"EXP" => Some(Function::Exp),
|
||||
@@ -776,6 +1048,7 @@ impl Function {
|
||||
|
||||
"AVERAGE" => Some(Function::Average),
|
||||
"AVERAGEA" => Some(Function::Averagea),
|
||||
"AVEDEV" => Some(Function::Avedev),
|
||||
"AVERAGEIF" => Some(Function::Averageif),
|
||||
"AVERAGEIFS" => Some(Function::Averageifs),
|
||||
"COUNT" => Some(Function::Count),
|
||||
@@ -909,6 +1182,98 @@ impl Function {
|
||||
"INFO" => Some(Function::Info),
|
||||
"SHEETS" | "_XLFN.SHEETS" => Some(Function::Sheets),
|
||||
|
||||
"DAVERAGE" => Some(Function::Daverage),
|
||||
"DCOUNT" => Some(Function::Dcount),
|
||||
"DGET" => Some(Function::Dget),
|
||||
"DMAX" => Some(Function::Dmax),
|
||||
"DMIN" => Some(Function::Dmin),
|
||||
"DSUM" => Some(Function::Dsum),
|
||||
"DCOUNTA" => Some(Function::Dcounta),
|
||||
"DPRODUCT" => Some(Function::Dproduct),
|
||||
"DSTDEV" => Some(Function::Dstdev),
|
||||
"DVAR" => Some(Function::Dvar),
|
||||
"DVARP" => Some(Function::Dvarp),
|
||||
"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,
|
||||
}
|
||||
}
|
||||
@@ -1017,6 +1382,7 @@ impl fmt::Display for Function {
|
||||
Function::Sheet => write!(f, "SHEET"),
|
||||
Function::Average => write!(f, "AVERAGE"),
|
||||
Function::Averagea => write!(f, "AVERAGEA"),
|
||||
Function::Avedev => write!(f, "AVEDEV"),
|
||||
Function::Averageif => write!(f, "AVERAGEIF"),
|
||||
Function::Averageifs => write!(f, "AVERAGEIFS"),
|
||||
Function::Count => write!(f, "COUNT"),
|
||||
@@ -1169,11 +1535,99 @@ impl fmt::Display for Function {
|
||||
Function::Combin => write!(f, "COMBIN"),
|
||||
Function::Combina => write!(f, "COMBINA"),
|
||||
Function::Sumsq => write!(f, "SUMSQ"),
|
||||
|
||||
Function::N => write!(f, "N"),
|
||||
Function::Cell => write!(f, "CELL"),
|
||||
Function::Info => write!(f, "INFO"),
|
||||
Function::Sheets => write!(f, "SHEETS"),
|
||||
Function::Daverage => write!(f, "DAVERAGE"),
|
||||
Function::Dcount => write!(f, "DCOUNT"),
|
||||
Function::Dget => write!(f, "DGET"),
|
||||
Function::Dmax => write!(f, "DMAX"),
|
||||
Function::Dmin => write!(f, "DMIN"),
|
||||
Function::Dsum => write!(f, "DSUM"),
|
||||
Function::Dcounta => write!(f, "DCOUNTA"),
|
||||
Function::Dproduct => write!(f, "DPRODUCT"),
|
||||
Function::Dstdev => write!(f, "DSTDEV"),
|
||||
Function::Dvar => write!(f, "DVAR"),
|
||||
Function::Dvarp => write!(f, "DVARP"),
|
||||
Function::Dstdevp => write!(f, "DSTDEVP"),
|
||||
Function::BetaDist => write!(f, "BETA.DIST"),
|
||||
Function::BetaInv => write!(f, "BETA.INV"),
|
||||
Function::BinomDist => write!(f, "BINOM.DIST"),
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1294,6 +1748,7 @@ impl Model {
|
||||
Function::Sheet => self.fn_sheet(args, cell),
|
||||
Function::Average => self.fn_average(args, cell),
|
||||
Function::Averagea => self.fn_averagea(args, cell),
|
||||
Function::Avedev => self.fn_avedev(args, cell),
|
||||
Function::Averageif => self.fn_averageif(args, cell),
|
||||
Function::Averageifs => self.fn_averageifs(args, cell),
|
||||
Function::Count => self.fn_count(args, cell),
|
||||
@@ -1458,6 +1913,94 @@ impl Model {
|
||||
Function::Cell => self.fn_cell(args, cell),
|
||||
Function::Info => self.fn_info(args, cell),
|
||||
Function::Sheets => self.fn_sheets(args, cell),
|
||||
Function::Daverage => self.fn_daverage(args, cell),
|
||||
Function::Dcount => self.fn_dcount(args, cell),
|
||||
Function::Dget => self.fn_dget(args, cell),
|
||||
Function::Dmax => self.fn_dmax(args, cell),
|
||||
Function::Dmin => self.fn_dmin(args, cell),
|
||||
Function::Dsum => self.fn_dsum(args, cell),
|
||||
Function::Dcounta => self.fn_dcounta(args, cell),
|
||||
Function::Dproduct => self.fn_dproduct(args, cell),
|
||||
Function::Dstdev => self.fn_dstdev(args, cell),
|
||||
Function::Dvar => self.fn_dvar(args, cell),
|
||||
Function::Dvarp => self.fn_dvarp(args, cell),
|
||||
Function::Dstdevp => self.fn_dstdevp(args, cell),
|
||||
Function::BetaDist => self.fn_beta_dist(args, cell),
|
||||
Function::BetaInv => self.fn_beta_inv(args, cell),
|
||||
Function::BinomDist => self.fn_binom_dist(args, cell),
|
||||
Function::BinomDistRange => self.fn_binom_dist_range(args, cell),
|
||||
Function::BinomInv => self.fn_binom_inv(args, cell),
|
||||
Function::ChisqDist => self.fn_chisq_dist(args, cell),
|
||||
Function::ChisqDistRT => self.fn_chisq_dist_rt(args, cell),
|
||||
Function::ChisqInv => self.fn_chisq_inv(args, cell),
|
||||
Function::ChisqInvRT => self.fn_chisq_inv_rt(args, cell),
|
||||
Function::ChisqTest => self.fn_chisq_test(args, cell),
|
||||
Function::ConfidenceNorm => self.fn_confidence_norm(args, cell),
|
||||
Function::ConfidenceT => self.fn_confidence_t(args, cell),
|
||||
Function::CovarianceP => self.fn_covariance_p(args, cell),
|
||||
Function::CovarianceS => self.fn_covariance_s(args, cell),
|
||||
Function::Devsq => self.fn_devsq(args, cell),
|
||||
Function::ExponDist => self.fn_expon_dist(args, cell),
|
||||
Function::FDist => self.fn_f_dist(args, cell),
|
||||
Function::FDistRT => self.fn_f_dist_rt(args, cell),
|
||||
Function::FInv => self.fn_f_inv(args, cell),
|
||||
Function::FInvRT => self.fn_f_inv_rt(args, cell),
|
||||
Function::Fisher => self.fn_fisher(args, cell),
|
||||
Function::FisherInv => self.fn_fisher_inv(args, cell),
|
||||
Function::FTest => self.fn_f_test(args, cell),
|
||||
Function::Gamma => self.fn_gamma(args, cell),
|
||||
Function::GammaDist => self.fn_gamma_dist(args, cell),
|
||||
Function::GammaInv => self.fn_gamma_inv(args, cell),
|
||||
Function::GammaLn => self.fn_gamma_ln(args, cell),
|
||||
Function::GammaLnPrecise => self.fn_gamma_ln_precise(args, cell),
|
||||
Function::HypGeomDist => self.fn_hyp_geom_dist(args, cell),
|
||||
Function::LogNormDist => self.fn_log_norm_dist(args, cell),
|
||||
Function::LogNormInv => self.fn_log_norm_inv(args, cell),
|
||||
Function::NegbinomDist => self.fn_negbinom_dist(args, cell),
|
||||
Function::NormDist => self.fn_norm_dist(args, cell),
|
||||
Function::NormInv => self.fn_norm_inv(args, cell),
|
||||
Function::NormSdist => self.fn_norm_s_dist(args, cell),
|
||||
Function::NormSInv => self.fn_norm_s_inv(args, cell),
|
||||
Function::Pearson => self.fn_pearson(args, cell),
|
||||
Function::Phi => self.fn_phi(args, cell),
|
||||
Function::PoissonDist => self.fn_poisson_dist(args, cell),
|
||||
Function::Standardize => self.fn_standardize(args, cell),
|
||||
Function::StDevP => self.fn_stdev_p(args, cell),
|
||||
Function::StDevS => self.fn_stdev_s(args, cell),
|
||||
Function::Stdeva => self.fn_stdeva(args, cell),
|
||||
Function::Stdevpa => self.fn_stdevpa(args, cell),
|
||||
Function::TDist => self.fn_t_dist(args, cell),
|
||||
Function::TDist2T => self.fn_t_dist_2t(args, cell),
|
||||
Function::TDistRT => self.fn_t_dist_rt(args, cell),
|
||||
Function::TInv => self.fn_t_inv(args, cell),
|
||||
Function::TInv2T => self.fn_t_inv_2t(args, cell),
|
||||
Function::TTest => self.fn_t_test(args, cell),
|
||||
Function::VarP => self.fn_var_p(args, cell),
|
||||
Function::VarS => self.fn_var_s(args, cell),
|
||||
Function::VarpA => self.fn_varpa(args, cell),
|
||||
Function::VarA => self.fn_vara(args, cell),
|
||||
Function::WeibullDist => self.fn_weibull_dist(args, cell),
|
||||
Function::ZTest => self.fn_z_test(args, cell),
|
||||
Function::Sumx2my2 => self.fn_sumx2my2(args, cell),
|
||||
Function::Sumx2py2 => self.fn_sumx2py2(args, cell),
|
||||
Function::Sumxmy2 => self.fn_sumxmy2(args, cell),
|
||||
Function::Correl => self.fn_correl(args, cell),
|
||||
Function::Rsq => self.fn_rsq(args, cell),
|
||||
Function::Intercept => self.fn_intercept(args, cell),
|
||||
Function::Slope => self.fn_slope(args, cell),
|
||||
Function::Steyx => self.fn_steyx(args, cell),
|
||||
Function::Gauss => self.fn_gauss(args, cell),
|
||||
Function::Harmean => self.fn_harmean(args, cell),
|
||||
Function::Kurt => self.fn_kurt(args, cell),
|
||||
Function::Large => self.fn_large(args, cell),
|
||||
Function::MaxA => self.fn_maxa(args, cell),
|
||||
Function::Median => self.fn_median(args, cell),
|
||||
Function::MinA => self.fn_mina(args, cell),
|
||||
Function::RankAvg => self.fn_rank_avg(args, cell),
|
||||
Function::RankEq => self.fn_rank_eq(args, cell),
|
||||
Function::Skew => self.fn_skew(args, cell),
|
||||
Function::SkewP => self.fn_skew_p(args, cell),
|
||||
Function::Small => self.fn_small(args, cell),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,733 +0,0 @@
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::{CalcResult, Range},
|
||||
expressions::parser::Node,
|
||||
expressions::token::Error,
|
||||
model::Model,
|
||||
};
|
||||
|
||||
use super::util::build_criteria;
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut count = 0.0;
|
||||
let mut sum = 0.0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
sum += value;
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
} else {
|
||||
sum += if b { 1.0 } else { 0.0 };
|
||||
count += 1.0;
|
||||
}
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
sum += value;
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::Range { .. } => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
"Unexpected Range".to_string(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::String(s) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
// Do nothing
|
||||
} else if let Ok(t) = s.parse::<f64>() {
|
||||
sum += t;
|
||||
count += 1.0;
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Argument cannot be cast into number".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Ignore everything else
|
||||
}
|
||||
};
|
||||
}
|
||||
if count == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Division by Zero".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(sum / count)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_averagea(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut count = 0.0;
|
||||
let mut sum = 0.0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::String(_) => count += 1.0,
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
sum += value;
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
if b {
|
||||
sum += 1.0;
|
||||
}
|
||||
count += 1.0;
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::Range { .. } => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
"Unexpected Range".to_string(),
|
||||
);
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
sum += value;
|
||||
}
|
||||
CalcResult::String(s) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
// Do nothing
|
||||
count += 1.0;
|
||||
} else if let Ok(t) = s.parse::<f64>() {
|
||||
sum += t;
|
||||
count += 1.0;
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Argument cannot be cast into number".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
count += 1.0;
|
||||
if b {
|
||||
sum += 1.0;
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
CalcResult::Array(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
if count == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Division by Zero".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(sum / count)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_count(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut result = 0.0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(_) => {
|
||||
result += 1.0;
|
||||
}
|
||||
CalcResult::Boolean(_) => {
|
||||
if !matches!(arg, Node::ReferenceKind { .. }) {
|
||||
result += 1.0;
|
||||
}
|
||||
}
|
||||
CalcResult::String(s) => {
|
||||
if !matches!(arg, Node::ReferenceKind { .. }) && s.parse::<f64>().is_ok() {
|
||||
result += 1.0;
|
||||
}
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
if let CalcResult::Number(_) = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
result += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Ignore everything else
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_counta(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut result = 0.0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
_ => {
|
||||
result += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
result += 1.0;
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_countblank(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
// COUNTBLANK requires only one argument
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut result = 0.0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => result += 1.0,
|
||||
CalcResult::String(s) => {
|
||||
if s.is_empty() {
|
||||
result += 1.0
|
||||
}
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => result += 1.0,
|
||||
CalcResult::String(s) => {
|
||||
if s.is_empty() {
|
||||
result += 1.0
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_countif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() == 2 {
|
||||
let arguments = vec![args[0].clone(), args[1].clone()];
|
||||
self.fn_countifs(&arguments, cell)
|
||||
} else {
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
}
|
||||
|
||||
/// AVERAGEIF(criteria_range, criteria, [average_range])
|
||||
/// if average_rage is missing then criteria_range will be used
|
||||
pub(crate) fn fn_averageif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() == 2 {
|
||||
let arguments = vec![args[0].clone(), args[0].clone(), args[1].clone()];
|
||||
self.fn_averageifs(&arguments, cell)
|
||||
} else if args.len() == 3 {
|
||||
let arguments = vec![args[2].clone(), args[0].clone(), args[1].clone()];
|
||||
self.fn_averageifs(&arguments, cell)
|
||||
} else {
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This function shares a lot of code with apply_ifs. Can we merge them?
|
||||
pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let args_count = args.len();
|
||||
if args_count < 2 || !args_count.is_multiple_of(2) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let case_count = args_count / 2;
|
||||
// NB: this is a beautiful example of the borrow checker
|
||||
// The order of these two definitions cannot be swapped.
|
||||
let mut criteria = Vec::new();
|
||||
let mut fn_criteria = Vec::new();
|
||||
let ranges = &mut Vec::new();
|
||||
for case_index in 0..case_count {
|
||||
let criterion = self.evaluate_node_in_context(&args[case_index * 2 + 1], cell);
|
||||
criteria.push(criterion);
|
||||
// NB: We cannot do:
|
||||
// fn_criteria.push(build_criteria(&criterion));
|
||||
// because criterion doesn't live long enough
|
||||
let result = self.evaluate_node_in_context(&args[case_index * 2], cell);
|
||||
if result.is_error() {
|
||||
return result;
|
||||
}
|
||||
if let CalcResult::Range { left, right } = result {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
// TODO test ranges are of the same size as sum_range
|
||||
ranges.push(Range { left, right });
|
||||
} else {
|
||||
return CalcResult::new_error(Error::VALUE, cell, "Expected a range".to_string());
|
||||
}
|
||||
}
|
||||
for criterion in criteria.iter() {
|
||||
fn_criteria.push(build_criteria(criterion));
|
||||
}
|
||||
|
||||
let mut total = 0.0;
|
||||
let first_range = &ranges[0];
|
||||
let left_row = first_range.left.row;
|
||||
let left_column = first_range.left.column;
|
||||
let right_row = first_range.right.row;
|
||||
let right_column = first_range.right.column;
|
||||
|
||||
let dimension = match self.workbook.worksheet(first_range.left.sheet) {
|
||||
Ok(s) => s.dimension(),
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", first_range.left.sheet),
|
||||
)
|
||||
}
|
||||
};
|
||||
let max_row = dimension.max_row;
|
||||
let max_column = dimension.max_column;
|
||||
|
||||
let open_row = left_row == 1 && right_row == LAST_ROW;
|
||||
let open_column = left_column == 1 && right_column == LAST_COLUMN;
|
||||
|
||||
for row in left_row..right_row + 1 {
|
||||
if open_row && row > max_row {
|
||||
// If the row is larger than the max row in the sheet then all cells are empty.
|
||||
// We compute it only once
|
||||
let mut is_true = true;
|
||||
for fn_criterion in fn_criteria.iter() {
|
||||
if !fn_criterion(&CalcResult::EmptyCell) {
|
||||
is_true = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if is_true {
|
||||
total += ((LAST_ROW - max_row) * (right_column - left_column + 1)) as f64;
|
||||
}
|
||||
break;
|
||||
}
|
||||
for column in left_column..right_column + 1 {
|
||||
if open_column && column > max_column {
|
||||
// If the column is larger than the max column in the sheet then all cells are empty.
|
||||
// We compute it only once
|
||||
let mut is_true = true;
|
||||
for fn_criterion in fn_criteria.iter() {
|
||||
if !fn_criterion(&CalcResult::EmptyCell) {
|
||||
is_true = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if is_true {
|
||||
total += (LAST_COLUMN - max_column) as f64;
|
||||
}
|
||||
break;
|
||||
}
|
||||
let mut is_true = true;
|
||||
for case_index in 0..case_count {
|
||||
// We check if value in range n meets criterion n
|
||||
let range = &ranges[case_index];
|
||||
let fn_criterion = &fn_criteria[case_index];
|
||||
let value = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: range.left.sheet,
|
||||
row: range.left.row + row - first_range.left.row,
|
||||
column: range.left.column + column - first_range.left.column,
|
||||
});
|
||||
if !fn_criterion(&value) {
|
||||
is_true = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if is_true {
|
||||
total += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Number(total)
|
||||
}
|
||||
|
||||
pub(crate) fn apply_ifs<F>(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
mut apply: F,
|
||||
) -> Result<(), CalcResult>
|
||||
where
|
||||
F: FnMut(f64),
|
||||
{
|
||||
let args_count = args.len();
|
||||
if args_count < 3 || args_count.is_multiple_of(2) {
|
||||
return Err(CalcResult::new_args_number_error(cell));
|
||||
}
|
||||
let arg_0 = self.evaluate_node_in_context(&args[0], cell);
|
||||
if arg_0.is_error() {
|
||||
return Err(arg_0);
|
||||
}
|
||||
let sum_range = if let CalcResult::Range { left, right } = arg_0 {
|
||||
if left.sheet != right.sheet {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
));
|
||||
}
|
||||
Range { left, right }
|
||||
} else {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Expected a range".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
let case_count = (args_count - 1) / 2;
|
||||
// NB: this is a beautiful example of the borrow checker
|
||||
// The order of these two definitions cannot be swapped.
|
||||
let mut criteria = Vec::new();
|
||||
let mut fn_criteria = Vec::new();
|
||||
let ranges = &mut Vec::new();
|
||||
for case_index in 1..=case_count {
|
||||
let criterion = self.evaluate_node_in_context(&args[case_index * 2], cell);
|
||||
// NB: criterion might be an error. That's ok
|
||||
criteria.push(criterion);
|
||||
// NB: We cannot do:
|
||||
// fn_criteria.push(build_criteria(&criterion));
|
||||
// because criterion doesn't live long enough
|
||||
let result = self.evaluate_node_in_context(&args[case_index * 2 - 1], cell);
|
||||
if result.is_error() {
|
||||
return Err(result);
|
||||
}
|
||||
if let CalcResult::Range { left, right } = result {
|
||||
if left.sheet != right.sheet {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
));
|
||||
}
|
||||
// TODO test ranges are of the same size as sum_range
|
||||
ranges.push(Range { left, right });
|
||||
} else {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Expected a range".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
for criterion in criteria.iter() {
|
||||
fn_criteria.push(build_criteria(criterion));
|
||||
}
|
||||
|
||||
let left_row = sum_range.left.row;
|
||||
let left_column = sum_range.left.column;
|
||||
let mut right_row = sum_range.right.row;
|
||||
let mut right_column = sum_range.right.column;
|
||||
|
||||
if left_row == 1 && right_row == LAST_ROW {
|
||||
right_row = match self.workbook.worksheet(sum_range.left.sheet) {
|
||||
Ok(s) => s.dimension().max_row,
|
||||
Err(_) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", sum_range.left.sheet),
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
if left_column == 1 && right_column == LAST_COLUMN {
|
||||
right_column = match self.workbook.worksheet(sum_range.left.sheet) {
|
||||
Ok(s) => s.dimension().max_column,
|
||||
Err(_) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", sum_range.left.sheet),
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for row in left_row..right_row + 1 {
|
||||
for column in left_column..right_column + 1 {
|
||||
let mut is_true = true;
|
||||
for case_index in 0..case_count {
|
||||
// We check if value in range n meets criterion n
|
||||
let range = &ranges[case_index];
|
||||
let fn_criterion = &fn_criteria[case_index];
|
||||
let value = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: range.left.sheet,
|
||||
row: range.left.row + row - sum_range.left.row,
|
||||
column: range.left.column + column - sum_range.left.column,
|
||||
});
|
||||
if !fn_criterion(&value) {
|
||||
is_true = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if is_true {
|
||||
let v = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: sum_range.left.sheet,
|
||||
row,
|
||||
column,
|
||||
});
|
||||
match v {
|
||||
CalcResult::Number(n) => apply(n),
|
||||
CalcResult::Error { .. } => return Err(v),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_averageifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let mut total = 0.0;
|
||||
let mut count = 0.0;
|
||||
|
||||
let average = |value: f64| {
|
||||
total += value;
|
||||
count += 1.0;
|
||||
};
|
||||
if let Err(e) = self.apply_ifs(args, cell, average) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if count == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "division by 0".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(total / count)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_minifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let mut min = f64::INFINITY;
|
||||
let apply_min = |value: f64| min = value.min(min);
|
||||
if let Err(e) = self.apply_ifs(args, cell, apply_min) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if min.is_infinite() {
|
||||
min = 0.0;
|
||||
}
|
||||
CalcResult::Number(min)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_maxifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let mut max = -f64::INFINITY;
|
||||
let apply_max = |value: f64| max = value.max(max);
|
||||
if let Err(e) = self.apply_ifs(args, cell, apply_max) {
|
||||
return e;
|
||||
}
|
||||
if max.is_infinite() {
|
||||
max = 0.0;
|
||||
}
|
||||
CalcResult::Number(max)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_geomean(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut count = 0.0;
|
||||
let mut product = 1.0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
product *= value;
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
} else {
|
||||
product *= if b { 1.0 } else { 0.0 };
|
||||
count += 1.0;
|
||||
}
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
product *= value;
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::Range { .. } => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
"Unexpected Range".to_string(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::String(s) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
// Do nothing
|
||||
} else if let Ok(t) = s.parse::<f64>() {
|
||||
product *= t;
|
||||
count += 1.0;
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Argument cannot be cast into number".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Ignore everything else
|
||||
}
|
||||
};
|
||||
}
|
||||
if count == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Division by Zero".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(product.powf(1.0 / count))
|
||||
}
|
||||
}
|
||||
213
base/src/functions/statistical/beta.rs
Normal file
213
base/src/functions/statistical/beta.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use statrs::distribution::{Beta, Continuous, ContinuousCDF};
|
||||
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// BETA.DIST(x, alpha, beta, cumulative, [A], [B])
|
||||
pub(crate) fn fn_beta_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let arg_count = args.len();
|
||||
if !(4..=6).contains(&arg_count) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let alpha = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let beta_param = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// cumulative argument: interpret like Excel
|
||||
let cumulative = match self.evaluate_node_in_context(&args[3], cell) {
|
||||
CalcResult::Boolean(b) => b,
|
||||
CalcResult::Number(n) => n != 0.0,
|
||||
CalcResult::String(s) => {
|
||||
let up = s.to_ascii_uppercase();
|
||||
if up == "TRUE" {
|
||||
true
|
||||
} else if up == "FALSE" {
|
||||
false
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "cumulative must be TRUE/FALSE or numeric".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Invalid cumulative argument".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Optional A, B
|
||||
let a = if arg_count >= 5 {
|
||||
match self.get_number_no_bools(&args[4], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let b = if arg_count >= 6 {
|
||||
match self.get_number_no_bools(&args[5], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
}
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
// Excel: alpha <= 0 or beta <= 0 → #NUM!
|
||||
if alpha <= 0.0 || beta_param <= 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"alpha and beta must be > 0 in BETA.DIST".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Excel: if x < A, x > B, or A = B → #NUM!
|
||||
if b == a || x < a || x > b {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"x must be between A and B and A < B in BETA.DIST".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Transform to standard Beta(0,1)
|
||||
let width = b - a;
|
||||
let t = (x - a) / width;
|
||||
|
||||
let dist = match Beta::new(alpha, beta_param) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for Beta distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let result = if cumulative {
|
||||
dist.cdf(t)
|
||||
} else {
|
||||
// general-interval beta pdf: f_X(x) = f_T(t) / (B - A), t=(x-A)/(B-A)
|
||||
dist.pdf(t) / width
|
||||
};
|
||||
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid result for BETA.DIST".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_beta_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let arg_count = args.len();
|
||||
if !(3..=5).contains(&arg_count) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let alpha = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let beta_param = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let a = if arg_count >= 4 {
|
||||
match self.get_number_no_bools(&args[3], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let b = if arg_count >= 5 {
|
||||
match self.get_number_no_bools(&args[4], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
}
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
if alpha <= 0.0 || beta_param <= 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"alpha and beta must be > 0 in BETA.INV".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// probability <= 0 or probability > 1 → #NUM!
|
||||
if p <= 0.0 || p > 1.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"probability must be in (0,1] in BETA.INV".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if b <= a {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"A must be < B in BETA.INV".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let dist = match Beta::new(alpha, beta_param) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for Beta distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let t = dist.inverse_cdf(p);
|
||||
if t.is_nan() || t.is_infinite() {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid result for BETA.INV".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Map back from [0,1] to [A,B]
|
||||
let x = a + t * (b - a);
|
||||
CalcResult::Number(x)
|
||||
}
|
||||
}
|
||||
311
base/src/functions/statistical/binom.rs
Normal file
311
base/src/functions/statistical/binom.rs
Normal file
@@ -0,0 +1,311 @@
|
||||
use statrs::distribution::{Binomial, Discrete, DiscreteCDF};
|
||||
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_binom_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 4 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
// number_s
|
||||
let number_s = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// trials
|
||||
let trials = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// probability_s
|
||||
let p = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// cumulative (logical)
|
||||
let cumulative = match self.get_boolean(&args[3], cell) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// Domain checks
|
||||
if trials < 0.0
|
||||
|| number_s < 0.0
|
||||
|| number_s > trials
|
||||
|| p.is_nan()
|
||||
|| !(0.0..=1.0).contains(&p)
|
||||
{
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for BINOM.DIST".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Limit to u64
|
||||
if trials > u64::MAX as f64 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Number of trials too large".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n = trials as u64;
|
||||
let k = number_s as u64;
|
||||
|
||||
let dist = match Binomial::new(p, n) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for binomial distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let prob = if cumulative { dist.cdf(k) } else { dist.pmf(k) };
|
||||
|
||||
if prob.is_nan() || prob.is_infinite() {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid result for BINOM.DIST".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
CalcResult::Number(prob)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_binom_dist_range(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
if args.len() < 3 || args.len() > 4 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
// trials
|
||||
let trials = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// probability_s
|
||||
let p = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// number_s (lower)
|
||||
let number_s = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// number_s2 (upper, optional)
|
||||
let number_s2 = if args.len() == 4 {
|
||||
match self.get_number_no_bools(&args[3], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
}
|
||||
} else {
|
||||
number_s
|
||||
};
|
||||
|
||||
if trials < 0.0
|
||||
|| number_s < 0.0
|
||||
|| number_s2 < 0.0
|
||||
|| number_s > number_s2
|
||||
|| number_s2 > trials
|
||||
|| p.is_nan()
|
||||
|| !(0.0..=1.0).contains(&p)
|
||||
{
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for BINOM.DIST.RANGE".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if trials > u64::MAX as f64 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Number of trials too large".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n = trials as u64;
|
||||
let lower = number_s as u64;
|
||||
let upper = number_s2 as u64;
|
||||
|
||||
let dist = match Binomial::new(p, n) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for binomial distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let prob = if lower == 0 {
|
||||
dist.cdf(upper)
|
||||
} else {
|
||||
let cdf_upper = dist.cdf(upper);
|
||||
let cdf_below_lower = dist.cdf(lower - 1);
|
||||
cdf_upper - cdf_below_lower
|
||||
};
|
||||
|
||||
if prob.is_nan() || prob.is_infinite() {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid result for BINOM.DIST.RANGE".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
CalcResult::Number(prob)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_binom_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
// trials
|
||||
let trials = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// probability_s
|
||||
let p = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// alpha
|
||||
let alpha = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if trials < 0.0
|
||||
|| trials > u64::MAX as f64
|
||||
|| p.is_nan()
|
||||
|| !(0.0..=1.0).contains(&p)
|
||||
|| alpha.is_nan()
|
||||
|| !(0.0..=1.0).contains(&alpha)
|
||||
{
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for BINOM.INV".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n = trials as u64;
|
||||
|
||||
let dist = match Binomial::new(p, n) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for binomial distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// DiscreteCDF::inverse_cdf returns u64 for binomial
|
||||
let k = statrs::distribution::DiscreteCDF::inverse_cdf(&dist, alpha);
|
||||
|
||||
CalcResult::Number(k as f64)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_negbinom_dist(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
use statrs::distribution::{Discrete, DiscreteCDF, NegativeBinomial};
|
||||
|
||||
if args.len() != 4 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let number_f = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
let number_s = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
let probability_s = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let cumulative = match self.get_boolean(&args[3], cell) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if number_f < 0.0 || number_s < 1.0 || !(0.0..=1.0).contains(&probability_s) {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for NEGBINOM.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// Guard against absurdly large failures that won't fit in u64
|
||||
if number_f > (u64::MAX as f64) {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for NEGBINOM.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let dist = match NegativeBinomial::new(number_s, probability_s) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for NEGBINOM.DIST".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let f_u = number_f as u64;
|
||||
let result = if cumulative {
|
||||
dist.cdf(f_u)
|
||||
} else {
|
||||
dist.pmf(f_u)
|
||||
};
|
||||
|
||||
if !result.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for NEGBINOM.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
}
|
||||
397
base/src/functions/statistical/chisq.rs
Normal file
397
base/src/functions/statistical/chisq.rs
Normal file
@@ -0,0 +1,397 @@
|
||||
use statrs::distribution::{ChiSquared, Continuous, ContinuousCDF};
|
||||
|
||||
use crate::expressions::parser::ArrayNode;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// CHISQ.DIST(x, deg_freedom, cumulative)
|
||||
pub(crate) fn fn_chisq_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let df = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let cumulative = match self.get_boolean(&args[2], cell) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if x < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"x must be >= 0 in CHISQ.DIST".to_string(),
|
||||
);
|
||||
}
|
||||
if df < 1.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"degrees of freedom must be >= 1 in CHISQ.DIST".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let dist = match ChiSquared::new(df) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for Chi-squared distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
|
||||
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid result for CHISQ.DIST".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
// CHISQ.DIST.RT(x, deg_freedom)
|
||||
pub(crate) fn fn_chisq_dist_rt(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let df_raw = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let df = df_raw.trunc();
|
||||
|
||||
if x < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"x must be >= 0 in CHISQ.DIST.RT".to_string(),
|
||||
);
|
||||
}
|
||||
if df < 1.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"degrees of freedom must be >= 1 in CHISQ.DIST.RT".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let dist = match ChiSquared::new(df) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for Chi-squared distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// Right-tail probability: P(X > x).
|
||||
// Use sf(x) directly for better numerical properties than 1 - cdf(x).
|
||||
let result = dist.sf(x);
|
||||
|
||||
if result.is_nan() || result.is_infinite() || result < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid result for CHISQ.DIST.RT".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
// CHISQ.INV(probability, deg_freedom)
|
||||
pub(crate) fn fn_chisq_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let df = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// if probability < 0 or > 1 → #NUM!
|
||||
if !(0.0..=1.0).contains(&p) {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"probability must be in [0,1] in CHISQ.INV".to_string(),
|
||||
);
|
||||
}
|
||||
if df < 1.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"degrees of freedom must be >= 1 in CHISQ.INV".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let dist = match ChiSquared::new(df) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for Chi-squared distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let x = dist.inverse_cdf(p);
|
||||
|
||||
if x.is_nan() || x.is_infinite() || x < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid result for CHISQ.INV".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
CalcResult::Number(x)
|
||||
}
|
||||
|
||||
// CHISQ.INV.RT(probability, deg_freedom)
|
||||
pub(crate) fn fn_chisq_inv_rt(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let df_raw = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let df = df_raw.trunc();
|
||||
|
||||
// if probability < 0 or > 1 → #NUM!
|
||||
if !(0.0..=1.0).contains(&p) {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"probability must be in [0,1] in CHISQ.INV.RT".to_string(),
|
||||
);
|
||||
}
|
||||
if df < 1.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"degrees of freedom must be >= 1 in CHISQ.INV.RT".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let dist = match ChiSquared::new(df) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for Chi-squared distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// Right-tail inverse: p = P(X > x) = SF(x) = 1 - CDF(x)
|
||||
// So x = inverse_cdf(1 - p).
|
||||
let x = dist.inverse_cdf(1.0 - p);
|
||||
|
||||
if x.is_nan() || x.is_infinite() || x < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid result for CHISQ.INV.RT".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
CalcResult::Number(x)
|
||||
}
|
||||
|
||||
pub(crate) fn values_from_range(
|
||||
&mut self,
|
||||
left: CellReferenceIndex,
|
||||
right: CellReferenceIndex,
|
||||
) -> Result<Vec<Option<f64>>, CalcResult> {
|
||||
let mut values = Vec::new();
|
||||
for row_offset in 0..=(right.row - left.row) {
|
||||
for col_offset in 0..=(right.column - left.column) {
|
||||
let cell_ref = CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row: left.row + row_offset,
|
||||
column: left.column + col_offset,
|
||||
};
|
||||
let cell_value = self.evaluate_cell(cell_ref);
|
||||
match cell_value {
|
||||
CalcResult::Number(v) => {
|
||||
values.push(Some(v));
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return Err(error),
|
||||
_ => {
|
||||
values.push(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
pub(crate) fn values_from_array(
|
||||
&mut self,
|
||||
array: Vec<Vec<ArrayNode>>,
|
||||
) -> Result<Vec<Option<f64>>, Error> {
|
||||
let mut values = Vec::new();
|
||||
for row in array {
|
||||
for item in row {
|
||||
match item {
|
||||
ArrayNode::Number(f) => {
|
||||
values.push(Some(f));
|
||||
}
|
||||
ArrayNode::Error(error) => {
|
||||
return Err(error);
|
||||
}
|
||||
_ => {
|
||||
values.push(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
// CHISQ.TEST(actual_range, expected_range)
|
||||
pub(crate) fn fn_chisq_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (width, height, values_left, values_right) = match self.fn_get_two_matrices(args, cell)
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(r) => return r,
|
||||
};
|
||||
|
||||
let mut values = Vec::with_capacity(values_left.len());
|
||||
|
||||
// Now we have:
|
||||
// - values: flattened (observed, expected)
|
||||
// - width, height: shape
|
||||
for i in 0..values_left.len() {
|
||||
match (values_left[i], values_right[i]) {
|
||||
(Some(v1), Some(v2)) => {
|
||||
values.push((v1, v2));
|
||||
}
|
||||
_ => {
|
||||
values.push((1.0, 1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
if width == 0 || height == 0 || values.len() < 2 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"CHISQ.TEST requires at least two data points".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut chi2 = 0.0;
|
||||
for (obs, exp) in &values {
|
||||
if *obs < 0.0 || *exp < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Negative value in CHISQ.TEST data".to_string(),
|
||||
);
|
||||
}
|
||||
if *exp == 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Zero expected value in CHISQ.TEST".to_string(),
|
||||
);
|
||||
}
|
||||
let diff = obs - exp;
|
||||
chi2 += (diff * diff) / exp;
|
||||
}
|
||||
|
||||
if chi2 < 0.0 && chi2 > -1e-12 {
|
||||
chi2 = 0.0;
|
||||
}
|
||||
|
||||
let total = width * height;
|
||||
if total <= 1 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"CHISQ.TEST degrees of freedom is zero".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let df = if width > 1 && height > 1 {
|
||||
(width - 1) * (height - 1)
|
||||
} else {
|
||||
total - 1
|
||||
};
|
||||
|
||||
let dist = match ChiSquared::new(df as f64) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid degrees of freedom in CHISQ.TEST".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let mut p = 1.0 - dist.cdf(chi2);
|
||||
|
||||
// clamp tiny fp noise
|
||||
if p < 0.0 && p > -1e-15 {
|
||||
p = 0.0;
|
||||
}
|
||||
if p > 1.0 && p < 1.0 + 1e-15 {
|
||||
p = 1.0;
|
||||
}
|
||||
|
||||
CalcResult::Number(p)
|
||||
}
|
||||
}
|
||||
227
base/src/functions/statistical/correl.rs
Normal file
227
base/src/functions/statistical/correl.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// CORREL(array1, array2) - Returns the correlation coefficient of two data sets
|
||||
pub(crate) fn fn_correl(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_, _, values_left, values_right) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let mut n = 0.0;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_x2 = 0.0;
|
||||
let mut sum_y2 = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
if let (Some(x), Some(y)) = (x_opt, y_opt) {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_y2 += y * y;
|
||||
sum_xy += x * y;
|
||||
}
|
||||
}
|
||||
|
||||
// Need at least 2 valid pairs
|
||||
if n < 2.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"CORREL requires at least two numeric data points in each range".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let denom_x = n * sum_x2 - sum_x * sum_x;
|
||||
let denom_y = n * sum_y2 - sum_y * sum_y;
|
||||
let denom = (denom_x * denom_y).sqrt();
|
||||
|
||||
if denom == 0.0 || !denom.is_finite() {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Division by zero in CORREL".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let r = num / denom;
|
||||
CalcResult::Number(r)
|
||||
}
|
||||
|
||||
// SLOPE(known_y's, known_x's) - Returns the slope of the linear regression line
|
||||
pub(crate) fn fn_slope(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let mut n = 0.0;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_x2 = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
|
||||
let len = values_y.len().min(values_x.len());
|
||||
for i in 0..len {
|
||||
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_xy += x * y;
|
||||
}
|
||||
}
|
||||
|
||||
if n < 2.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"SLOPE requires at least two numeric data points".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let denom = n * sum_x2 - sum_x * sum_x;
|
||||
if denom == 0.0 || !denom.is_finite() {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Division by zero in SLOPE".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let slope = num / denom;
|
||||
|
||||
CalcResult::Number(slope)
|
||||
}
|
||||
|
||||
// INTERCEPT(known_y's, known_x's) - Returns the y-intercept of the linear regression line
|
||||
pub(crate) fn fn_intercept(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let mut n = 0.0;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_x2 = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
|
||||
let len = values_y.len().min(values_x.len());
|
||||
for i in 0..len {
|
||||
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_xy += x * y;
|
||||
}
|
||||
}
|
||||
|
||||
if n < 2.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"INTERCEPT requires at least two numeric data points".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let denom = n * sum_x2 - sum_x * sum_x;
|
||||
if denom == 0.0 || !denom.is_finite() {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Division by zero in INTERCEPT".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let slope = num / denom;
|
||||
let intercept = (sum_y - slope * sum_x) / n;
|
||||
|
||||
CalcResult::Number(intercept)
|
||||
}
|
||||
|
||||
// STEYX(known_y's, known_x's) - Returns the standard error of the predicted y-values
|
||||
pub(crate) fn fn_steyx(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let mut n = 0.0;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_x2 = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
|
||||
// We need the actual pairs again later for residuals
|
||||
let mut pairs: Vec<(f64, f64)> = Vec::new();
|
||||
|
||||
let len = values_y.len().min(values_x.len());
|
||||
for i in 0..len {
|
||||
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_xy += x * y;
|
||||
pairs.push((x, y));
|
||||
}
|
||||
}
|
||||
|
||||
// Need at least 3 points for STEYX (n - 2 in denominator)
|
||||
if n < 3.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"STEYX requires at least three numeric data points".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let denom = n * sum_x2 - sum_x * sum_x;
|
||||
if denom == 0.0 || !denom.is_finite() {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Division by zero in STEYX".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let slope = num / denom;
|
||||
let intercept = (sum_y - slope * sum_x) / n;
|
||||
|
||||
// Sum of squared residuals: Σ (y - ŷ)^2, ŷ = intercept + slope * x
|
||||
let mut sse = 0.0;
|
||||
for (x, y) in pairs {
|
||||
let y_hat = intercept + slope * x;
|
||||
let diff = y - y_hat;
|
||||
sse += diff * diff;
|
||||
}
|
||||
|
||||
let dof = n - 2.0;
|
||||
if dof <= 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"STEYX has non-positive degrees of freedom".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let sey = (sse / dof).sqrt();
|
||||
if !sey.is_finite() {
|
||||
return CalcResult::new_error(Error::DIV, cell, "Numerical error in STEYX".to_string());
|
||||
}
|
||||
|
||||
CalcResult::Number(sey)
|
||||
}
|
||||
}
|
||||
1071
base/src/functions/statistical/count_and_average.rs
Normal file
1071
base/src/functions/statistical/count_and_average.rs
Normal file
File diff suppressed because it is too large
Load Diff
264
base/src/functions/statistical/covariance.rs
Normal file
264
base/src/functions/statistical/covariance.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_covariance_p(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let values1_opts = match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||
Ok(v) => v,
|
||||
Err(error) => return error,
|
||||
},
|
||||
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in first array: {:?}", error),
|
||||
);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"First argument must be a range or array".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let values2_opts = match self.evaluate_node_in_context(&args[1], cell) {
|
||||
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||
Ok(v) => v,
|
||||
Err(error) => return error,
|
||||
},
|
||||
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Second argument must be a range or array".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Same number of cells
|
||||
if values1_opts.len() != values2_opts.len() {
|
||||
return CalcResult::new_error(
|
||||
Error::NA,
|
||||
cell,
|
||||
"COVARIANCE.P requires arrays of the same size".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Count numeric data points in each array (ignoring text/booleans/empty)
|
||||
let count1 = values1_opts.iter().filter(|v| v.is_some()).count();
|
||||
let count2 = values2_opts.iter().filter(|v| v.is_some()).count();
|
||||
|
||||
if count1 == 0 || count2 == 0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"COVARIANCE.P requires at least one numeric value in each array".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if count1 != count2 {
|
||||
return CalcResult::new_error(
|
||||
Error::NA,
|
||||
cell,
|
||||
"COVARIANCE.P arrays must have the same number of numeric data points".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Build paired numeric vectors, position by position
|
||||
let mut xs: Vec<f64> = Vec::with_capacity(count1);
|
||||
let mut ys: Vec<f64> = Vec::with_capacity(count2);
|
||||
|
||||
for (v1_opt, v2_opt) in values1_opts.into_iter().zip(values2_opts.into_iter()) {
|
||||
if let (Some(x), Some(y)) = (v1_opt, v2_opt) {
|
||||
xs.push(x);
|
||||
ys.push(y);
|
||||
}
|
||||
}
|
||||
|
||||
let n = xs.len();
|
||||
if n == 0 {
|
||||
// Should be impossible given the checks above, but guard anyway
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"COVARIANCE.P has no paired numeric data points".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n_f = n as f64;
|
||||
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
for i in 0..n {
|
||||
sum_x += xs[i];
|
||||
sum_y += ys[i];
|
||||
}
|
||||
let mean_x = sum_x / n_f;
|
||||
let mean_y = sum_y / n_f;
|
||||
|
||||
let mut sum_prod = 0.0;
|
||||
for i in 0..n {
|
||||
let dx = xs[i] - mean_x;
|
||||
let dy = ys[i] - mean_y;
|
||||
sum_prod += dx * dy;
|
||||
}
|
||||
|
||||
let cov = sum_prod / n_f;
|
||||
CalcResult::Number(cov)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_covariance_s(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let values1_opts = match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||
Ok(v) => v,
|
||||
Err(error) => return error,
|
||||
},
|
||||
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in first array: {:?}", error),
|
||||
);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"First argument must be a range or array".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let values2_opts = match self.evaluate_node_in_context(&args[1], cell) {
|
||||
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||
Ok(v) => v,
|
||||
Err(error) => return error,
|
||||
},
|
||||
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Second argument must be a range or array".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Same number of cells
|
||||
if values1_opts.len() != values2_opts.len() {
|
||||
return CalcResult::new_error(
|
||||
Error::NA,
|
||||
cell,
|
||||
"COVARIANCE.S requires arrays of the same size".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Count numeric data points in each array (ignoring text/booleans/empty)
|
||||
let count1 = values1_opts.iter().filter(|v| v.is_some()).count();
|
||||
let count2 = values2_opts.iter().filter(|v| v.is_some()).count();
|
||||
|
||||
if count1 == 0 || count2 == 0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"COVARIANCE.S requires numeric values in each array".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if count1 != count2 {
|
||||
return CalcResult::new_error(
|
||||
Error::NA,
|
||||
cell,
|
||||
"COVARIANCE.S arrays must have the same number of numeric data points".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Build paired numeric vectors
|
||||
let mut xs: Vec<f64> = Vec::with_capacity(count1);
|
||||
let mut ys: Vec<f64> = Vec::with_capacity(count2);
|
||||
|
||||
for (v1_opt, v2_opt) in values1_opts.into_iter().zip(values2_opts.into_iter()) {
|
||||
if let (Some(x), Some(y)) = (v1_opt, v2_opt) {
|
||||
xs.push(x);
|
||||
ys.push(y);
|
||||
}
|
||||
}
|
||||
|
||||
let n = xs.len();
|
||||
if n < 2 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"COVARIANCE.S requires at least two paired data points".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n_f = n as f64;
|
||||
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
for i in 0..n {
|
||||
sum_x += xs[i];
|
||||
sum_y += ys[i];
|
||||
}
|
||||
let mean_x = sum_x / n_f;
|
||||
let mean_y = sum_y / n_f;
|
||||
|
||||
let mut sum_prod = 0.0;
|
||||
for i in 0..n {
|
||||
let dx = xs[i] - mean_x;
|
||||
let dy = ys[i] - mean_y;
|
||||
sum_prod += dx * dy;
|
||||
}
|
||||
|
||||
let cov = sum_prod / (n_f - 1.0);
|
||||
|
||||
CalcResult::Number(cov)
|
||||
}
|
||||
}
|
||||
135
base/src/functions/statistical/devsq.rs
Normal file
135
base/src/functions/statistical/devsq.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::parser::ArrayNode;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// DEVSQ(number1, [number2], ...)
|
||||
pub(crate) fn fn_devsq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut sum = 0.0;
|
||||
let mut sumsq = 0.0;
|
||||
let mut count: u64 = 0;
|
||||
|
||||
// tiny helper so we don't repeat ourselves
|
||||
#[inline]
|
||||
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||
*sum += value;
|
||||
*sumsq += value * value;
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
let row1 = left.row;
|
||||
let mut row2 = right.row;
|
||||
let column1 = left.column;
|
||||
let mut column2 = right.column;
|
||||
|
||||
if row1 == 1 && row2 == LAST_ROW {
|
||||
row2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_row,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
column2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_column,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for row in row1..row2 + 1 {
|
||||
for column in column1..(column2 + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// We ignore booleans and strings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Array(array) => {
|
||||
for row in array {
|
||||
for value in row {
|
||||
match value {
|
||||
ArrayNode::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
ArrayNode::Error(error) => {
|
||||
return CalcResult::Error {
|
||||
error,
|
||||
origin: cell,
|
||||
message: "Error in array".to_string(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// We ignore booleans and strings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// We ignore booleans and strings
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
// No numeric data at all
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"DEVSQ with no numeric data".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n = count as f64;
|
||||
let mut result = sumsq - (sum * sum) / n;
|
||||
|
||||
// Numerical noise can make result slightly negative when it should be 0
|
||||
if result < 0.0 && result > -1e-12 {
|
||||
result = 0.0;
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
}
|
||||
54
base/src/functions/statistical/exponential.rs
Normal file
54
base/src/functions/statistical/exponential.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_expon_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
// EXPON.DIST(x, lambda, cumulative)
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let lambda = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let cumulative = match self.get_boolean(&args[2], cell) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if x < 0.0 || lambda <= 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for EXPON.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let result = if cumulative {
|
||||
// CDF
|
||||
1.0 - (-lambda * x).exp()
|
||||
} else {
|
||||
// PDF
|
||||
lambda * (-lambda * x).exp()
|
||||
};
|
||||
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for EXPON.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
}
|
||||
418
base/src/functions/statistical/fisher.rs
Normal file
418
base/src/functions/statistical/fisher.rs
Normal file
@@ -0,0 +1,418 @@
|
||||
use statrs::distribution::{Continuous, ContinuousCDF, FisherSnedecor};
|
||||
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::functions::statistical::t_dist::sample_var;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// FISHER(x) = 0.5 * ln((1 + x) / (1 - x))
|
||||
pub(crate) fn fn_fisher(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if x <= -1.0 || x >= 1.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "x must be between -1 and 1 (exclusive) in FISHER".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let ratio = (1.0 + x) / (1.0 - x);
|
||||
let result = 0.5 * ratio.ln();
|
||||
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for FISHER".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
// FISHERINV(y) = (e^(2y) - 1) / (e^(2y) + 1) = tanh(y)
|
||||
pub(crate) fn fn_fisher_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let y = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// Use tanh directly to avoid overflow from exp(2y)
|
||||
let result = y.tanh();
|
||||
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for FISHERINV".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
// F.DIST(x, deg_freedom1, deg_freedom2, cumulative)
|
||||
pub(crate) fn fn_f_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 4 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let df1 = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
let df2 = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let cumulative = match self.get_boolean(&args[3], cell) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// Excel domain checks
|
||||
if x < 0.0 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "x must be >= 0 in F.DIST".to_string());
|
||||
}
|
||||
if df1 < 1.0 || df2 < 1.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"degrees of freedom must be >= 1 in F.DIST".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let dist = match FisherSnedecor::new(df1, df2) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for F distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
|
||||
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid result for F.DIST".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_f_dist_rt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
// F.DIST.RT(x, deg_freedom1, deg_freedom2)
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let df1 = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
let df2 = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if x < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"x must be >= 0 in F.DIST.RT".to_string(),
|
||||
);
|
||||
}
|
||||
if df1 < 1.0 || df2 < 1.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"degrees of freedom must be >= 1 in F.DIST.RT".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let dist = match FisherSnedecor::new(df1, df2) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for F distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// Right-tail probability: P(F > x) = 1 - CDF(x)
|
||||
let result = 1.0 - dist.cdf(x);
|
||||
|
||||
if result.is_nan() || result.is_infinite() || result < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid result for F.DIST.RT".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
// F.INV(probability, deg_freedom1, deg_freedom2)
|
||||
pub(crate) fn fn_f_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let df1 = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
let df2 = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// probability < 0 or > 1 → #NUM!
|
||||
if !(0.0..=1.0).contains(&p) {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"probability must be in [0,1] in F.INV".to_string(),
|
||||
);
|
||||
}
|
||||
if df1 < 1.0 || df2 < 1.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"degrees of freedom must be >= 1 in F.INV".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let dist = match FisherSnedecor::new(df1, df2) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for F distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let x = dist.inverse_cdf(p);
|
||||
if x.is_nan() || x.is_infinite() || x < 0.0 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Invalid result for F.INV".to_string());
|
||||
}
|
||||
|
||||
CalcResult::Number(x)
|
||||
}
|
||||
|
||||
// F.INV.RT(probability, deg_freedom1, deg_freedom2)
|
||||
pub(crate) fn fn_f_inv_rt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let df1 = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
let df2 = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if p <= 0.0 || p > 1.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"probability must be in (0,1] in F.INV.RT".to_string(),
|
||||
);
|
||||
}
|
||||
if df1 < 1.0 || df2 < 1.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"degrees of freedom must be >= 1 in F.INV.RT".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let dist = match FisherSnedecor::new(df1, df2) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for F distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// p is right-tail: p = P(F > x) = 1 - CDF(x)
|
||||
let x = dist.inverse_cdf(1.0 - p);
|
||||
if x.is_nan() || x.is_infinite() || x < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid result for F.INV.RT".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
CalcResult::Number(x)
|
||||
}
|
||||
|
||||
// F.TEST(array1, array2)
|
||||
pub(crate) fn fn_f_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let values1_opts = match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||
Ok(v) => v,
|
||||
Err(error) => return error,
|
||||
},
|
||||
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in first array: {:?}", error),
|
||||
);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"First argument must be a range or array".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Get second sample as Vec<Option<f64>>
|
||||
let values2_opts = match self.evaluate_node_in_context(&args[1], cell) {
|
||||
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||
Ok(v) => v,
|
||||
Err(error) => return error,
|
||||
},
|
||||
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Second argument must be a range or array".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let values1: Vec<f64> = values1_opts.into_iter().flatten().collect();
|
||||
let values2: Vec<f64> = values2_opts.into_iter().flatten().collect();
|
||||
|
||||
let n1 = values1.len();
|
||||
let n2 = values2.len();
|
||||
|
||||
// If fewer than 2 numeric values in either sample -> #DIV/0!
|
||||
if n1 < 2 || n2 < 2 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"F.TEST requires at least two numeric values in each sample".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let v1 = sample_var(&values1);
|
||||
let v2 = sample_var(&values2);
|
||||
|
||||
if v1 <= 0.0 || v2 <= 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Variance of one sample is zero in F.TEST".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// F ratio: larger variance / smaller variance
|
||||
let mut f = v1 / v2;
|
||||
let mut df1 = (n1 - 1) as f64;
|
||||
let mut df2 = (n2 - 1) as f64;
|
||||
|
||||
if f < 1.0 {
|
||||
f = 1.0 / f;
|
||||
std::mem::swap(&mut df1, &mut df2);
|
||||
}
|
||||
|
||||
let dist = match FisherSnedecor::new(df1, df2) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for F distribution in F.TEST".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// One-tailed right-tail probability
|
||||
let tail = 1.0 - dist.cdf(f);
|
||||
// F.TEST is two-tailed: p = 2 * tail (with F >= 1)
|
||||
let mut p = 2.0 * tail;
|
||||
|
||||
// Clamp tiny FP noise
|
||||
if p < 0.0 && p > -1e-15 {
|
||||
p = 0.0;
|
||||
}
|
||||
if p > 1.0 && p < 1.0 + 1e-15 {
|
||||
p = 1.0;
|
||||
}
|
||||
|
||||
CalcResult::Number(p)
|
||||
}
|
||||
}
|
||||
194
base/src/functions/statistical/gamma.rs
Normal file
194
base/src/functions/statistical/gamma.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use statrs::distribution::{Continuous, ContinuousCDF, Gamma};
|
||||
use statrs::function::gamma::{gamma, ln_gamma};
|
||||
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_gamma(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if x < 0.0 && x.floor() == x {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for Gamma function".to_string(),
|
||||
};
|
||||
}
|
||||
let result = gamma(x);
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for Gamma function".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_gamma_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
// GAMMA.DIST(x, alpha, beta, cumulative)
|
||||
if args.len() != 4 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let alpha = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let beta_scale = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let cumulative = match self.get_boolean(&args[3], cell) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if x < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"x must be >= 0 in GAMMA.DIST".to_string(),
|
||||
);
|
||||
}
|
||||
if alpha <= 0.0 || beta_scale <= 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"alpha and beta must be > 0 in GAMMA.DIST".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let rate = 1.0 / beta_scale;
|
||||
|
||||
let dist = match Gamma::new(alpha, rate) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for Gamma distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
|
||||
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid result for GAMMA.DIST".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_gamma_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
// GAMMA.INV(probability, alpha, beta)
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let alpha = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let beta_scale = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if !(0.0..=1.0).contains(&p) {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"probability must be in [0,1] in GAMMA.INV".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if alpha <= 0.0 || beta_scale <= 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"alpha and beta must be > 0 in GAMMA.INV".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let rate = 1.0 / beta_scale;
|
||||
|
||||
let dist = match Gamma::new(alpha, rate) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for Gamma distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let x = dist.inverse_cdf(p);
|
||||
if x.is_nan() || x.is_infinite() || x < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid result for GAMMA.INV".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
CalcResult::Number(x)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_gamma_ln(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if x < 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for Gamma function".to_string(),
|
||||
};
|
||||
}
|
||||
let result = ln_gamma(x);
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for Gamma Ln function".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_gamma_ln_precise(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
self.fn_gamma_ln(args, cell)
|
||||
}
|
||||
}
|
||||
39
base/src/functions/statistical/gauss.rs
Normal file
39
base/src/functions/statistical/gauss.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use statrs::distribution::{ContinuousCDF, Normal};
|
||||
|
||||
use crate::expressions::token::Error;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{calc_result::CalcResult, expressions::parser::Node, model::Model};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_gauss(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let z = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let dist = match Normal::new(0.0, 1.0) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Failed to construct standard normal distribution".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let result = dist.cdf(z) - 0.5;
|
||||
|
||||
if !result.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for GAUSS".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
}
|
||||
87
base/src/functions/statistical/geomean.rs
Normal file
87
base/src/functions/statistical/geomean.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_geomean(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut count = 0.0;
|
||||
let mut product = 1.0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
product *= value;
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
} else {
|
||||
product *= if b { 1.0 } else { 0.0 };
|
||||
count += 1.0;
|
||||
}
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
product *= value;
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::Range { .. } => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
"Unexpected Range".to_string(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::String(s) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
// Do nothing
|
||||
} else if let Ok(t) = s.parse::<f64>() {
|
||||
product *= t;
|
||||
count += 1.0;
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Argument cannot be cast into number".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Ignore everything else
|
||||
}
|
||||
};
|
||||
}
|
||||
if count == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Division by Zero".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(product.powf(1.0 / count))
|
||||
}
|
||||
}
|
||||
108
base/src/functions/statistical/hypegeom.rs
Normal file
108
base/src/functions/statistical/hypegeom.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use statrs::distribution::{Discrete, DiscreteCDF, Hypergeometric};
|
||||
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// =HYPGEOM.DIST(sample_s, number_sample, population_s, number_pop, cumulative)
|
||||
pub(crate) fn fn_hyp_geom_dist(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
if args.len() != 5 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
// sample_s (number of successes in the sample)
|
||||
let sample_s = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// number_sample (sample size)
|
||||
let number_sample = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// population_s (number of successes in the population)
|
||||
let population_s = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// number_pop (population size)
|
||||
let number_pop = match self.get_number_no_bools(&args[3], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let cumulative = match self.get_boolean(&args[4], cell) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if sample_s < 0.0 || sample_s > f64::min(number_sample, population_s) {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for HYPGEOM.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
if sample_s < f64::max(0.0, number_sample + population_s - number_pop) {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for HYPGEOM.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
if number_sample <= 0.0 || number_sample > number_pop {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for HYPGEOM.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
if population_s <= 0.0 || population_s > number_pop {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for HYPGEOM.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let n_pop = number_pop as u64;
|
||||
let k_pop = population_s as u64;
|
||||
let n_sample = number_sample as u64;
|
||||
let k = sample_s as u64;
|
||||
|
||||
let dist = match Hypergeometric::new(n_pop, k_pop, n_sample) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for hypergeometric distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let prob = if cumulative { dist.cdf(k) } else { dist.pmf(k) };
|
||||
|
||||
if !prob.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for HYPGEOM.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(prob)
|
||||
}
|
||||
}
|
||||
337
base/src/functions/statistical/if_ifs.rs
Normal file
337
base/src/functions/statistical/if_ifs.rs
Normal file
@@ -0,0 +1,337 @@
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::functions::util::build_criteria;
|
||||
use crate::{
|
||||
calc_result::{CalcResult, Range},
|
||||
expressions::parser::Node,
|
||||
expressions::token::Error,
|
||||
model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_countif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() == 2 {
|
||||
let arguments = vec![args[0].clone(), args[1].clone()];
|
||||
self.fn_countifs(&arguments, cell)
|
||||
} else {
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
}
|
||||
|
||||
/// AVERAGEIF(criteria_range, criteria, [average_range])
|
||||
/// if average_rage is missing then criteria_range will be used
|
||||
pub(crate) fn fn_averageif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() == 2 {
|
||||
let arguments = vec![args[0].clone(), args[0].clone(), args[1].clone()];
|
||||
self.fn_averageifs(&arguments, cell)
|
||||
} else if args.len() == 3 {
|
||||
let arguments = vec![args[2].clone(), args[0].clone(), args[1].clone()];
|
||||
self.fn_averageifs(&arguments, cell)
|
||||
} else {
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This function shares a lot of code with apply_ifs. Can we merge them?
|
||||
pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let args_count = args.len();
|
||||
if args_count < 2 || !args_count.is_multiple_of(2) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let case_count = args_count / 2;
|
||||
// NB: this is a beautiful example of the borrow checker
|
||||
// The order of these two definitions cannot be swapped.
|
||||
let mut criteria = Vec::new();
|
||||
let mut fn_criteria = Vec::new();
|
||||
let ranges = &mut Vec::new();
|
||||
for case_index in 0..case_count {
|
||||
let criterion = self.evaluate_node_in_context(&args[case_index * 2 + 1], cell);
|
||||
criteria.push(criterion);
|
||||
// NB: We cannot do:
|
||||
// fn_criteria.push(build_criteria(&criterion));
|
||||
// because criterion doesn't live long enough
|
||||
let result = self.evaluate_node_in_context(&args[case_index * 2], cell);
|
||||
if result.is_error() {
|
||||
return result;
|
||||
}
|
||||
if let CalcResult::Range { left, right } = result {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
// TODO test ranges are of the same size as sum_range
|
||||
ranges.push(Range { left, right });
|
||||
} else {
|
||||
return CalcResult::new_error(Error::VALUE, cell, "Expected a range".to_string());
|
||||
}
|
||||
}
|
||||
for criterion in criteria.iter() {
|
||||
fn_criteria.push(build_criteria(criterion));
|
||||
}
|
||||
|
||||
let mut total = 0.0;
|
||||
let first_range = &ranges[0];
|
||||
let left_row = first_range.left.row;
|
||||
let left_column = first_range.left.column;
|
||||
let right_row = first_range.right.row;
|
||||
let right_column = first_range.right.column;
|
||||
|
||||
let dimension = match self.workbook.worksheet(first_range.left.sheet) {
|
||||
Ok(s) => s.dimension(),
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", first_range.left.sheet),
|
||||
)
|
||||
}
|
||||
};
|
||||
let max_row = dimension.max_row;
|
||||
let max_column = dimension.max_column;
|
||||
|
||||
let open_row = left_row == 1 && right_row == LAST_ROW;
|
||||
let open_column = left_column == 1 && right_column == LAST_COLUMN;
|
||||
|
||||
for row in left_row..right_row + 1 {
|
||||
if open_row && row > max_row {
|
||||
// If the row is larger than the max row in the sheet then all cells are empty.
|
||||
// We compute it only once
|
||||
let mut is_true = true;
|
||||
for fn_criterion in fn_criteria.iter() {
|
||||
if !fn_criterion(&CalcResult::EmptyCell) {
|
||||
is_true = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if is_true {
|
||||
total += ((LAST_ROW - max_row) * (right_column - left_column + 1)) as f64;
|
||||
}
|
||||
break;
|
||||
}
|
||||
for column in left_column..right_column + 1 {
|
||||
if open_column && column > max_column {
|
||||
// If the column is larger than the max column in the sheet then all cells are empty.
|
||||
// We compute it only once
|
||||
let mut is_true = true;
|
||||
for fn_criterion in fn_criteria.iter() {
|
||||
if !fn_criterion(&CalcResult::EmptyCell) {
|
||||
is_true = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if is_true {
|
||||
total += (LAST_COLUMN - max_column) as f64;
|
||||
}
|
||||
break;
|
||||
}
|
||||
let mut is_true = true;
|
||||
for case_index in 0..case_count {
|
||||
// We check if value in range n meets criterion n
|
||||
let range = &ranges[case_index];
|
||||
let fn_criterion = &fn_criteria[case_index];
|
||||
let value = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: range.left.sheet,
|
||||
row: range.left.row + row - first_range.left.row,
|
||||
column: range.left.column + column - first_range.left.column,
|
||||
});
|
||||
if !fn_criterion(&value) {
|
||||
is_true = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if is_true {
|
||||
total += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Number(total)
|
||||
}
|
||||
|
||||
pub(crate) fn apply_ifs<F>(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
mut apply: F,
|
||||
) -> Result<(), CalcResult>
|
||||
where
|
||||
F: FnMut(f64),
|
||||
{
|
||||
let args_count = args.len();
|
||||
if args_count < 3 || args_count.is_multiple_of(2) {
|
||||
return Err(CalcResult::new_args_number_error(cell));
|
||||
}
|
||||
let arg_0 = self.evaluate_node_in_context(&args[0], cell);
|
||||
if arg_0.is_error() {
|
||||
return Err(arg_0);
|
||||
}
|
||||
let sum_range = if let CalcResult::Range { left, right } = arg_0 {
|
||||
if left.sheet != right.sheet {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
));
|
||||
}
|
||||
Range { left, right }
|
||||
} else {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Expected a range".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
let case_count = (args_count - 1) / 2;
|
||||
// NB: this is a beautiful example of the borrow checker
|
||||
// The order of these two definitions cannot be swapped.
|
||||
let mut criteria = Vec::new();
|
||||
let mut fn_criteria = Vec::new();
|
||||
let ranges = &mut Vec::new();
|
||||
for case_index in 1..=case_count {
|
||||
let criterion = self.evaluate_node_in_context(&args[case_index * 2], cell);
|
||||
// NB: criterion might be an error. That's ok
|
||||
criteria.push(criterion);
|
||||
// NB: We cannot do:
|
||||
// fn_criteria.push(build_criteria(&criterion));
|
||||
// because criterion doesn't live long enough
|
||||
let result = self.evaluate_node_in_context(&args[case_index * 2 - 1], cell);
|
||||
if result.is_error() {
|
||||
return Err(result);
|
||||
}
|
||||
if let CalcResult::Range { left, right } = result {
|
||||
if left.sheet != right.sheet {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
));
|
||||
}
|
||||
// TODO test ranges are of the same size as sum_range
|
||||
ranges.push(Range { left, right });
|
||||
} else {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Expected a range".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
for criterion in criteria.iter() {
|
||||
fn_criteria.push(build_criteria(criterion));
|
||||
}
|
||||
|
||||
let left_row = sum_range.left.row;
|
||||
let left_column = sum_range.left.column;
|
||||
let mut right_row = sum_range.right.row;
|
||||
let mut right_column = sum_range.right.column;
|
||||
|
||||
if left_row == 1 && right_row == LAST_ROW {
|
||||
right_row = match self.workbook.worksheet(sum_range.left.sheet) {
|
||||
Ok(s) => s.dimension().max_row,
|
||||
Err(_) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", sum_range.left.sheet),
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
if left_column == 1 && right_column == LAST_COLUMN {
|
||||
right_column = match self.workbook.worksheet(sum_range.left.sheet) {
|
||||
Ok(s) => s.dimension().max_column,
|
||||
Err(_) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", sum_range.left.sheet),
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for row in left_row..right_row + 1 {
|
||||
for column in left_column..right_column + 1 {
|
||||
let mut is_true = true;
|
||||
for case_index in 0..case_count {
|
||||
// We check if value in range n meets criterion n
|
||||
let range = &ranges[case_index];
|
||||
let fn_criterion = &fn_criteria[case_index];
|
||||
let value = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: range.left.sheet,
|
||||
row: range.left.row + row - sum_range.left.row,
|
||||
column: range.left.column + column - sum_range.left.column,
|
||||
});
|
||||
if !fn_criterion(&value) {
|
||||
is_true = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if is_true {
|
||||
let v = self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: sum_range.left.sheet,
|
||||
row,
|
||||
column,
|
||||
});
|
||||
match v {
|
||||
CalcResult::Number(n) => apply(n),
|
||||
CalcResult::Error { .. } => return Err(v),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_averageifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let mut total = 0.0;
|
||||
let mut count = 0.0;
|
||||
|
||||
let average = |value: f64| {
|
||||
total += value;
|
||||
count += 1.0;
|
||||
};
|
||||
if let Err(e) = self.apply_ifs(args, cell, average) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if count == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "division by 0".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(total / count)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_minifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let mut min = f64::INFINITY;
|
||||
let apply_min = |value: f64| min = value.min(min);
|
||||
if let Err(e) = self.apply_ifs(args, cell, apply_min) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if min.is_infinite() {
|
||||
min = 0.0;
|
||||
}
|
||||
CalcResult::Number(min)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_maxifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let mut max = -f64::INFINITY;
|
||||
let apply_max = |value: f64| max = value.max(max);
|
||||
if let Err(e) = self.apply_ifs(args, cell, apply_max) {
|
||||
return e;
|
||||
}
|
||||
if max.is_infinite() {
|
||||
max = 0.0;
|
||||
}
|
||||
CalcResult::Number(max)
|
||||
}
|
||||
}
|
||||
124
base/src/functions/statistical/log_normal.rs
Normal file
124
base/src/functions/statistical/log_normal.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use statrs::distribution::{Continuous, ContinuousCDF, LogNormal};
|
||||
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_log_norm_dist(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
if args.len() != 4 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let mean = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let std_dev = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let cumulative = match self.get_boolean(&args[3], cell) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// Excel domain checks
|
||||
if x <= 0.0 || std_dev <= 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for LOGNORM.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let dist = match LogNormal::new(mean, std_dev) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for LOGNORM.DIST".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
|
||||
|
||||
if !result.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for LOGNORM.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_log_norm_inv(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
use statrs::distribution::{ContinuousCDF, LogNormal};
|
||||
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let mean = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let std_dev = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// Excel domain checks
|
||||
if p <= 0.0 || p >= 1.0 || std_dev <= 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for LOGNORM.INV".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let dist = match LogNormal::new(mean, std_dev) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for LOGNORM.INV".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let result = dist.inverse_cdf(p);
|
||||
|
||||
if !result.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for LOGNORM.INV".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
}
|
||||
26
base/src/functions/statistical/mod.rs
Normal file
26
base/src/functions/statistical/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
mod beta;
|
||||
mod binom;
|
||||
mod chisq;
|
||||
mod correl;
|
||||
mod count_and_average;
|
||||
mod covariance;
|
||||
mod devsq;
|
||||
mod exponential;
|
||||
mod fisher;
|
||||
mod gamma;
|
||||
mod gauss;
|
||||
mod geomean;
|
||||
mod hypegeom;
|
||||
mod if_ifs;
|
||||
mod log_normal;
|
||||
mod normal;
|
||||
mod pearson;
|
||||
mod phi;
|
||||
mod poisson;
|
||||
mod rank_eq_avg;
|
||||
mod standard_dev;
|
||||
mod standardize;
|
||||
mod t_dist;
|
||||
mod variance;
|
||||
mod weibull;
|
||||
mod z_test;
|
||||
325
base/src/functions/statistical/normal.rs
Normal file
325
base/src/functions/statistical/normal.rs
Normal file
@@ -0,0 +1,325 @@
|
||||
use statrs::distribution::{Continuous, ContinuousCDF, Normal, StudentsT};
|
||||
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// NORM.DIST(x, mean, standard_dev, cumulative)
|
||||
pub(crate) fn fn_norm_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 4 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let mean = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let std_dev = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let cumulative = match self.get_boolean(&args[3], cell) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// Excel: standard_dev must be > 0
|
||||
if std_dev <= 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "standard_dev must be > 0 in NORM.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let dist = match Normal::new(mean, std_dev) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for NORM.DIST".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
|
||||
|
||||
if !result.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for NORM.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
// NORM.INV(probability, mean, standard_dev)
|
||||
pub(crate) fn fn_norm_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let mean = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let std_dev = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if p <= 0.0 || p >= 1.0 || std_dev <= 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for NORM.INV".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let dist = match Normal::new(mean, std_dev) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for NORM.INV".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let x = dist.inverse_cdf(p);
|
||||
|
||||
if !x.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for NORM.INV".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(x)
|
||||
}
|
||||
|
||||
// NORM.S.DIST(z, cumulative)
|
||||
pub(crate) fn fn_norm_s_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let z = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let cumulative = match self.get_boolean(&args[1], cell) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let dist = match Normal::new(0.0, 1.0) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Failed to construct standard normal distribution".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let result = if cumulative { dist.cdf(z) } else { dist.pdf(z) };
|
||||
|
||||
if !result.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for NORM.S.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
// NORM.S.INV(probability)
|
||||
pub(crate) fn fn_norm_s_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if p <= 0.0 || p >= 1.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "probability must be in (0,1) in NORM.S.INV".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let dist = match Normal::new(0.0, 1.0) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Failed to construct standard normal distribution".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let z = dist.inverse_cdf(p);
|
||||
|
||||
if !z.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for NORM.S.INV".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(z)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_confidence_norm(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let alpha = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let std_dev = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let size = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f.floor(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if alpha <= 0.0 || alpha >= 1.0 || std_dev <= 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for CONFIDENCE.NORM".to_string(),
|
||||
};
|
||||
}
|
||||
if size < 1.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Sample size must be at least 1".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let normal = match Normal::new(0.0, 1.0) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
"Failed to construct normal distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let quantile = normal.inverse_cdf(1.0 - alpha / 2.0);
|
||||
if !quantile.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid quantile for CONFIDENCE.NORM".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let margin = quantile * std_dev / size.sqrt();
|
||||
CalcResult::Number(margin)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_confidence_t(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let alpha = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let std_dev = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let size = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// Domain checks
|
||||
if alpha <= 0.0 || alpha >= 1.0 || std_dev <= 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for CONFIDENCE.T".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// Need at least 2 observations so df = n - 1 > 0
|
||||
if size < 2.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Sample size must be at least 2".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let df = size - 1.0;
|
||||
|
||||
let t_dist = match StudentsT::new(0.0, 1.0, df) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
"Failed to construct Student's t distribution".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// Two-sided CI => use 1 - alpha/2
|
||||
let t_crit = t_dist.inverse_cdf(1.0 - alpha / 2.0);
|
||||
if !t_crit.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid quantile for CONFIDENCE.T".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let margin = t_crit * std_dev / size.sqrt();
|
||||
CalcResult::Number(margin)
|
||||
}
|
||||
}
|
||||
113
base/src/functions/statistical/pearson.rs
Normal file
113
base/src/functions/statistical/pearson.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// PEARSON(array1, array2)
|
||||
pub(crate) fn fn_pearson(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_, _, values_left, values_right) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(result) => result,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// Flatten into (x, y) pairs, skipping non-numeric entries (None)
|
||||
let mut n: f64 = 0.0;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_x2 = 0.0;
|
||||
let mut sum_y2 = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
|
||||
let len = values_left.len().min(values_right.len());
|
||||
for i in 0..len {
|
||||
match (values_left[i], values_right[i]) {
|
||||
(Some(x), Some(y)) => {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_y2 += y * y;
|
||||
sum_xy += x * y;
|
||||
}
|
||||
_ => {
|
||||
// Ignore pairs where at least one side is non-numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if n < 2.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"PEARSON requires at least two numeric pairs".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Pearson correlation:
|
||||
// r = [ n*Σxy - (Σx)(Σy) ] / sqrt( [n*Σx² - (Σx)²] [n*Σy² - (Σy)²] )
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let denom_x = n * sum_x2 - sum_x * sum_x;
|
||||
let denom_y = n * sum_y2 - sum_y * sum_y;
|
||||
|
||||
if denom_x.abs() < 1e-15 || denom_y.abs() < 1e-15 {
|
||||
// Zero variance in at least one series
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"PEARSON cannot be computed when one series has zero variance".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let denom = (denom_x * denom_y).sqrt();
|
||||
|
||||
CalcResult::Number(num / denom)
|
||||
}
|
||||
|
||||
// RSQ(array1, array2) = CORREL(array1, array2)^2
|
||||
pub(crate) fn fn_rsq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_rows, _cols, values1, values2) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let mut n = 0.0_f64;
|
||||
let mut sum_x = 0.0_f64;
|
||||
let mut sum_y = 0.0_f64;
|
||||
let mut sum_x2 = 0.0_f64;
|
||||
let mut sum_y2 = 0.0_f64;
|
||||
let mut sum_xy = 0.0_f64;
|
||||
|
||||
let len = values1.len().min(values2.len());
|
||||
for i in 0..len {
|
||||
if let (Some(x), Some(y)) = (values1[i], values2[i]) {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_y2 += y * y;
|
||||
sum_xy += x * y;
|
||||
}
|
||||
}
|
||||
|
||||
if n < 2.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"RSQ requires at least two numeric data points in each range".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let denom_x = n * sum_x2 - sum_x * sum_x;
|
||||
let denom_y = n * sum_y2 - sum_y * sum_y;
|
||||
let denom = (denom_x * denom_y).sqrt();
|
||||
|
||||
if denom == 0.0 || !denom.is_finite() {
|
||||
return CalcResult::new_error(Error::DIV, cell, "Division by zero in RSQ".to_string());
|
||||
}
|
||||
|
||||
let r = num / denom;
|
||||
CalcResult::Number(r * r)
|
||||
}
|
||||
}
|
||||
21
base/src/functions/statistical/phi.rs
Normal file
21
base/src/functions/statistical/phi.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{calc_result::CalcResult, expressions::parser::Node, model::Model};
|
||||
|
||||
impl Model {
|
||||
// PHI(x) = standard normal PDF at x
|
||||
pub(crate) fn fn_phi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// Standard normal PDF: (1 / sqrt(2π)) * exp(-x^2 / 2)
|
||||
let result = (-(x * x) / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt();
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
}
|
||||
94
base/src/functions/statistical/poisson.rs
Normal file
94
base/src/functions/statistical/poisson.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use statrs::distribution::{Discrete, DiscreteCDF, Poisson};
|
||||
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// =POISSON.DIST(x, mean, cumulative)
|
||||
pub(crate) fn fn_poisson_dist(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
// x
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// mean (lambda)
|
||||
let lambda = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let cumulative = match self.get_boolean(&args[2], cell) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if x < 0.0 || lambda < 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for POISSON.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// Guard against insane k for u64
|
||||
if x < 0.0 || x > (u64::MAX as f64) {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for POISSON.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let k = x as u64;
|
||||
|
||||
// Special-case lambda = 0: degenerate distribution at 0
|
||||
if lambda == 0.0 {
|
||||
let result = if cumulative {
|
||||
// For x >= 0, P(X <= x) = 1
|
||||
1.0
|
||||
} else {
|
||||
// P(X = 0) = 1, P(X = k>0) = 0
|
||||
if k == 0 {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
return CalcResult::Number(result);
|
||||
}
|
||||
|
||||
let dist = match Poisson::new(lambda) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for POISSON.DIST".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let prob = if cumulative { dist.cdf(k) } else { dist.pmf(k) };
|
||||
|
||||
if !prob.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for POISSON.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(prob)
|
||||
}
|
||||
}
|
||||
202
base/src/functions/statistical/rank_eq_avg.rs
Normal file
202
base/src/functions/statistical/rank_eq_avg.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// Helper to collect numeric values from the 2nd argument of RANK.*
|
||||
fn collect_rank_values(
|
||||
&mut self,
|
||||
arg: &Node,
|
||||
cell: CellReferenceIndex,
|
||||
) -> Result<Vec<f64>, CalcResult> {
|
||||
let values = match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Array(array) => match self.values_from_array(array) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: format!("Unsupported array argument: {}", e),
|
||||
})
|
||||
}
|
||||
},
|
||||
CalcResult::Range { left, right } => self.values_from_range(left, right)?,
|
||||
CalcResult::Boolean(value) => {
|
||||
if !matches!(arg, Node::ReferenceKind { .. }) {
|
||||
vec![Some(if value { 1.0 } else { 0.0 })]
|
||||
} else {
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let numeric_values: Vec<f64> = values.into_iter().flatten().collect();
|
||||
Ok(numeric_values)
|
||||
}
|
||||
|
||||
// RANK.EQ(number, ref, [order])
|
||||
pub(crate) fn fn_rank_eq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if !(2..=3).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
// number
|
||||
let number = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// ref
|
||||
let mut values = match self.collect_rank_values(&args[1], cell) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if values.is_empty() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "No numeric values for RANK.EQ".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// order: default 0 (descending)
|
||||
let order = if args.len() == 2 {
|
||||
0.0
|
||||
} else {
|
||||
match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
}
|
||||
};
|
||||
|
||||
values.retain(|v| !v.is_nan());
|
||||
|
||||
// "better" = greater (descending) or smaller (ascending)
|
||||
let mut better = 0;
|
||||
let mut equal = 0;
|
||||
|
||||
if order == 0.0 {
|
||||
// descending
|
||||
for v in &values {
|
||||
if *v > number {
|
||||
better += 1;
|
||||
} else if *v == number {
|
||||
equal += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ascending
|
||||
for v in &values {
|
||||
if *v < number {
|
||||
better += 1;
|
||||
} else if *v == number {
|
||||
equal += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if equal == 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Number not found in reference for RANK.EQ".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let rank = (better as f64) + 1.0;
|
||||
CalcResult::Number(rank)
|
||||
}
|
||||
|
||||
// RANK.AVG(number, ref, [order])
|
||||
pub(crate) fn fn_rank_avg(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if !(2..=3).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
// number
|
||||
let number = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// ref
|
||||
let mut values = match self.collect_rank_values(&args[1], cell) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if values.is_empty() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "No numeric values for RANK.AVG".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// order: default 0 (descending)
|
||||
let order = if args.len() == 2 {
|
||||
0.0
|
||||
} else {
|
||||
match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
}
|
||||
};
|
||||
|
||||
values.retain(|v| !v.is_nan());
|
||||
|
||||
// > or < depending on order
|
||||
let mut better = 0;
|
||||
let mut equal = 0;
|
||||
|
||||
if order == 0.0 {
|
||||
// descending
|
||||
for v in &values {
|
||||
if *v > number {
|
||||
better += 1;
|
||||
} else if *v == number {
|
||||
equal += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ascending
|
||||
for v in &values {
|
||||
if *v < number {
|
||||
better += 1;
|
||||
} else if *v == number {
|
||||
equal += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if equal == 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Number not found in reference for RANK.AVG".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// For ties, average of the ranks. If the equal values occupy positions
|
||||
// (better+1) ..= (better+equal), the average is:
|
||||
// better + (equal + 1) / 2
|
||||
let better_f = better as f64;
|
||||
let equal_f = equal as f64;
|
||||
let rank = better_f + (equal_f + 1.0) / 2.0;
|
||||
|
||||
CalcResult::Number(rank)
|
||||
}
|
||||
}
|
||||
519
base/src/functions/statistical/standard_dev.rs
Normal file
519
base/src/functions/statistical/standard_dev.rs
Normal file
@@ -0,0 +1,519 @@
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::parser::ArrayNode;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_stdev_p(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut sum = 0.0;
|
||||
let mut sumsq = 0.0;
|
||||
let mut count: u64 = 0;
|
||||
|
||||
#[inline]
|
||||
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||
*sum += value;
|
||||
*sumsq += value * value;
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let row1 = left.row;
|
||||
let mut row2 = right.row;
|
||||
let column1 = left.column;
|
||||
let mut column2 = right.column;
|
||||
|
||||
if row1 == 1 && row2 == LAST_ROW {
|
||||
row2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_row,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
column2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_column,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for row in row1..row2 + 1 {
|
||||
for column in column1..(column2 + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Array(array) => {
|
||||
for row in array {
|
||||
for value in row {
|
||||
match value {
|
||||
ArrayNode::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
ArrayNode::Error(error) => {
|
||||
return CalcResult::Error {
|
||||
error,
|
||||
origin: cell,
|
||||
message: "Error in array".to_string(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// ignore non-numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"STDEV.P with no numeric data".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n = count as f64;
|
||||
let mut var = (sumsq - (sum * sum) / n) / n;
|
||||
|
||||
// clamp tiny negatives from FP noise
|
||||
if var < 0.0 && var > -1e-12 {
|
||||
var = 0.0;
|
||||
}
|
||||
|
||||
CalcResult::Number(var.sqrt())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_stdev_s(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut sum = 0.0;
|
||||
let mut sumsq = 0.0;
|
||||
let mut count: u64 = 0;
|
||||
|
||||
#[inline]
|
||||
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||
*sum += value;
|
||||
*sumsq += value * value;
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let row1 = left.row;
|
||||
let mut row2 = right.row;
|
||||
let column1 = left.column;
|
||||
let mut column2 = right.column;
|
||||
|
||||
if row1 == 1 && row2 == LAST_ROW {
|
||||
row2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_row,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
column2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_column,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for row in row1..row2 + 1 {
|
||||
for column in column1..(column2 + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Array(array) => {
|
||||
for row in array {
|
||||
for value in row {
|
||||
match value {
|
||||
ArrayNode::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
ArrayNode::Error(error) => {
|
||||
return CalcResult::Error {
|
||||
error,
|
||||
origin: cell,
|
||||
message: "Error in array".to_string(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// ignore non-numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count <= 1 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"STDEV.S requires at least two numeric values".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n = count as f64;
|
||||
let mut var = (sumsq - (sum * sum) / n) / (n - 1.0);
|
||||
|
||||
if var < 0.0 && var > -1e-12 {
|
||||
var = 0.0;
|
||||
}
|
||||
|
||||
CalcResult::Number(var.sqrt())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_stdeva(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut sum = 0.0;
|
||||
let mut sumsq = 0.0;
|
||||
let mut count: u64 = 0;
|
||||
|
||||
#[inline]
|
||||
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||
*sum += value;
|
||||
*sumsq += value * value;
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let row1 = left.row;
|
||||
let mut row2 = right.row;
|
||||
let column1 = left.column;
|
||||
let mut column2 = right.column;
|
||||
|
||||
if row1 == 1 && row2 == LAST_ROW {
|
||||
row2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_row,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
column2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_column,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for row in row1..row2 + 1 {
|
||||
for column in column1..(column2 + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
CalcResult::String(_) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, 0.0);
|
||||
}
|
||||
CalcResult::Boolean(value) => {
|
||||
let val = if value { 1.0 } else { 0.0 };
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, val);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric for now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Array(array) => {
|
||||
for row in array {
|
||||
for value in row {
|
||||
match value {
|
||||
ArrayNode::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
ArrayNode::Error(error) => {
|
||||
return CalcResult::Error {
|
||||
error,
|
||||
origin: cell,
|
||||
message: "Error in array".to_string(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// ignore non-numeric for now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric for now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count <= 1 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"STDEVA requires at least two numeric values".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n = count as f64;
|
||||
let mut var = (sumsq - (sum * sum) / n) / (n - 1.0);
|
||||
|
||||
if var < 0.0 && var > -1e-12 {
|
||||
var = 0.0;
|
||||
}
|
||||
|
||||
CalcResult::Number(var.sqrt())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_stdevpa(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut sum = 0.0;
|
||||
let mut sumsq = 0.0;
|
||||
let mut count: u64 = 0;
|
||||
|
||||
#[inline]
|
||||
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||
*sum += value;
|
||||
*sumsq += value * value;
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let row1 = left.row;
|
||||
let mut row2 = right.row;
|
||||
let column1 = left.column;
|
||||
let mut column2 = right.column;
|
||||
|
||||
if row1 == 1 && row2 == LAST_ROW {
|
||||
row2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_row,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
column2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_column,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for row in row1..row2 + 1 {
|
||||
for column in column1..(column2 + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
CalcResult::String(_) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, 0.0);
|
||||
}
|
||||
CalcResult::Boolean(value) => {
|
||||
let val = if value { 1.0 } else { 0.0 };
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, val);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric for now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Array(array) => {
|
||||
for row in array {
|
||||
for value in row {
|
||||
match value {
|
||||
ArrayNode::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
ArrayNode::Error(error) => {
|
||||
return CalcResult::Error {
|
||||
error,
|
||||
origin: cell,
|
||||
message: "Error in array".to_string(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// ignore non-numeric for now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric for now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"STDEVPA with no numeric data".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n = count as f64;
|
||||
let mut var = (sumsq - (sum * sum) / n) / n;
|
||||
|
||||
if var < 0.0 && var > -1e-12 {
|
||||
var = 0.0;
|
||||
}
|
||||
|
||||
CalcResult::Number(var.sqrt())
|
||||
}
|
||||
}
|
||||
38
base/src/functions/statistical/standardize.rs
Normal file
38
base/src/functions/statistical/standardize.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_standardize(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
// STANDARDIZE(x, mean, standard_dev)
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let mean = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let std_dev = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if std_dev <= 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "standard_dev must be > 0 in STANDARDIZE".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let z = (x - mean) / std_dev;
|
||||
|
||||
CalcResult::Number(z)
|
||||
}
|
||||
}
|
||||
588
base/src/functions/statistical/t_dist.rs
Normal file
588
base/src/functions/statistical/t_dist.rs
Normal file
@@ -0,0 +1,588 @@
|
||||
use statrs::distribution::{Continuous, ContinuousCDF, StudentsT};
|
||||
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
fn mean(xs: &[f64]) -> f64 {
|
||||
let n = xs.len();
|
||||
if n == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
let mut s = 0.0;
|
||||
for &x in xs {
|
||||
s += x;
|
||||
}
|
||||
s / (n as f64)
|
||||
}
|
||||
|
||||
pub(crate) fn sample_var(xs: &[f64]) -> f64 {
|
||||
let n = xs.len();
|
||||
if n < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
let m = mean(xs);
|
||||
let mut s = 0.0;
|
||||
for &x in xs {
|
||||
let d = x - m;
|
||||
s += d * d;
|
||||
}
|
||||
s / ((n - 1) as f64)
|
||||
}
|
||||
|
||||
enum TTestType {
|
||||
Paired,
|
||||
TwoSampleEqualVar,
|
||||
TwoSampleUnequalVar,
|
||||
}
|
||||
|
||||
enum TTestTails {
|
||||
OneTailed,
|
||||
TwoTailed,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
// T.DIST(x, deg_freedom, cumulative)
|
||||
pub(crate) fn fn_t_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let df = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let cumulative = match self.get_boolean(&args[2], cell) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if df < 1.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "deg_freedom must be >= 1 in T.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let dist = match StudentsT::new(0.0, 1.0, df) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for T.DIST".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
|
||||
|
||||
if !result.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for T.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
// T.DIST.2T(x, deg_freedom)
|
||||
pub(crate) fn fn_t_dist_2t(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let df = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if x < 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "x must be >= 0 in T.DIST.2T".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
if df < 1.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "deg_freedom must be >= 1 in T.DIST.2T".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let dist = match StudentsT::new(0.0, 1.0, df) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for T.DIST.2T".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let upper_tail = 1.0 - dist.cdf(x);
|
||||
let mut result = 2.0 * upper_tail;
|
||||
|
||||
result = result.clamp(0.0, 1.0);
|
||||
|
||||
if !result.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for T.DIST.2T".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
// T.DIST.RT(x, deg_freedom)
|
||||
pub(crate) fn fn_t_dist_rt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let df = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if df < 1.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "deg_freedom must be >= 1 in T.DIST.RT".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let dist = match StudentsT::new(0.0, 1.0, df) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for T.DIST.RT".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let result = 1.0 - dist.cdf(x);
|
||||
|
||||
if !result.is_finite() || result < 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for T.DIST.RT".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
// T.INV(probability, deg_freedom)
|
||||
pub(crate) fn fn_t_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let df = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if p <= 0.0 || p >= 1.0 || df < 1.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for T.INV".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let dist = match StudentsT::new(0.0, 1.0, df) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for T.INV".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let x = dist.inverse_cdf(p);
|
||||
|
||||
if !x.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for T.INV".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(x)
|
||||
}
|
||||
|
||||
// T.INV.2T(probability, deg_freedom)
|
||||
pub(crate) fn fn_t_inv_2t(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let p = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let df = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if p <= 0.0 || p > 1.0 || df < 1.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for T.INV.2T".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let dist = match StudentsT::new(0.0, 1.0, df) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for T.INV.2T".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Two-sided: F(x) = 1 - p/2
|
||||
let target_cdf = 1.0 - p / 2.0;
|
||||
let x = dist.inverse_cdf(target_cdf);
|
||||
|
||||
if !x.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for T.INV.2T".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(x.abs())
|
||||
}
|
||||
|
||||
// T.TEST(array1, array2, tails, type)
|
||||
pub(crate) fn fn_t_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 4 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let values1_opts = match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||
Ok(v) => v,
|
||||
Err(error) => return error,
|
||||
},
|
||||
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in first array: {:?}", error),
|
||||
);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"First argument must be a range or array".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let values2_opts = match self.evaluate_node_in_context(&args[1], cell) {
|
||||
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||
Ok(v) => v,
|
||||
Err(error) => return error,
|
||||
},
|
||||
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Second argument must be a range or array".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let tails = match self.get_number(&args[2], cell) {
|
||||
Ok(f) => {
|
||||
let tf = f.trunc();
|
||||
if tf == 1.0 {
|
||||
TTestTails::OneTailed
|
||||
} else if tf == 2.0 {
|
||||
TTestTails::TwoTailed
|
||||
} else {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"tails must be 1 or 2".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => return e,
|
||||
};
|
||||
let test_type = match self.get_number(&args[3], cell) {
|
||||
Ok(f) => {
|
||||
let tf = f.trunc();
|
||||
match tf {
|
||||
1.0 => TTestType::Paired,
|
||||
2.0 => TTestType::TwoSampleEqualVar,
|
||||
3.0 => TTestType::TwoSampleUnequalVar,
|
||||
_ => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"type must be 1, 2, or 3".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let (values1, values2): (Vec<f64>, Vec<f64>) = if matches!(test_type, TTestType::Paired) {
|
||||
values1_opts
|
||||
.into_iter()
|
||||
.zip(values2_opts)
|
||||
.filter_map(|(o1, o2)| match (o1, o2) {
|
||||
(Some(v1), Some(v2)) => Some((v1, v2)),
|
||||
_ => None, // skip if either is None
|
||||
})
|
||||
.unzip()
|
||||
} else {
|
||||
// keep only numeric entries, ignore non-numeric (Option::None)
|
||||
let v1: Vec<f64> = values1_opts.into_iter().flatten().collect();
|
||||
let v2: Vec<f64> = values2_opts.into_iter().flatten().collect();
|
||||
(v1, v2)
|
||||
};
|
||||
|
||||
let n1 = values1.len();
|
||||
let n2 = values2.len();
|
||||
|
||||
if n1 == 0 || n2 == 0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"T.TEST requires non-empty samples".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let (t_stat, df) = match test_type {
|
||||
TTestType::Paired => {
|
||||
if n1 != n2 {
|
||||
return CalcResult::new_error(
|
||||
Error::NA,
|
||||
cell,
|
||||
"For paired T.TEST, both samples must have the same length".to_string(),
|
||||
);
|
||||
}
|
||||
if n1 < 2 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Paired T.TEST requires at least two pairs".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut diffs = Vec::with_capacity(n1);
|
||||
for i in 0..n1 {
|
||||
diffs.push(values1[i] - values2[i]);
|
||||
}
|
||||
|
||||
let nd = diffs.len();
|
||||
let md = mean(&diffs);
|
||||
let vd = sample_var(&diffs);
|
||||
if vd <= 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Zero variance in paired T.TEST".to_string(),
|
||||
);
|
||||
}
|
||||
let sd = vd.sqrt();
|
||||
let t_stat = md / (sd / (nd as f64).sqrt());
|
||||
let df = (nd - 1) as f64;
|
||||
(t_stat, df)
|
||||
}
|
||||
|
||||
// 2: two-sample, equal variance (homoscedastic)
|
||||
TTestType::TwoSampleEqualVar => {
|
||||
if n1 < 2 || n2 < 2 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Two-sample T.TEST type 2 requires at least two values in each sample"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let m1 = mean(&values1);
|
||||
let m2 = mean(&values2);
|
||||
let v1 = sample_var(&values1);
|
||||
let v2 = sample_var(&values2);
|
||||
|
||||
let df_i = (n1 + n2 - 2) as i32;
|
||||
if df_i <= 0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Degrees of freedom must be positive in T.TEST type 2".to_string(),
|
||||
);
|
||||
}
|
||||
let df = df_i as f64;
|
||||
|
||||
let sp2 = (((n1 - 1) as f64) * v1 + ((n2 - 1) as f64) * v2) / df; // pooled variance
|
||||
|
||||
if sp2 <= 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Zero pooled variance in T.TEST type 2".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let denom = (sp2 * (1.0 / (n1 as f64) + 1.0 / (n2 as f64))).sqrt();
|
||||
if denom == 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Zero denominator in T.TEST type 2".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let t_stat = (m1 - m2) / denom;
|
||||
(t_stat, df)
|
||||
}
|
||||
|
||||
// two-sample, unequal variance (Welch)
|
||||
TTestType::TwoSampleUnequalVar => {
|
||||
if n1 < 2 || n2 < 2 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Two-sample T.TEST type 3 requires at least two values in each sample"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let m1 = mean(&values1);
|
||||
let m2 = mean(&values2);
|
||||
let v1 = sample_var(&values1);
|
||||
let v2 = sample_var(&values2);
|
||||
|
||||
let s1n = v1 / (n1 as f64);
|
||||
let s2n = v2 / (n2 as f64);
|
||||
let denom = (s1n + s2n).sqrt();
|
||||
if denom == 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Zero denominator in T.TEST type 3".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let t_stat = (m1 - m2) / denom;
|
||||
|
||||
let num_df = (s1n + s2n).powi(2);
|
||||
let den_df = (s1n * s1n) / ((n1 - 1) as f64) + (s2n * s2n) / ((n2 - 1) as f64);
|
||||
if den_df == 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Invalid degrees of freedom in T.TEST type 3".to_string(),
|
||||
);
|
||||
}
|
||||
let df = num_df / den_df;
|
||||
(t_stat, df)
|
||||
}
|
||||
};
|
||||
|
||||
if df <= 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Degrees of freedom must be positive in T.TEST".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let dist = match StudentsT::new(0.0, 1.0, df) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Invalid parameters for Student's t distribution".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let t_abs = t_stat.abs();
|
||||
let cdf = dist.cdf(t_abs);
|
||||
|
||||
let mut p = match tails {
|
||||
TTestTails::OneTailed => 1.0 - cdf,
|
||||
TTestTails::TwoTailed => 2.0 * (1.0 - cdf),
|
||||
};
|
||||
|
||||
// clamp tiny fp noise
|
||||
if p < 0.0 && p > -1e-15 {
|
||||
p = 0.0;
|
||||
}
|
||||
if p > 1.0 && p < 1.0 + 1e-15 {
|
||||
p = 1.0;
|
||||
}
|
||||
|
||||
CalcResult::Number(p)
|
||||
}
|
||||
}
|
||||
518
base/src/functions/statistical/variance.rs
Normal file
518
base/src/functions/statistical/variance.rs
Normal file
@@ -0,0 +1,518 @@
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::parser::ArrayNode;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_var_p(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut sum = 0.0;
|
||||
let mut sumsq = 0.0;
|
||||
let mut count: u64 = 0;
|
||||
|
||||
#[inline]
|
||||
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||
*sum += value;
|
||||
*sumsq += value * value;
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let row1 = left.row;
|
||||
let mut row2 = right.row;
|
||||
let column1 = left.column;
|
||||
let mut column2 = right.column;
|
||||
|
||||
if row1 == 1 && row2 == LAST_ROW {
|
||||
row2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_row,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
column2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_column,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for row in row1..row2 + 1 {
|
||||
for column in column1..(column2 + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Array(array) => {
|
||||
for row in array {
|
||||
for value in row {
|
||||
match value {
|
||||
ArrayNode::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
ArrayNode::Error(error) => {
|
||||
return CalcResult::Error {
|
||||
error,
|
||||
origin: cell,
|
||||
message: "Error in array".to_string(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// ignore non-numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"VAR.P with no numeric data".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n = count as f64;
|
||||
let mut var = (sumsq - (sum * sum) / n) / n;
|
||||
|
||||
if var < 0.0 && var > -1e-12 {
|
||||
var = 0.0;
|
||||
}
|
||||
|
||||
CalcResult::Number(var)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_var_s(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut sum = 0.0;
|
||||
let mut sumsq = 0.0;
|
||||
let mut count: u64 = 0;
|
||||
|
||||
#[inline]
|
||||
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||
*sum += value;
|
||||
*sumsq += value * value;
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let row1 = left.row;
|
||||
let mut row2 = right.row;
|
||||
let column1 = left.column;
|
||||
let mut column2 = right.column;
|
||||
|
||||
if row1 == 1 && row2 == LAST_ROW {
|
||||
row2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_row,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
column2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_column,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for row in row1..row2 + 1 {
|
||||
for column in column1..(column2 + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Array(array) => {
|
||||
for row in array {
|
||||
for value in row {
|
||||
match value {
|
||||
ArrayNode::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
ArrayNode::Error(error) => {
|
||||
return CalcResult::Error {
|
||||
error,
|
||||
origin: cell,
|
||||
message: "Error in array".to_string(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// ignore non-numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count <= 1 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"VAR.S requires at least two numeric values".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n = count as f64;
|
||||
let mut var = (sumsq - (sum * sum) / n) / (n - 1.0);
|
||||
|
||||
if var < 0.0 && var > -1e-12 {
|
||||
var = 0.0;
|
||||
}
|
||||
|
||||
CalcResult::Number(var)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_vara(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut sum = 0.0;
|
||||
let mut sumsq = 0.0;
|
||||
let mut count: u64 = 0;
|
||||
|
||||
#[inline]
|
||||
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||
*sum += value;
|
||||
*sumsq += value * value;
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let row1 = left.row;
|
||||
let mut row2 = right.row;
|
||||
let column1 = left.column;
|
||||
let mut column2 = right.column;
|
||||
|
||||
if row1 == 1 && row2 == LAST_ROW {
|
||||
row2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_row,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
column2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_column,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for row in row1..=row2 {
|
||||
for column in column1..=column2 {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
CalcResult::String(_) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, 0.0);
|
||||
}
|
||||
CalcResult::Boolean(value) => {
|
||||
let val = if value { 1.0 } else { 0.0 };
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, val);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric for now (A semantics to be added)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Array(array) => {
|
||||
for row in array {
|
||||
for value in row {
|
||||
match value {
|
||||
ArrayNode::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
ArrayNode::Error(error) => {
|
||||
return CalcResult::Error {
|
||||
error,
|
||||
origin: cell,
|
||||
message: "Error in array".to_string(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// ignore non-numeric for now (A semantics to be added)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric for now (A semantics to be added)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count <= 1 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"VARA requires at least two numeric values".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n = count as f64;
|
||||
let mut var = (sumsq - (sum * sum) / n) / (n - 1.0);
|
||||
|
||||
if var < 0.0 && var > -1e-12 {
|
||||
var = 0.0;
|
||||
}
|
||||
|
||||
CalcResult::Number(var)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_varpa(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut sum = 0.0;
|
||||
let mut sumsq = 0.0;
|
||||
let mut count: u64 = 0;
|
||||
|
||||
#[inline]
|
||||
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
|
||||
*sum += value;
|
||||
*sumsq += value * value;
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let row1 = left.row;
|
||||
let mut row2 = right.row;
|
||||
let column1 = left.column;
|
||||
let mut column2 = right.column;
|
||||
|
||||
if row1 == 1 && row2 == LAST_ROW {
|
||||
row2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_row,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
column2 = match self.workbook.worksheet(left.sheet) {
|
||||
Ok(s) => s.dimension().max_column,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Invalid worksheet index: '{}'", left.sheet),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for row in row1..row2 + 1 {
|
||||
for column in column1..(column2 + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
CalcResult::String(_) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, 0.0);
|
||||
}
|
||||
CalcResult::Boolean(value) => {
|
||||
let val = if value { 1.0 } else { 0.0 };
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, val);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric for now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Array(array) => {
|
||||
for row in array {
|
||||
for value in row {
|
||||
match value {
|
||||
ArrayNode::Number(value) => {
|
||||
accumulate(&mut sum, &mut sumsq, &mut count, value);
|
||||
}
|
||||
ArrayNode::Error(error) => {
|
||||
return CalcResult::Error {
|
||||
error,
|
||||
origin: cell,
|
||||
message: "Error in array".to_string(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// ignore non-numeric for now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// ignore non-numeric for now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"VARPA with no numeric data".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n = count as f64;
|
||||
let mut var = (sumsq - (sum * sum) / n) / n;
|
||||
|
||||
if var < 0.0 && var > -1e-12 {
|
||||
var = 0.0;
|
||||
}
|
||||
|
||||
CalcResult::Number(var)
|
||||
}
|
||||
}
|
||||
71
base/src/functions/statistical/weibull.rs
Normal file
71
base/src/functions/statistical/weibull.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use statrs::distribution::{Continuous, ContinuousCDF, Weibull};
|
||||
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// WEIBULL.DIST(x, alpha, beta, cumulative)
|
||||
pub(crate) fn fn_weibull_dist(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> CalcResult {
|
||||
if args.len() != 4 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let alpha = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let beta = match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let cumulative = match self.get_boolean(&args[3], cell) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if x < 0.0 || alpha <= 0.0 || beta <= 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for WEIBULL.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// statrs::Weibull: shape = k (alpha), scale = lambda (beta)
|
||||
let dist = match Weibull::new(alpha, beta) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameters for WEIBULL.DIST".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let result = if cumulative { dist.cdf(x) } else { dist.pdf(x) };
|
||||
|
||||
if !result.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for WEIBULL.DIST".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
}
|
||||
171
base/src/functions/statistical/z_test.rs
Normal file
171
base/src/functions/statistical/z_test.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use statrs::distribution::{ContinuousCDF, Normal};
|
||||
|
||||
use crate::expressions::token::Error;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{calc_result::CalcResult, expressions::parser::Node, model::Model};
|
||||
|
||||
impl Model {
|
||||
// Z.TEST(array, x, [sigma])
|
||||
pub(crate) fn fn_z_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
// 2 or 3 arguments
|
||||
if args.len() < 2 || args.len() > 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let array_arg = self.evaluate_node_in_context(&args[0], cell);
|
||||
|
||||
// Flatten first argument into Vec<Option<f64>> (numeric / non-numeric)
|
||||
let values = match array_arg {
|
||||
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||
Ok(v) => v,
|
||||
Err(error) => return error,
|
||||
},
|
||||
CalcResult::Array(array) => match self.values_from_array(array) {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in array argument: {:?}", error),
|
||||
);
|
||||
}
|
||||
},
|
||||
CalcResult::Number(v) => vec![Some(v)],
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Z.TEST first argument must be a range or array".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Collect basic stats on numeric entries
|
||||
let mut sum = 0.0;
|
||||
let mut count: u64 = 0;
|
||||
|
||||
for x in values.iter().flatten() {
|
||||
sum += x;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Excel: if array has no numeric values -> #N/A
|
||||
if count == 0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NA,
|
||||
cell,
|
||||
"Z.TEST array has no numeric data".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let n = count as f64;
|
||||
let mean = sum / n;
|
||||
|
||||
// x argument (hypothesized population mean)
|
||||
let x_value = match self.evaluate_node_in_context(&args[1], cell) {
|
||||
CalcResult::Number(v) => v,
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Z.TEST second argument (x) must be numeric".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Optional sigma
|
||||
let mut sigma: Option<f64> = None;
|
||||
if args.len() == 3 {
|
||||
match self.evaluate_node_in_context(&args[2], cell) {
|
||||
CalcResult::Number(v) => {
|
||||
if v == 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Z.TEST sigma cannot be zero".to_string(),
|
||||
);
|
||||
}
|
||||
sigma = Some(v);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Z.TEST sigma (third argument) must be numeric".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If sigma omitted, use sample standard deviation STDEV(array)
|
||||
let sigma_value = if let Some(s) = sigma {
|
||||
s
|
||||
} else {
|
||||
// Excel: if only one numeric value and sigma omitted -> #DIV/0!
|
||||
if count <= 1 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Z.TEST requires at least two values when sigma is omitted".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Compute sum of squared deviations
|
||||
let mut sumsq_dev = 0.0;
|
||||
for x in values.iter().flatten() {
|
||||
let d = x - mean;
|
||||
sumsq_dev += d * d;
|
||||
}
|
||||
|
||||
let var = sumsq_dev / (n - 1.0);
|
||||
if var <= 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Z.TEST standard deviation is zero".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
var.sqrt()
|
||||
};
|
||||
|
||||
// Compute z statistic: (mean - x) / (sigma / sqrt(n))
|
||||
let denom = sigma_value / n.sqrt();
|
||||
if denom == 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Z.TEST denominator is zero".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let z = (mean - x_value) / denom;
|
||||
|
||||
// Standard normal CDF
|
||||
let dist = match Normal::new(0.0, 1.0) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Cannot create standard normal distribution in Z.TEST".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let mut p = 1.0 - dist.cdf(z);
|
||||
|
||||
// clamp tiny FP noise
|
||||
if p < 0.0 && p > -1e-15 {
|
||||
p = 0.0;
|
||||
}
|
||||
if p > 1.0 && p < 1.0 + 1e-15 {
|
||||
p = 1.0;
|
||||
}
|
||||
|
||||
CalcResult::Number(p)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
#[cfg(feature = "use_regex_lite")]
|
||||
use regex_lite as regex;
|
||||
|
||||
use crate::{calc_result::CalcResult, expressions::token::is_english_error_string};
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::token::is_english_error_string,
|
||||
number_format::to_excel_precision,
|
||||
};
|
||||
|
||||
/// This test for exact match (modulo case).
|
||||
/// * strings are not cast into bools or numbers
|
||||
@@ -34,6 +37,8 @@ pub(crate) fn values_are_equal(left: &CalcResult, right: &CalcResult) -> bool {
|
||||
pub(crate) fn compare_values(left: &CalcResult, right: &CalcResult) -> i32 {
|
||||
match (left, right) {
|
||||
(CalcResult::Number(value1), CalcResult::Number(value2)) => {
|
||||
let value1 = to_excel_precision(*value1, 15);
|
||||
let value2 = to_excel_precision(*value2, 15);
|
||||
if (value2 - value1).abs() < f64::EPSILON {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2068,21 +2068,7 @@ impl Model {
|
||||
scope: Option<u32>,
|
||||
formula: &str,
|
||||
) -> Result<(), String> {
|
||||
if !is_valid_identifier(name) {
|
||||
return Err("Invalid defined name".to_string());
|
||||
};
|
||||
let name_upper = name.to_uppercase();
|
||||
let defined_names = &self.workbook.defined_names;
|
||||
let sheet_id = match scope {
|
||||
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
|
||||
None => None,
|
||||
};
|
||||
// if the defined name already exist return error
|
||||
for df in defined_names {
|
||||
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
|
||||
return Err("Defined name already exists".to_string());
|
||||
}
|
||||
}
|
||||
let sheet_id = self.is_valid_defined_name(name, scope, formula)?;
|
||||
self.workbook.defined_names.push(DefinedName {
|
||||
name: name.to_string(),
|
||||
formula: formula.to_string(),
|
||||
@@ -2093,6 +2079,48 @@ impl Model {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validates if a defined name can be created
|
||||
pub fn is_valid_defined_name(
|
||||
&self,
|
||||
name: &str,
|
||||
scope: Option<u32>,
|
||||
formula: &str,
|
||||
) -> Result<Option<u32>, String> {
|
||||
if !is_valid_identifier(name) {
|
||||
return Err("Name: Invalid defined name".to_string());
|
||||
}
|
||||
let name_upper = name.to_uppercase();
|
||||
let defined_names = &self.workbook.defined_names;
|
||||
let sheet_id = match scope {
|
||||
Some(index) => match self.workbook.worksheet(index) {
|
||||
Ok(ws) => Some(ws.sheet_id),
|
||||
Err(_) => return Err("Scope: Invalid sheet index".to_string()),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
// if the defined name already exist return error
|
||||
for df in defined_names {
|
||||
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
|
||||
return Err("Name: Defined name already exists".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the formula is valid
|
||||
match common::ParsedReference::parse_reference_formula(
|
||||
None,
|
||||
formula,
|
||||
&self.locale,
|
||||
|name| self.get_sheet_index_by_name(name),
|
||||
) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
return Err("Formula: Invalid defined name formula".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(sheet_id)
|
||||
}
|
||||
|
||||
/// Delete defined name of name and scope
|
||||
pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> {
|
||||
let name_upper = name.to_uppercase();
|
||||
@@ -2126,7 +2154,7 @@ impl Model {
|
||||
new_formula: &str,
|
||||
) -> Result<(), String> {
|
||||
if !is_valid_identifier(new_name) {
|
||||
return Err("Invalid defined name".to_string());
|
||||
return Err("Name: Invalid defined name".to_string());
|
||||
};
|
||||
let name_upper = name.to_uppercase();
|
||||
let new_name_upper = new_name.to_uppercase();
|
||||
@@ -2134,18 +2162,28 @@ impl Model {
|
||||
if name_upper != new_name_upper || scope != new_scope {
|
||||
for key in self.parsed_defined_names.keys() {
|
||||
if key.1.to_uppercase() == new_name_upper && key.0 == new_scope {
|
||||
return Err("Defined name already exists".to_string());
|
||||
return Err("Name: Defined name already exists".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
let defined_names = &self.workbook.defined_names;
|
||||
let sheet_id = match scope {
|
||||
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
|
||||
Some(index) => Some(
|
||||
self.workbook
|
||||
.worksheet(index)
|
||||
.map_err(|_| "Scope: Invalid sheet index")?
|
||||
.sheet_id,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let new_sheet_id = match new_scope {
|
||||
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
|
||||
Some(index) => Some(
|
||||
self.workbook
|
||||
.worksheet(index)
|
||||
.map_err(|_| "Scope: Invalid sheet index")?
|
||||
.sheet_id,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
|
||||
@@ -112,29 +112,36 @@ pub fn to_precision(value: f64, precision: usize) -> f64 {
|
||||
/// ```
|
||||
/// This intends to be equivalent to the js: `${parseFloat(value.toPrecision(precision)})`
|
||||
/// See ([ecma](https://tc39.es/ecma262/#sec-number.prototype.toprecision)).
|
||||
/// FIXME: There has to be a better algorithm :/
|
||||
pub fn to_excel_precision_str(value: f64) -> String {
|
||||
to_precision_str(value, 15)
|
||||
}
|
||||
|
||||
pub fn to_excel_precision(value: f64, precision: usize) -> f64 {
|
||||
if !value.is_finite() {
|
||||
return value;
|
||||
}
|
||||
|
||||
let s = format!("{:.*e}", precision.saturating_sub(1), value);
|
||||
s.parse::<f64>().unwrap_or(value)
|
||||
}
|
||||
|
||||
pub fn to_precision_str(value: f64, precision: usize) -> String {
|
||||
if value.is_infinite() {
|
||||
return "inf".to_string();
|
||||
if !value.is_finite() {
|
||||
if value.is_infinite() {
|
||||
return "inf".to_string();
|
||||
} else {
|
||||
return "NaN".to_string();
|
||||
}
|
||||
}
|
||||
if value.is_nan() {
|
||||
return "NaN".to_string();
|
||||
}
|
||||
let exponent = value.abs().log10().floor();
|
||||
let base = value / 10.0_f64.powf(exponent);
|
||||
let base = format!("{0:.1$}", base, precision - 1);
|
||||
let value = format!("{base}e{exponent}").parse::<f64>().unwrap_or({
|
||||
// TODO: do this in a way that does not require a possible error
|
||||
0.0
|
||||
});
|
||||
|
||||
let s = format!("{:.*e}", precision.saturating_sub(1), value);
|
||||
let parsed = s.parse::<f64>().unwrap_or(value);
|
||||
|
||||
// I would love to use the std library. There is not a speed concern here
|
||||
// problem is it doesn't do the right thing
|
||||
// Also ryu is my favorite _modern_ algorithm
|
||||
let mut buffer = ryu::Buffer::new();
|
||||
let text = buffer.format(value);
|
||||
let text = buffer.format(parsed);
|
||||
// The above algorithm converts 2 to 2.0 regrettably
|
||||
if let Some(stripped) = text.strip_suffix(".0") {
|
||||
return stripped.to_string();
|
||||
|
||||
@@ -133,6 +133,7 @@ fn fn_imcot() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "windows", ignore)]
|
||||
#[test]
|
||||
fn fn_imtan() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
@@ -45,6 +45,7 @@ mod test_set_user_input;
|
||||
mod test_sheet_markup;
|
||||
mod test_sheets;
|
||||
mod test_styles;
|
||||
mod test_sumsq;
|
||||
mod test_trigonometric;
|
||||
mod test_true_false;
|
||||
mod test_weekday_return_types;
|
||||
@@ -55,12 +56,18 @@ mod test_yearfrac_basis;
|
||||
pub(crate) mod util;
|
||||
|
||||
mod engineering;
|
||||
mod statistical;
|
||||
mod test_fn_offset;
|
||||
mod test_number_format;
|
||||
|
||||
mod test_arrays;
|
||||
mod test_combin_combina;
|
||||
mod test_escape_quotes;
|
||||
mod test_even_odd;
|
||||
mod test_exp_sign;
|
||||
mod test_extend;
|
||||
mod test_floor;
|
||||
mod test_fn_datevalue_timevalue;
|
||||
mod test_fn_fv;
|
||||
mod test_fn_round;
|
||||
mod test_fn_type;
|
||||
@@ -73,10 +80,13 @@ mod test_issue_483;
|
||||
mod test_ln;
|
||||
mod test_log;
|
||||
mod test_log10;
|
||||
mod test_mod_quotient;
|
||||
mod test_networkdays;
|
||||
mod test_now;
|
||||
mod test_percentage;
|
||||
mod test_set_functions_error_handling;
|
||||
mod test_sheet_names;
|
||||
mod test_today;
|
||||
mod test_trigonometric_reciprocals;
|
||||
mod test_types;
|
||||
mod user_model;
|
||||
|
||||
24
base/src/test/statistical/mod.rs
Normal file
24
base/src/test/statistical/mod.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
mod test_fn_avedev;
|
||||
mod test_fn_binom;
|
||||
mod test_fn_chisq;
|
||||
mod test_fn_chisq_test;
|
||||
mod test_fn_confidence;
|
||||
mod test_fn_covariance;
|
||||
mod test_fn_devsq;
|
||||
mod test_fn_expon_dist;
|
||||
mod test_fn_f;
|
||||
mod test_fn_f_test;
|
||||
mod test_fn_fisher;
|
||||
mod test_fn_gauss;
|
||||
mod test_fn_hyp_geom_dist;
|
||||
mod test_fn_log_norm;
|
||||
mod test_fn_norm_dist;
|
||||
mod test_fn_pearson;
|
||||
mod test_fn_phi;
|
||||
mod test_fn_poisson;
|
||||
mod test_fn_stdev;
|
||||
mod test_fn_t_dist;
|
||||
mod test_fn_t_test;
|
||||
mod test_fn_var;
|
||||
mod test_fn_weibull;
|
||||
mod test_fn_z_test;
|
||||
40
base/src/test/statistical/test_fn_avedev.rs
Normal file
40
base/src/test/statistical/test_fn_avedev.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn smoke_test() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=STDEV.P(10, 12, 23, 23, 16, 23, 21)");
|
||||
model._set("A2", "=STDEV.S(10, 12, 23, 23, 16, 23, 21)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"5.174505793");
|
||||
|
||||
assert_eq!(model._get_text("A2"), *"5.589105048");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A2", "24");
|
||||
model._set("A3", "25");
|
||||
model._set("A4", "27");
|
||||
model._set("A5", "23");
|
||||
model._set("A6", "45");
|
||||
model._set("A7", "23.5");
|
||||
model._set("A8", "34");
|
||||
model._set("A9", "23");
|
||||
model._set("A10", "23");
|
||||
model._set("A11", "TRUE");
|
||||
model._set("A12", "'23");
|
||||
model._set("A13", "Text");
|
||||
model._set("A14", "FALSE");
|
||||
model._set("A15", "45");
|
||||
|
||||
model._set("B1", "=AVEDEV(A2:A15)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"7.25");
|
||||
}
|
||||
86
base/src/test/statistical/test_fn_binom.rs
Normal file
86
base/src/test/statistical/test_fn_binom.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_binom_dist_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=BINOM.DIST(6, 10, 0.5, TRUE)");
|
||||
model._set("A2", "=BINOM.DIST(6, 10, 0.5, FALSE)");
|
||||
model._set("A3", "=BINOM.DIST(6, 10, 0.5)"); // wrong args
|
||||
model._set("A4", "=BINOM.DIST(6, 10, 0.5, TRUE, FALSE)"); // too many args
|
||||
model.evaluate();
|
||||
|
||||
// P(X <= 6) for X ~ Bin(10, 0.5) = 0.828125
|
||||
assert_eq!(model._get_text("A1"), *"0.828125");
|
||||
|
||||
// P(X = 6) for X ~ Bin(10, 0.5) = 0.205078125
|
||||
assert_eq!(model._get_text("A2"), *"0.205078125");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_binom_dist_range_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=BINOM.DIST.RANGE(60, 0.75, 48)");
|
||||
model._set("A2", "=BINOM.DIST.RANGE(60, 0.75, 45, 50)");
|
||||
model._set("A3", "=BINOM.DIST.RANGE(60, 1.2, 45, 50)"); // p > 1 -> #NUM!
|
||||
model._set("A4", "=BINOM.DIST.RANGE(60, 0.75, 50, 45)"); // lower > upper -> #NUM!");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.083974967");
|
||||
|
||||
assert_eq!(model._get_text("A2"), *"0.523629793");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_binom_inv_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=BINOM.INV(6, 0.5, 0.75)");
|
||||
model._set("A2", "=BINOM.INV(6, 0.5, -0.1)"); // alpha < 0 -> #NUM!
|
||||
model._set("A3", "=BINOM.INV(6, 1.2, 0.75)"); // p > 1 -> #NUM!
|
||||
model._set("A4", "=BINOM.INV(6, 0.5)"); // args error
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"4");
|
||||
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A3"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_negbinom_dist_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid: PMF (non-cumulative) and CDF (cumulative)
|
||||
model._set("A1", "=NEGBINOM.DIST(10, 5, 0.25, FALSE)");
|
||||
model._set("A2", "=NEGBINOM.DIST(10, 5, 0.25, TRUE)");
|
||||
|
||||
// Wrong number of arguments -> #ERROR!
|
||||
model._set("A3", "=NEGBINOM.DIST(10, 5, 0.25)");
|
||||
model._set("A4", "=NEGBINOM.DIST(10, 5, 0.25, TRUE, FALSE)");
|
||||
|
||||
// Domain errors:
|
||||
// p < 0 or p > 1 -> #NUM!
|
||||
model._set("A5", "=NEGBINOM.DIST(10, 5, 1.5, TRUE)");
|
||||
// number_f < 0 -> #NUM!
|
||||
model._set("A6", "=NEGBINOM.DIST(-1, 5, 0.25, TRUE)");
|
||||
// number_s < 1 -> #NUM!
|
||||
model._set("A7", "=NEGBINOM.DIST(10, 0, 0.25, TRUE)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.05504866");
|
||||
assert_eq!(model._get_text("A2"), *"0.313514058");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||
}
|
||||
140
base/src/test/statistical/test_fn_chisq.rs
Normal file
140
base/src/test/statistical/test_fn_chisq.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_chisq_dist_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid: CDF
|
||||
model._set("A1", "=CHISQ.DIST(0.5, 4, TRUE)");
|
||||
|
||||
// Valid: PDF
|
||||
model._set("A2", "=CHISQ.DIST(0.5, 4, FALSE)");
|
||||
|
||||
// Valid: CDF with numeric cumulative (1 -> TRUE)
|
||||
model._set("A3", "=CHISQ.DIST(0.5, 4, 1)");
|
||||
|
||||
// Wrong number of args -> #ERROR!
|
||||
model._set("A4", "=CHISQ.DIST(0.5, 4)");
|
||||
model._set("A5", "=CHISQ.DIST(0.5, 4, TRUE, FALSE)");
|
||||
|
||||
// Domain errors
|
||||
// x < 0 -> #NUM!
|
||||
model._set("A6", "=CHISQ.DIST(-1, 4, TRUE)");
|
||||
// deg_freedom < 1 -> #NUM!
|
||||
model._set("A7", "=CHISQ.DIST(0.5, 0, TRUE)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// Values for df = 4
|
||||
// CDF(0.5) ≈ 0.026499021, PDF(0.5) ≈ 0.097350098
|
||||
assert_eq!(model._get_text("A1"), *"0.026499021");
|
||||
assert_eq!(model._get_text("A2"), *"0.097350098");
|
||||
assert_eq!(model._get_text("A3"), *"0.026499021");
|
||||
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_chisq_dist_rt_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid calls
|
||||
model._set("A1", "=CHISQ.DIST.RT(0.5, 4)");
|
||||
model._set("A2", "=CHISQ.DIST.RT(5, 4)");
|
||||
|
||||
// Too few / too many args -> #ERROR!
|
||||
model._set("A3", "=CHISQ.DIST.RT(0.5)");
|
||||
model._set("A4", "=CHISQ.DIST.RT(0.5, 4, 1)");
|
||||
|
||||
// Domain errors
|
||||
// x < 0 -> #NUM!
|
||||
model._set("A5", "=CHISQ.DIST.RT(-1, 4)");
|
||||
// deg_freedom < 1 -> #NUM!
|
||||
model._set("A6", "=CHISQ.DIST.RT(0.5, 0)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// For df = 4:
|
||||
// right tail at 0.5 ≈ 0.973500979
|
||||
// right tail at 5.0 ≈ 0.287297495
|
||||
assert_eq!(model._get_text("A1"), *"0.973500979");
|
||||
assert_eq!(model._get_text("A2"), *"0.287297495");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_chisq_inv_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid calls
|
||||
model._set("A1", "=CHISQ.INV(0.95, 4)");
|
||||
model._set("A2", "=CHISQ.INV(0.1, 10)");
|
||||
|
||||
// Wrong number of args -> #ERROR!
|
||||
model._set("A3", "=CHISQ.INV(0.95)");
|
||||
model._set("A4", "=CHISQ.INV(0.95, 4, 1)");
|
||||
|
||||
// Domain errors
|
||||
// probability < 0 or > 1 -> #NUM!
|
||||
model._set("A5", "=CHISQ.INV(-0.1, 4)");
|
||||
model._set("A6", "=CHISQ.INV(1.1, 4)");
|
||||
// deg_freedom < 1 -> #NUM!
|
||||
model._set("A7", "=CHISQ.INV(0.5, 0)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// Standard critical values:
|
||||
// CHISQ.INV(0.95, 4) ≈ 9.487729037
|
||||
// CHISQ.INV(0.1, 10) ≈ 4.865182052
|
||||
assert_eq!(model._get_text("A1"), *"9.487729037");
|
||||
assert_eq!(model._get_text("A2"), *"4.865182052");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_chisq_inv_rt_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid calls
|
||||
model._set("A1", "=CHISQ.INV.RT(0.05, 4)");
|
||||
model._set("A2", "=CHISQ.INV.RT(0.9, 10)");
|
||||
|
||||
// Wrong number of args -> #ERROR!
|
||||
model._set("A3", "=CHISQ.INV.RT(0.05)");
|
||||
model._set("A4", "=CHISQ.INV.RT(0.05, 4, 1)");
|
||||
|
||||
// Domain errors
|
||||
// probability < 0 or > 1 -> #NUM!
|
||||
model._set("A5", "=CHISQ.INV.RT(-0.1, 4)");
|
||||
model._set("A6", "=CHISQ.INV.RT(1.1, 4)");
|
||||
// deg_freedom < 1 -> #NUM!
|
||||
model._set("A7", "=CHISQ.INV.RT(0.5, 0)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// For chi-square:
|
||||
// CHISQ.INV.RT(0.05, 4) = CHISQ.INV(0.95, 4) ≈ 9.487729037
|
||||
// CHISQ.INV.RT(0.9, 10) = CHISQ.INV(0.1, 10) ≈ 4.865182052
|
||||
assert_eq!(model._get_text("A1"), *"9.487729037");
|
||||
assert_eq!(model._get_text("A2"), *"4.865182052");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||
}
|
||||
127
base/src/test/statistical/test_fn_chisq_test.rs
Normal file
127
base/src/test/statistical/test_fn_chisq_test.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_chisq_test_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A2", "48");
|
||||
model._set("A3", "32");
|
||||
model._set("A4", "12");
|
||||
model._set("A5", "1");
|
||||
model._set("A6", "'13");
|
||||
model._set("A7", "TRUE");
|
||||
model._set("A8", "1");
|
||||
model._set("A9", "13");
|
||||
model._set("A10", "15");
|
||||
|
||||
model._set("B2", "55");
|
||||
model._set("B3", "34");
|
||||
model._set("B4", "13");
|
||||
model._set("B5", "blah");
|
||||
model._set("B6", "13");
|
||||
model._set("B7", "1");
|
||||
model._set("B8", "TRUE");
|
||||
model._set("B9", "'14");
|
||||
model._set("B10", "16");
|
||||
|
||||
model._set("C1", "=CHISQ.TEST(A2:A10, B2:B10)");
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text("C1"), *"0.997129538");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrays() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A2", "TRUE");
|
||||
model._set("A3", "4");
|
||||
model._set("A4", "'3");
|
||||
model._set("B2", "2");
|
||||
model._set("B3", "2");
|
||||
model._set("B4", "2");
|
||||
model._set("C1", "=CHISQ.TEST(A2:A4, B2:B4)");
|
||||
|
||||
model._set("G5", "=CHISQ.TEST({TRUE,4,\"3\"}, {2,2,2})");
|
||||
|
||||
// 1D arrays with different shapes
|
||||
model._set("G6", "=CHISQ.TEST({1,2,3}, {3;3;4})");
|
||||
|
||||
// 2D array
|
||||
model._set("G7", "=CHISQ.TEST({1,2;3,4},{2,3;2,2})");
|
||||
|
||||
// 1D arrays with same shape
|
||||
model._set("G8", "=CHISQ.TEST({1,2,3,4}, {2,3,4,5})");
|
||||
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text("C1"), *"0.367879441");
|
||||
assert_eq!(model._get_text("G5"), *"0.367879441");
|
||||
|
||||
assert_eq!(model._get_text("G6"), *"0.383531573");
|
||||
|
||||
assert_eq!(model._get_text("G7"), *"0.067889155");
|
||||
|
||||
assert_eq!(model._get_text("G8"), *"0.733094495");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn more_arrays() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("V20", "2");
|
||||
model._set("V21", "4");
|
||||
model._set("W20", "3");
|
||||
model._set("W21", "5");
|
||||
model._set("C1", "=CHISQ.TEST({1,2;3,4},V20:W21)");
|
||||
model._set("C2", "=CHISQ.TEST({1,2;3,4}, {2,3;4,5})");
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text("C1"), *"0.257280177");
|
||||
assert_eq!(model._get_text("C2"), *"0.257280177");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn array_ranges() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A2", "TRUE");
|
||||
model._set("A3", "4");
|
||||
model._set("A4", "'3");
|
||||
model._set("B2", "2");
|
||||
model._set("B3", "2");
|
||||
model._set("B4", "2");
|
||||
model._set("C1", "=CHISQ.TEST(A2:A4, {2;2;2})");
|
||||
|
||||
model._set("G5", "=CHISQ.TEST({TRUE;4;\"3\"}, B2:B4)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("C1"), *"0.367879441");
|
||||
assert_eq!(model._get_text("G5"), *"0.367879441");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn array_2d_ranges() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A2", "2");
|
||||
model._set("B2", "3");
|
||||
model._set("C2", "4");
|
||||
model._set("A3", "5");
|
||||
model._set("B3", "6");
|
||||
model._set("C3", "7");
|
||||
model._set("G1", "=CHISQ.TEST({1,2,3;4,2,6}, A2:C3)");
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text("G1"), *"0.129195493");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ranges_1d() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A2", "1");
|
||||
model._set("A3", "2");
|
||||
model._set("A4", "3");
|
||||
model._set("B2", "4");
|
||||
model._set("C2", "5");
|
||||
model._set("D2", "6");
|
||||
model._set("G1", "=CHISQ.TEST(A2:A4, B2:D2)");
|
||||
model._set("G2", "=CHISQ.TEST(B2:D2, A2:A4)");
|
||||
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text("G1"), *"0.062349477");
|
||||
assert_eq!(model._get_text("G2"), *"0.000261259");
|
||||
}
|
||||
51
base/src/test/statistical/test_fn_confidence.rs
Normal file
51
base/src/test/statistical/test_fn_confidence.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_confidence_norm_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A1", "=CONFIDENCE.NORM(0.05, 2.5, 50)");
|
||||
|
||||
// Some edge/error cases
|
||||
model._set("A2", "=CONFIDENCE.NORM(0, 2.5, 50)"); // alpha <= 0 -> #NUM!
|
||||
model._set("A3", "=CONFIDENCE.NORM(1, 2.5, 50)"); // alpha >= 1 -> #NUM!
|
||||
model._set("A4", "=CONFIDENCE.NORM(0.05, -1, 50)"); // std_dev <=0 -> #NUM!
|
||||
model._set("A5", "=CONFIDENCE.NORM(0.05, 2.5, 1)");
|
||||
model._set("A6", "=CONFIDENCE.NORM(0.05, 2.5, 0.99)"); // size < 1 -> #NUM!
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.692951912");
|
||||
|
||||
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A3"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A5"), *"4.899909961");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_confidence_t_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A1", "=CONFIDENCE.T(0.05, 50000, 100)");
|
||||
|
||||
// Some edge/error cases
|
||||
model._set("A2", "=CONFIDENCE.T(0, 50000, 100)"); // alpha <= 0 -> #NUM!
|
||||
model._set("A3", "=CONFIDENCE.T(1, 50000, 100)"); // alpha >= 1 -> #NUM!
|
||||
model._set("A4", "=CONFIDENCE.T(0.05, -1, 100)");
|
||||
model._set("A5", "=CONFIDENCE.T(0.05, 50000, 1)");
|
||||
model._set("A6", "=CONFIDENCE.T(0.05, 50000, 1.7)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"9921.08475793");
|
||||
|
||||
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A3"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A5"), *"#DIV/0!");
|
||||
assert_eq!(model._get_text("A6"), *"#DIV/0!");
|
||||
}
|
||||
57
base/src/test/statistical/test_fn_covariance.rs
Normal file
57
base/src/test/statistical/test_fn_covariance.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_covariance_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "3");
|
||||
model._set("A2", "9");
|
||||
model._set("A3", "2");
|
||||
model._set("A4", "7");
|
||||
model._set("A5", "4");
|
||||
model._set("A6", "12");
|
||||
|
||||
model._set("B1", "5");
|
||||
model._set("B2", "15");
|
||||
model._set("B3", "6");
|
||||
model._set("B4", "17");
|
||||
model._set("B5", "8");
|
||||
model._set("B6", "20");
|
||||
|
||||
model._set("C1", "=COVARIANCE.P(A1:A6, B1:B6)");
|
||||
model._set("C2", "=COVARIANCE.S(A1:A6, B1:B6)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("C1"), *"19.194444444");
|
||||
assert_eq!(model._get_text("C2"), *"23.033333333");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrays_mixed() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A2", "2");
|
||||
model._set("A3", "4");
|
||||
model._set("A4", "6");
|
||||
model._set("A5", "8");
|
||||
|
||||
model._set("B2", "1");
|
||||
model._set("B3", "3");
|
||||
model._set("B4", "5");
|
||||
model._set("B5", "7");
|
||||
|
||||
model._set("C1", "=COVARIANCE.P(A2:A5, {1,3,5,7})");
|
||||
model._set("C2", "=COVARIANCE.S(A2:A5, {1,3,5,7})");
|
||||
model._set("C3", "=COVARIANCE.P(A2:A5, B2:B5)");
|
||||
model._set("C4", "=COVARIANCE.S(A2:A5, B2:B5)");
|
||||
model._set("C5", "=COVARIANCE.P({2,4,6,8}, B2:B5)");
|
||||
model._set("C6", "=COVARIANCE.S({2,4,6,8}, B2:B5)");
|
||||
model._set("C7", "=COVARIANCE.P({2,4,6,8}, {1,3,5,7})");
|
||||
model._set("C8", "=COVARIANCE.S({2,4,6,8}, {1,3,5,7})");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("C1"), *"5");
|
||||
assert_eq!(model._get_text("C2"), *"6.666666667");
|
||||
}
|
||||
50
base/src/test/statistical/test_fn_devsq.rs
Normal file
50
base/src/test/statistical/test_fn_devsq.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments_smoke_test() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=DEVSQ()");
|
||||
model._set("A2", "=DEVSQ(1, 2, 3)");
|
||||
model._set("A3", "=DEVSQ(1, )");
|
||||
model._set("A4", "=DEVSQ(1, , 3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), *"2");
|
||||
assert_eq!(model._get_text("A3"), *"0");
|
||||
assert_eq!(model._get_text("A4"), *"2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ranges() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=DEVSQ(A2:A8)");
|
||||
model._set("A2", "4");
|
||||
model._set("A3", "5");
|
||||
model._set("A4", "8");
|
||||
model._set("A5", "7");
|
||||
model._set("A6", "11");
|
||||
model._set("A7", "4");
|
||||
model._set("A8", "3");
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text("A1"), *"48");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrays() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=DEVSQ({1, 2, 3})");
|
||||
model._set("A2", "=DEVSQ({1; 2; 3})");
|
||||
model._set("A3", "=DEVSQ({1, 2; 3, 4})");
|
||||
model._set("A4", "=DEVSQ({1, 2; 3, 4; 5, 6})");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"2");
|
||||
assert_eq!(model._get_text("A2"), *"2");
|
||||
assert_eq!(model._get_text("A3"), *"5");
|
||||
assert_eq!(model._get_text("A4"), *"17.5");
|
||||
}
|
||||
32
base/src/test/statistical/test_fn_expon_dist.rs
Normal file
32
base/src/test/statistical/test_fn_expon_dist.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_expon_dist_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// λ = 1, x = 0.5
|
||||
// CDF = 1 - e^-0.5 ≈ 0.393469340
|
||||
// PDF = e^-0.5 ≈ 0.606530660
|
||||
model._set("A1", "=EXPON.DIST(0.5, 1, TRUE)");
|
||||
model._set("A2", "=EXPON.DIST(0.5, 1, FALSE)");
|
||||
|
||||
// Wrong number of args
|
||||
model._set("A3", "=EXPON.DIST(0.5, 1)");
|
||||
model._set("A4", "=EXPON.DIST(0.5, 1, TRUE, FALSE)");
|
||||
|
||||
// Domain errors
|
||||
model._set("A5", "=EXPON.DIST(-1, 1, TRUE)"); // x < 0
|
||||
model._set("A6", "=EXPON.DIST(0.5, 0, TRUE)"); // lambda <= 0
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.39346934");
|
||||
assert_eq!(model._get_text("A2"), *"0.60653066");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
}
|
||||
75
base/src/test/statistical/test_fn_f.rs
Normal file
75
base/src/test/statistical/test_fn_f.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_f_dist_sanity() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=F.DIST(15, 6, 4, TRUE)");
|
||||
model._set("A2", "=F.DIST(15, 6, 4, FALSE)");
|
||||
model._set("A3", "=F.DIST(15, 6, 4)");
|
||||
model._set("A4", "=F.DIST(15, 6, 4, TRUE, FALSE)");
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text("A1"), *"0.989741952");
|
||||
assert_eq!(model._get_text("A2"), *"0.001271447");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_f_dist_rt_sanity() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid call
|
||||
model._set("A1", "=F.DIST.RT(15, 6, 4)");
|
||||
// Too few args
|
||||
model._set("A2", "=F.DIST.RT(15, 6)");
|
||||
// Too many args
|
||||
model._set("A3", "=F.DIST.RT(15, 6, 4, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.010258048");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_f_inv_sanity() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid call: left-tail inverse
|
||||
model._set("A1", "=F.INV(0.9897419523940, 6, 4)");
|
||||
|
||||
// Too many args
|
||||
model._set("A2", "=F.INV(0.5, 6, 4, 2)");
|
||||
|
||||
// Too few args
|
||||
model._set("A3", "=F.INV(0.5, 6)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"15");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_f_inv_rt_sanity() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid call: left-tail inverse
|
||||
model._set("A1", "=F.INV.RT(0.0102580476059808, 6, 4)");
|
||||
|
||||
// Too many args
|
||||
model._set("A2", "=F.INV.RT(0.5, 6, 4, 2)");
|
||||
|
||||
// Too few args
|
||||
model._set("A3", "=F.INV.RT(0.5, 6)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"15");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
}
|
||||
35
base/src/test/statistical/test_fn_f_test.rs
Normal file
35
base/src/test/statistical/test_fn_f_test.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_f_test_sanity() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid call
|
||||
model._set("A1", "=F.TEST(A2:A7, B2:B7)");
|
||||
model._set("A2", "9");
|
||||
model._set("A3", "12");
|
||||
model._set("A4", "14");
|
||||
model._set("A5", "16");
|
||||
model._set("A6", "18");
|
||||
model._set("A7", "20");
|
||||
model._set("B2", "11");
|
||||
model._set("B3", "10");
|
||||
model._set("B4", "15");
|
||||
model._set("B5", "17");
|
||||
model._set("B6", "19");
|
||||
model._set("B7", "21");
|
||||
|
||||
// Too few args
|
||||
model._set("A8", "=F.TEST(A2:A7)");
|
||||
|
||||
// Too many args
|
||||
model._set("A9", "=F.TEST(A2:A7, B2:B7, C2:C7)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.859284302");
|
||||
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A9"), *"#ERROR!");
|
||||
}
|
||||
53
base/src/test/statistical/test_fn_fisher.rs
Normal file
53
base/src/test/statistical/test_fn_fisher.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
#[test]
|
||||
fn test_fn_fisher_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid inputs
|
||||
model._set("A1", "=FISHER(0.1)");
|
||||
model._set("A2", "=FISHER(-0.5)");
|
||||
model._set("A3", "=FISHER(0.8)");
|
||||
|
||||
// Domain errors: x <= -1 or x >= 1 -> #NUM!
|
||||
model._set("A4", "=FISHER(1)");
|
||||
model._set("A5", "=FISHER(-1)");
|
||||
model._set("A6", "=FISHER(2)");
|
||||
|
||||
// Wrong number of arguments -> #ERROR!
|
||||
model._set("A7", "=FISHER(0.1, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.100335348");
|
||||
assert_eq!(model._get_text("A2"), *"-0.549306144");
|
||||
assert_eq!(model._get_text("A3"), *"1.098612289");
|
||||
|
||||
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
|
||||
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_fisher_inv_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid inputs
|
||||
model._set("A1", "=FISHERINV(-1.5)");
|
||||
model._set("A2", "=FISHERINV(0.5)");
|
||||
model._set("A3", "=FISHERINV(2)");
|
||||
|
||||
// Wrong number of arguments -> #ERROR!
|
||||
model._set("A4", "=FISHERINV(0.5, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"-0.905148254");
|
||||
assert_eq!(model._get_text("A2"), *"0.462117157");
|
||||
assert_eq!(model._get_text("A3"), *"0.96402758");
|
||||
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
}
|
||||
35
base/src/test/statistical/test_fn_gauss.rs
Normal file
35
base/src/test/statistical/test_fn_gauss.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_gauss_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=GAUSS(-3)");
|
||||
model._set("A2", "=GAUSS(-2.3)");
|
||||
model._set("A3", "=GAUSS(-1.7)");
|
||||
model._set("A4", "=GAUSS(0)");
|
||||
model._set("A5", "=GAUSS(0.5)");
|
||||
model._set("A6", "=GAUSS(1)");
|
||||
model._set("A7", "=GAUSS(1.3)");
|
||||
model._set("A8", "=GAUSS(3)");
|
||||
model._set("A9", "=GAUSS(4)");
|
||||
|
||||
model._set("G6", "=GAUSS()");
|
||||
model._set("G7", "=GAUSS(1, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"-0.498650102");
|
||||
assert_eq!(model._get_text("A2"), *"-0.48927589");
|
||||
assert_eq!(model._get_text("A3"), *"-0.455434537");
|
||||
assert_eq!(model._get_text("A4"), *"0");
|
||||
assert_eq!(model._get_text("A5"), *"0.191462461");
|
||||
assert_eq!(model._get_text("A6"), *"0.341344746");
|
||||
assert_eq!(model._get_text("A7"), *"0.403199515");
|
||||
assert_eq!(model._get_text("A8"), *"0.498650102");
|
||||
assert_eq!(model._get_text("A9"), *"0.499968329");
|
||||
|
||||
assert_eq!(model._get_text("G6"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("G7"), *"#ERROR!");
|
||||
}
|
||||
42
base/src/test/statistical/test_fn_hyp_geom_dist.rs
Normal file
42
base/src/test/statistical/test_fn_hyp_geom_dist.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_hyp_geom_dist_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid: PDF (non-cumulative)
|
||||
model._set("A1", "=HYPGEOM.DIST(1, 4, 12, 20, FALSE)");
|
||||
|
||||
// Valid: CDF (cumulative)
|
||||
model._set("A2", "=HYPGEOM.DIST(1, 4, 12, 20, TRUE)");
|
||||
|
||||
// Wrong number of arguments -> #ERROR!
|
||||
model._set("A3", "=HYPGEOM.DIST(1, 4, 12, 20)");
|
||||
model._set("A4", "=HYPGEOM.DIST(1, 4, 12, 20, TRUE, FALSE)");
|
||||
|
||||
// Domain errors:
|
||||
// sample_s > number_sample -> #NUM!
|
||||
model._set("A5", "=HYPGEOM.DIST(5, 4, 12, 20, TRUE)");
|
||||
|
||||
// population_s > number_pop -> #NUM!
|
||||
model._set("A6", "=HYPGEOM.DIST(1, 4, 25, 20, TRUE)");
|
||||
|
||||
// number_sample > number_pop -> #NUM!
|
||||
model._set("A7", "=HYPGEOM.DIST(1, 25, 12, 20, TRUE)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// PDF: P(X = 1)
|
||||
assert_eq!(model._get_text("A1"), *"0.13869969");
|
||||
|
||||
// CDF: P(X <= 1)
|
||||
assert_eq!(model._get_text("A2"), *"0.153147575");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||
}
|
||||
61
base/src/test/statistical/test_fn_log_norm.rs
Normal file
61
base/src/test/statistical/test_fn_log_norm.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_log_norm_dist_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid: CDF and PDF
|
||||
model._set("A1", "=LOGNORM.DIST(4, 3.5, 1.2, TRUE)");
|
||||
model._set("A2", "=LOGNORM.DIST(4, 3.5, 1.2, FALSE)");
|
||||
|
||||
// Wrong number of arguments -> #ERROR!
|
||||
model._set("A3", "=LOGNORM.DIST(4, 3.5, 1.2)");
|
||||
model._set("A4", "=LOGNORM.DIST(4, 3.5, 1.2, TRUE, FALSE)");
|
||||
|
||||
// Domain errors:
|
||||
// x <= 0 -> #NUM!
|
||||
model._set("A5", "=LOGNORM.DIST(0, 3.5, 1.2, TRUE)");
|
||||
// std_dev <= 0 -> #NUM!
|
||||
model._set("A6", "=LOGNORM.DIST(4, 3.5, 0, TRUE)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.039083556");
|
||||
assert_eq!(model._get_text("A2"), *"0.017617597");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_log_norm_inv_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid call
|
||||
model._set("A1", "=LOGNORM.INV(0.5, 3.5, 1.2)");
|
||||
|
||||
// Wrong number of arguments -> #ERROR!
|
||||
model._set("A2", "=LOGNORM.INV(0.5, 3.5)");
|
||||
model._set("A3", "=LOGNORM.INV(0.5, 3.5, 1.2, 0)");
|
||||
|
||||
// Domain errors:
|
||||
// probability <= 0 or >= 1 -> #NUM!
|
||||
model._set("A4", "=LOGNORM.INV(0, 3.5, 1.2)");
|
||||
model._set("A5", "=LOGNORM.INV(1, 3.5, 1.2)");
|
||||
// std_dev <= 0 -> #NUM!
|
||||
model._set("A6", "=LOGNORM.INV(0.5, 3.5, 0)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"33.115451959");
|
||||
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
}
|
||||
119
base/src/test/statistical/test_fn_norm_dist.rs
Normal file
119
base/src/test/statistical/test_fn_norm_dist.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_norm_dist_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid: standard normal as a special case
|
||||
model._set("A1", "=NORM.DIST(1, 0, 1, TRUE)");
|
||||
model._set("A2", "=NORM.DIST(1, 0, 1, FALSE)");
|
||||
|
||||
// Wrong number of arguments -> #ERROR!
|
||||
model._set("A3", "=NORM.DIST(1, 0, 1)");
|
||||
model._set("A4", "=NORM.DIST(1, 0, 1, TRUE, FALSE)");
|
||||
|
||||
// Domain errors: standard_dev <= 0 -> #NUM!
|
||||
model._set("A5", "=NORM.DIST(1, 0, 0, TRUE)");
|
||||
model._set("A6", "=NORM.DIST(1, 0, -1, TRUE)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.841344746");
|
||||
assert_eq!(model._get_text("A2"), *"0.241970725");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_norm_inv_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid: median of standard normal
|
||||
model._set("A1", "=NORM.INV(0.5, 0, 1)");
|
||||
|
||||
// Wrong number of arguments -> #ERROR!
|
||||
model._set("A2", "=NORM.INV(0.5, 0)");
|
||||
model._set("A3", "=NORM.INV(0.5, 0, 1, 0)");
|
||||
|
||||
// Domain errors:
|
||||
// probability <= 0 or >= 1 -> #NUM!
|
||||
model._set("A4", "=NORM.INV(0, 0, 1)");
|
||||
model._set("A5", "=NORM.INV(1, 0, 1)");
|
||||
// standard_dev <= 0 -> #NUM!
|
||||
model._set("A6", "=NORM.INV(0.5, 0, 0)");
|
||||
|
||||
model._set("A7", "=NORM.INV(0.7, 0.2, 1)");
|
||||
model._set("A8", "=NORM.INV(0.7, 0.2, 5)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0");
|
||||
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A7"), *"0.724400513");
|
||||
assert_eq!(model._get_text("A8"), *"2.822002564");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_norm_s_dist_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid: CDF and PDF at z = 0
|
||||
model._set("A1", "=NORM.S.DIST(0, TRUE)");
|
||||
model._set("A2", "=NORM.S.DIST(0, FALSE)");
|
||||
|
||||
// Wrong number of arguments -> #ERROR!
|
||||
model._set("A3", "=NORM.S.DIST(0)");
|
||||
model._set("A4", "=NORM.S.DIST(0, TRUE, FALSE)");
|
||||
|
||||
model._set("A5", "=NORM.S.DIST(0.2, FALSE)");
|
||||
model._set("A6", "=NORM.S.DIST(2.2, TRUE)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.5");
|
||||
assert_eq!(model._get_text("A2"), *"0.39894228");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A5"), *"0.391042694");
|
||||
assert_eq!(model._get_text("A6"), *"0.986096552");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_norm_s_inv_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid: symmetric points
|
||||
model._set("A1", "=NORM.S.INV(0.5)");
|
||||
model._set("A2", "=NORM.S.INV(0.841344746)");
|
||||
|
||||
// Wrong number of arguments -> #ERROR!
|
||||
model._set("A3", "=NORM.S.INV()");
|
||||
model._set("A4", "=NORM.S.INV(0.5, 0)");
|
||||
|
||||
// Domain errors: probability <= 0 or >= 1 -> #NUM!
|
||||
model._set("A5", "=NORM.S.INV(0)");
|
||||
model._set("A6", "=NORM.S.INV(1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0");
|
||||
// Approximately 1
|
||||
assert_eq!(model._get_text("A2"), *"1");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
}
|
||||
31
base/src/test/statistical/test_fn_pearson.rs
Normal file
31
base/src/test/statistical/test_fn_pearson.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_chisq_test_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A2", "48");
|
||||
model._set("A3", "32");
|
||||
model._set("A4", "12");
|
||||
model._set("A5", "1");
|
||||
model._set("A6", "'13");
|
||||
model._set("A7", "TRUE");
|
||||
model._set("A8", "1");
|
||||
model._set("A9", "13");
|
||||
model._set("A10", "15");
|
||||
|
||||
model._set("B2", "55");
|
||||
model._set("B3", "34");
|
||||
model._set("B4", "13");
|
||||
model._set("B5", "blah");
|
||||
model._set("B6", "13");
|
||||
model._set("B7", "1");
|
||||
model._set("B8", "TRUE");
|
||||
model._set("B9", "'14");
|
||||
model._set("B10", "16");
|
||||
|
||||
model._set("C1", "=PEARSON(A2:A10, B2:B10)");
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text("C1"), *"0.998381439");
|
||||
}
|
||||
26
base/src/test/statistical/test_fn_phi.rs
Normal file
26
base/src/test/statistical/test_fn_phi.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_phi_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A1", "=PHI(0)");
|
||||
model._set("A2", "=PHI(1)");
|
||||
model._set("A3", "=PHI(-1)");
|
||||
|
||||
// Wrong number of arguments -> #ERROR!
|
||||
model._set("A4", "=PHI()");
|
||||
model._set("A5", "=PHI(0, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// Standard values
|
||||
assert_eq!(model._get_text("A1"), *"0.39894228");
|
||||
assert_eq!(model._get_text("A2"), *"0.241970725");
|
||||
assert_eq!(model._get_text("A3"), *"0.241970725");
|
||||
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
}
|
||||
41
base/src/test/statistical/test_fn_poisson.rs
Normal file
41
base/src/test/statistical/test_fn_poisson.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_poisson_dist_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// λ = 2, x = 3
|
||||
// P(X = 3) ≈ 0.180447045
|
||||
// P(X <= 3) ≈ 0.857123461
|
||||
model._set("A1", "=POISSON.DIST(3, 2, FALSE)");
|
||||
model._set("A2", "=POISSON.DIST(3, 2, TRUE)");
|
||||
|
||||
// Wrong arg count
|
||||
model._set("A3", "=POISSON.DIST(3, 2)");
|
||||
model._set("A4", "=POISSON.DIST(3, 2, TRUE, FALSE)");
|
||||
|
||||
// Domain errors
|
||||
model._set("A5", "=POISSON.DIST(-1, 2, TRUE)"); // x < 0
|
||||
model._set("A6", "=POISSON.DIST(3, -2, TRUE)"); // mean < 0
|
||||
|
||||
// λ = 0 special cases
|
||||
model._set("A7", "=POISSON.DIST(0, 0, FALSE)"); // 1
|
||||
model._set("A8", "=POISSON.DIST(1, 0, FALSE)"); // 0
|
||||
model._set("A9", "=POISSON.DIST(5, 0, TRUE)"); // 1
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.180447044");
|
||||
assert_eq!(model._get_text("A2"), *"0.85712346");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
|
||||
assert_eq!(model._get_text("A7"), *"1");
|
||||
assert_eq!(model._get_text("A8"), *"0");
|
||||
assert_eq!(model._get_text("A9"), *"1");
|
||||
}
|
||||
46
base/src/test/statistical/test_fn_stdev.rs
Normal file
46
base/src/test/statistical/test_fn_stdev.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn smoke_test() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=STDEV.P(10, 12, 23, 23, 16, 23, 21)");
|
||||
model._set("A2", "=STDEV.S(10, 12, 23, 23, 16, 23, 21)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"5.174505793");
|
||||
|
||||
assert_eq!(model._get_text("A2"), *"5.589105048");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A2", "24");
|
||||
model._set("A3", "25");
|
||||
model._set("A4", "27");
|
||||
model._set("A5", "23");
|
||||
model._set("A6", "45");
|
||||
model._set("A7", "23.5");
|
||||
model._set("A8", "34");
|
||||
model._set("A9", "23");
|
||||
model._set("A10", "23");
|
||||
model._set("A11", "TRUE");
|
||||
model._set("A12", "'23");
|
||||
model._set("A13", "Text");
|
||||
model._set("A14", "FALSE");
|
||||
model._set("A15", "45");
|
||||
|
||||
model._set("B1", "=STDEV.P(A2:A15)");
|
||||
model._set("B2", "=STDEV.S(A2:A15)");
|
||||
model._set("B3", "=STDEVA(A2:A15)");
|
||||
model._set("B4", "=STDEVPA(A2:A15)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"8.483071378");
|
||||
assert_eq!(model._get_text("B2"), *"8.941942369");
|
||||
assert_eq!(model._get_text("B3"), *"15.499955689");
|
||||
assert_eq!(model._get_text("B4"), *"14.936131032");
|
||||
}
|
||||
160
base/src/test/statistical/test_fn_t_dist.rs
Normal file
160
base/src/test/statistical/test_fn_t_dist.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_t_dist_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid: cumulative (left-tail CDF)
|
||||
model._set("A1", "=T.DIST(2, 10, TRUE)");
|
||||
// Valid: probability density function (PDF)
|
||||
model._set("B1", "=T.DIST(2, 10, FALSE)");
|
||||
|
||||
// Wrong number of arguments
|
||||
model._set("A2", "=T.DIST(2, 10)");
|
||||
model._set("A3", "=T.DIST(2, 10, TRUE, FALSE)");
|
||||
|
||||
// Domain error: df < 1 -> #NUM!
|
||||
model._set("A4", "=T.DIST(2, 0, TRUE)");
|
||||
model._set("A5", "=T.DIST(2, -1, TRUE)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.963305983");
|
||||
assert_eq!(model._get_text("B1"), *"0.061145766");
|
||||
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_t_dist_rt_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid: right tail probability
|
||||
model._set("A1", "=T.DIST.RT(2, 10)");
|
||||
|
||||
// Wrong number of arguments
|
||||
model._set("A2", "=T.DIST.RT(2)");
|
||||
model._set("A3", "=T.DIST.RT(2, 10, TRUE)");
|
||||
|
||||
// Domain error: df < 1
|
||||
model._set("A4", "=T.DIST.RT(2, 0)");
|
||||
model._set("A5", "=T.DIST.RT(2, -1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.036694017");
|
||||
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_t_dist_2t_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid: two-tailed probability
|
||||
model._set("A1", "=T.DIST.2T(2, 10)");
|
||||
|
||||
// In the limit case of x = 0, the two-tailed probability is 1.0
|
||||
model._set("A4", "=T.DIST.2T(0, 10)");
|
||||
|
||||
// Wrong number of arguments
|
||||
model._set("A2", "=T.DIST.2T(2)");
|
||||
model._set("A3", "=T.DIST.2T(2, 10, TRUE)");
|
||||
|
||||
// Domain errors:
|
||||
// x < 0 -> #NUM!
|
||||
model._set("A5", "=T.DIST.2T(-0.001, 10)");
|
||||
// df < 1 -> #NUM!
|
||||
model._set("A6", "=T.DIST.2T(2, 0)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.073388035");
|
||||
assert_eq!(model._get_text("A4"), *"1");
|
||||
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_t_inv_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid: upper and lower tail
|
||||
model._set("A1", "=T.INV(0.95, 10)");
|
||||
model._set("A2", "=T.INV(0.05, 10)");
|
||||
// limit case:
|
||||
model._set("B2", "=T.INV(0.95, 1)");
|
||||
|
||||
// Wrong number of arguments
|
||||
model._set("A3", "=T.INV(0.95)");
|
||||
model._set("A4", "=T.INV(0.95, 10, 1)");
|
||||
|
||||
// Domain errors:
|
||||
// p <= 0 or >= 1
|
||||
model._set("A5", "=T.INV(0, 10)");
|
||||
model._set("A6", "=T.INV(1, 10)");
|
||||
// df < 1
|
||||
model._set("A7", "=T.INV(0.95, 0)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"1.812461123");
|
||||
assert_eq!(model._get_text("A2"), *"-1.812461123");
|
||||
assert_eq!(model._get_text("B2"), *"6.313751515");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fn_t_inv_2t_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid: two-tailed critical values
|
||||
model._set("A1", "=T.INV.2T(0.1, 10)");
|
||||
model._set("A2", "=T.INV.2T(0.05, 10)");
|
||||
|
||||
// p = 1 should give t = 0 (both tails outside are 1.0, so cut at the mean)
|
||||
model._set("A3", "=T.INV.2T(1, 10)");
|
||||
|
||||
model._set("A7", "=T.INV.2T(1.5, 10)");
|
||||
|
||||
// Wrong number of arguments
|
||||
model._set("A4", "=T.INV.2T(0.1)");
|
||||
model._set("A5", "=T.INV.2T(0.1, 10, 1)");
|
||||
|
||||
// Domain errors:
|
||||
// p <= 0 or p > 1
|
||||
model._set("A6", "=T.INV.2T(0, 10)");
|
||||
// df < 1
|
||||
model._set("A8", "=T.INV.2T(0.1, 0)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"1.812461123");
|
||||
assert_eq!(model._get_text("A2"), *"2.228138852");
|
||||
assert_eq!(model._get_text("A3"), *"0");
|
||||
|
||||
// NB: Excel returns -0.699812061 for T.INV.2T(1.5, 10)
|
||||
// which seems inconsistent with its documented behavior
|
||||
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A8"), *"#NUM!");
|
||||
}
|
||||
41
base/src/test/statistical/test_fn_t_test.rs
Normal file
41
base/src/test/statistical/test_fn_t_test.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use crate::test::util::new_empty_model;
|
||||
#[test]
|
||||
fn test_fn_t_test_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A2", "3");
|
||||
model._set("A3", "4");
|
||||
model._set("A4", "5");
|
||||
model._set("A5", "6");
|
||||
model._set("A6", "10");
|
||||
model._set("A7", "3");
|
||||
model._set("A8", "2");
|
||||
model._set("A9", "4");
|
||||
model._set("A10", "7");
|
||||
|
||||
model._set("B2", "6");
|
||||
model._set("B3", "19");
|
||||
model._set("B4", "3");
|
||||
model._set("B5", "2");
|
||||
model._set("B6", "13");
|
||||
model._set("B7", "4");
|
||||
model._set("B8", "5");
|
||||
model._set("B9", "17");
|
||||
model._set("B10", "3");
|
||||
|
||||
model._set("C1", "=T.TEST(A2:A10, B2:B10, 1, 1)");
|
||||
model._set("C2", "=T.TEST(A2:A10, B2:B10, 1, 2)");
|
||||
model._set("C3", "=T.TEST(A2:A10, B2:B10, 1, 3)");
|
||||
model._set("C4", "=T.TEST(A2:A10, B2:B10, 2, 1)");
|
||||
model._set("C5", "=T.TEST(A2:A10, B2:B10, 2, 2)");
|
||||
model._set("C6", "=T.TEST(A2:A10, B2:B10, 2, 3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("C1"), *"0.103836888");
|
||||
assert_eq!(model._get_text("C2"), *"0.100244599");
|
||||
assert_eq!(model._get_text("C3"), *"0.105360319");
|
||||
assert_eq!(model._get_text("C4"), *"0.207673777");
|
||||
assert_eq!(model._get_text("C5"), *"0.200489197");
|
||||
assert_eq!(model._get_text("C6"), *"0.210720639");
|
||||
}
|
||||
46
base/src/test/statistical/test_fn_var.rs
Normal file
46
base/src/test/statistical/test_fn_var.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn smoke_test() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=STDEV.P(10, 12, 23, 23, 16, 23, 21)");
|
||||
model._set("A2", "=STDEV.S(10, 12, 23, 23, 16, 23, 21)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"5.174505793");
|
||||
|
||||
assert_eq!(model._get_text("A2"), *"5.589105048");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A2", "24");
|
||||
model._set("A3", "25");
|
||||
model._set("A4", "27");
|
||||
model._set("A5", "23");
|
||||
model._set("A6", "45");
|
||||
model._set("A7", "23.5");
|
||||
model._set("A8", "34");
|
||||
model._set("A9", "23");
|
||||
model._set("A10", "23");
|
||||
model._set("A11", "TRUE");
|
||||
model._set("A12", "'23");
|
||||
model._set("A13", "Text");
|
||||
model._set("A14", "FALSE");
|
||||
model._set("A15", "45");
|
||||
|
||||
model._set("B1", "=VAR.P(A2:A15)");
|
||||
model._set("B2", "=VAR.S(A2:A15)");
|
||||
model._set("B3", "=VARA(A2:A15)");
|
||||
model._set("B4", "=VARPA(A2:A15)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"71.9625");
|
||||
assert_eq!(model._get_text("B2"), *"79.958333333");
|
||||
assert_eq!(model._get_text("B3"), *"240.248626374");
|
||||
assert_eq!(model._get_text("B4"), *"223.088010204");
|
||||
}
|
||||
41
base/src/test/statistical/test_fn_weibull.rs
Normal file
41
base/src/test/statistical/test_fn_weibull.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_weibull_dist_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid: CDF and PDF for x = 1, alpha = 2, beta = 1
|
||||
model._set("A1", "=WEIBULL.DIST(1, 2, 1, TRUE)");
|
||||
model._set("A2", "=WEIBULL.DIST(1, 2, 1, FALSE)");
|
||||
|
||||
// Wrong number of arguments -> #ERROR!
|
||||
model._set("A3", "=WEIBULL.DIST(1, 2, 1)");
|
||||
model._set("A4", "=WEIBULL.DIST(1, 2, 1, TRUE, FALSE)");
|
||||
|
||||
// Domain errors:
|
||||
// x < 0 -> #NUM!
|
||||
model._set("A5", "=WEIBULL.DIST(-1, 2, 1, TRUE)");
|
||||
// alpha <= 0 -> #NUM!
|
||||
model._set("A6", "=WEIBULL.DIST(1, 0, 1, TRUE)");
|
||||
model._set("A7", "=WEIBULL.DIST(1, -1, 1, TRUE)");
|
||||
// beta <= 0 -> #NUM!
|
||||
model._set("A8", "=WEIBULL.DIST(1, 2, 0, TRUE)");
|
||||
model._set("A9", "=WEIBULL.DIST(1, 2, -1, TRUE)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// 1 - e^-1
|
||||
assert_eq!(model._get_text("A1"), *"0.632120559");
|
||||
// 2 * e^-1
|
||||
assert_eq!(model._get_text("A2"), *"0.735758882");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A7"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A8"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A9"), *"#NUM!");
|
||||
}
|
||||
36
base/src/test/statistical/test_fn_z_test.rs
Normal file
36
base/src/test/statistical/test_fn_z_test.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_z_test_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A2", "3");
|
||||
model._set("A3", "6");
|
||||
model._set("A4", "7");
|
||||
model._set("A5", "8");
|
||||
model._set("A6", "6");
|
||||
model._set("A7", "5");
|
||||
model._set("A8", "4");
|
||||
model._set("A9", "2");
|
||||
model._set("A10", "1");
|
||||
model._set("A11", "9");
|
||||
|
||||
model._set("G1", "=Z.TEST(A2:A11, 4)");
|
||||
model._set("G2", "=Z.TEST(A2:A11, 6)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("G1"), *"0.090574197");
|
||||
assert_eq!(model._get_text("G2"), *"0.863043389");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrays() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("D1", "=Z.TEST({5,2,3,4}, 4, 123)");
|
||||
model._set("D2", "=Z.TEST({5,2,3,4}, 4)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("D1"), *"0.503243397");
|
||||
assert_eq!(model._get_text("D2"), *"0.780710987");
|
||||
}
|
||||
27
base/src/test/test_arabic_roman
Normal file
27
base/src/test/test_arabic_roman
Normal file
@@ -0,0 +1,27 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=ARABIC()");
|
||||
model._set("A2", "=ARABIC(V)");
|
||||
model._set("A3", "=ARABIC(V, 2)");
|
||||
|
||||
model._set("A4", "=ROMAN()");
|
||||
model._set("A5", "=ROMAN(5)");
|
||||
model._set("A6", "=ROMAN(5, 0)");
|
||||
model._set("A7", "=ROMAN(5, 0, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), *"5");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"V");
|
||||
assert_eq!(model._get_text("A6"), *"V");
|
||||
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||
}
|
||||
35
base/src/test/test_cell_info_n_sheets
Normal file
35
base/src/test/test_cell_info_n_sheets
Normal file
@@ -0,0 +1,35 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=CELL("address",A1)");
|
||||
model._set("A2", "=CELL()");
|
||||
|
||||
model._set("A3", "=INFO("system")");
|
||||
model._set("A4", "=INFO()");
|
||||
|
||||
model._set("A5", "=N(TRUE)");
|
||||
model._set("A6", "=N()");
|
||||
model._set("A7", "=N(1, 2)");
|
||||
|
||||
model._set("A8", "=SHEETS()");
|
||||
model._set("A9", "=SHEETS(1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"$A$1");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#N/IMPL!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A5"), *"1");
|
||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A8"), *"1");
|
||||
assert_eq!(model._get_text("A9"), *"#N/IMPL!");
|
||||
}
|
||||
27
base/src/test/test_combin_combina.rs
Normal file
27
base/src/test/test_combin_combina.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=COMBIN(5,2)");
|
||||
model._set("A2", "=COMBINA(5,2)");
|
||||
model._set("A3", "=COMBIN()");
|
||||
model._set("A4", "=COMBINA()");
|
||||
model._set("A5", "=COMBIN(2)");
|
||||
model._set("A6", "=COMBINA(2)");
|
||||
model._set("A7", "=COMBIN(1, 2, 3)");
|
||||
model._set("A8", "=COMBINA(1, 2, 3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"10");
|
||||
assert_eq!(model._get_text("A2"), *"15");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||
}
|
||||
@@ -542,7 +542,6 @@ fn test_yearfrac_function() {
|
||||
|
||||
// Edge cases
|
||||
model._set("A4", "=YEARFRAC(44561,44561,1)"); // Same date = 0
|
||||
model._set("A5", "=YEARFRAC(44926,44561,1)"); // Reverse = negative
|
||||
model._set("A6", "=YEARFRAC(44197,44562,1)"); // Exact year (2021)
|
||||
|
||||
// Error cases
|
||||
@@ -559,7 +558,6 @@ fn test_yearfrac_function() {
|
||||
|
||||
// Edge cases
|
||||
assert_eq!(model._get_text("A4"), *"0"); // Same date
|
||||
assert_eq!(model._get_text("A5"), *"-1"); // Negative
|
||||
assert_eq!(model._get_text("A6"), *"1"); // Exact year
|
||||
|
||||
// Error cases
|
||||
|
||||
26
base/src/test/test_exp_sign.rs
Normal file
26
base/src/test/test_exp_sign.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn fn_arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=EXP()");
|
||||
model._set("A2", "=SIGN()");
|
||||
|
||||
model._set("A3", "=EXP(0)");
|
||||
model._set("A4", "=SIGN(-10)");
|
||||
|
||||
model._set("A5", "=EXP(1, 2)");
|
||||
model._set("A6", "=SIGN(1, 2)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"1");
|
||||
assert_eq!(model._get_text("A4"), *"-1");
|
||||
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||
}
|
||||
123
base/src/test/test_floor.rs
Normal file
123
base/src/test/test_floor.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_floor_floating_point_precision() {
|
||||
// This test specifically checks the floating-point precision bug fix
|
||||
// Bug: FLOOR(7.1, 0.1) was returning 7.0 instead of 7.1
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// FLOOR tests
|
||||
model._set("C5", "=FLOOR(7.1, 0.1)");
|
||||
model._set("H7", "=FLOOR(-7.1, -0.1)");
|
||||
|
||||
// FLOOR.PRECISE tests
|
||||
model._set("C53", "=FLOOR.PRECISE(7.1, 0.1)");
|
||||
model._set("H53", "=FLOOR.PRECISE(7.1, -0.1)");
|
||||
|
||||
// FLOOR.MATH tests
|
||||
model._set("C101", "=FLOOR.MATH(7.1, 0.1)");
|
||||
model._set("H101", "=FLOOR.MATH(7.1, -0.1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// All should return 7.1
|
||||
assert_eq!(model._get_text("C5"), *"7.1");
|
||||
assert_eq!(model._get_text("H7"), *"-7.1");
|
||||
assert_eq!(model._get_text("C53"), *"7.1");
|
||||
assert_eq!(model._get_text("H53"), *"7.1");
|
||||
assert_eq!(model._get_text("C101"), *"7.1");
|
||||
assert_eq!(model._get_text("H101"), *"7.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_additional_precision_cases() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=FLOOR(7.9, 0.1)");
|
||||
model._set("A2", "=FLOOR(2.6, 0.5)");
|
||||
model._set("A3", "=FLOOR(0.3, 0.1)"); // 0.1 + 0.2 type scenario
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"7.9");
|
||||
assert_eq!(model._get_text("A2"), *"2.5");
|
||||
assert_eq!(model._get_text("A3"), *"0.3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_basic_cases() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=FLOOR(3.7, 2)");
|
||||
model._set("A2", "=FLOOR(3.2, 1)");
|
||||
model._set("A3", "=FLOOR(10, 3)");
|
||||
model._set("A4", "=FLOOR(7, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"2");
|
||||
assert_eq!(model._get_text("A2"), *"3");
|
||||
assert_eq!(model._get_text("A3"), *"9");
|
||||
assert_eq!(model._get_text("A4"), *"6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_negative_numbers() {
|
||||
let mut model = new_empty_model();
|
||||
// Both negative: rounds toward zero
|
||||
model._set("A1", "=FLOOR(-2.5, -2)");
|
||||
model._set("A2", "=FLOOR(-11, -3)");
|
||||
|
||||
// Negative number, positive significance: rounds away from zero
|
||||
model._set("A3", "=FLOOR(-11, 3)");
|
||||
model._set("A4", "=FLOOR(-2.5, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"-2");
|
||||
assert_eq!(model._get_text("A2"), *"-9");
|
||||
assert_eq!(model._get_text("A3"), *"-12");
|
||||
assert_eq!(model._get_text("A4"), *"-4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_error_cases() {
|
||||
let mut model = new_empty_model();
|
||||
// Positive number with negative significance should error
|
||||
model._set("A1", "=FLOOR(2.5, -2)");
|
||||
model._set("A2", "=FLOOR(10, -3)");
|
||||
|
||||
// Division by zero
|
||||
model._set("A3", "=FLOOR(5, 0)");
|
||||
|
||||
// Wrong number of arguments
|
||||
model._set("A4", "=FLOOR(5)");
|
||||
model._set("A5", "=FLOOR(5, 1, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A3"), *"#DIV/0!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_edge_cases() {
|
||||
let mut model = new_empty_model();
|
||||
// Zero value
|
||||
model._set("A1", "=FLOOR(0, 5)");
|
||||
model._set("A2", "=FLOOR(0, 0)");
|
||||
|
||||
// Exact multiples
|
||||
model._set("A3", "=FLOOR(10, 5)");
|
||||
model._set("A4", "=FLOOR(9, 3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0");
|
||||
assert_eq!(model._get_text("A2"), *"0");
|
||||
assert_eq!(model._get_text("A3"), *"10");
|
||||
assert_eq!(model._get_text("A4"), *"9");
|
||||
}
|
||||
@@ -7,8 +7,8 @@ fn datevalue_timevalue_arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=DATEVALUE()");
|
||||
model._set("A2", "=TIMEVALUE()");
|
||||
model._set("A3", "=DATEVALUE("2000-01-01")")
|
||||
model._set("A4", "=TIMEVALUE("12:00:00")")
|
||||
model._set("A3", "=DATEVALUE(\"2000-01-01\")");
|
||||
model._set("A4", "=TIMEVALUE(\"12:00:00\")");
|
||||
model._set("A5", "=DATEVALUE(1,2)");
|
||||
model._set("A6", "=TIMEVALUE(1,2)");
|
||||
model.evaluate();
|
||||
@@ -20,5 +20,3 @@ fn datevalue_timevalue_arguments() {
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||
}
|
||||
|
||||
|
||||
|
||||
22
base/src/test/test_mod_quotient.rs
Normal file
22
base/src/test/test_mod_quotient.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=MOD(5,2)");
|
||||
model._set("A2", "=MOD()");
|
||||
model._set("A3", "=MOD(5, 2, 1)");
|
||||
model._set("A4", "=QUOTIENT(5, 2)");
|
||||
model._set("A5", "=QUOTIENT()");
|
||||
model._set("A6", "=QUOTIENT(5, 2, 1)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"1");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A4"), *"2");
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||
}
|
||||
40
base/src/test/test_mround_trunc_int
Normal file
40
base/src/test/test_mround_trunc_int
Normal file
@@ -0,0 +1,40 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=MROUND()");
|
||||
model._set("A2", "=MROUND(10)");
|
||||
model._set("A3", "=MROUND(10, 3)");
|
||||
model._set("A4", "=MROUND(10, 3, 1)");
|
||||
|
||||
model._set("A5", "=TRUNC()");
|
||||
model._set("A6", "=TRUNC(10)");
|
||||
model._set("A7", "=TRUNC(10.22, 1)");
|
||||
model._set("A8", "=TRUNC(10, 3, 1)");
|
||||
|
||||
model._set("A9", "=INT()");
|
||||
model._set("A10", "=INT(10.22)");
|
||||
model._set("A11", "=INT(10.22, 1)");
|
||||
model._set("A12", "=INT(10.22, 1, 2)");
|
||||
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"9");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A7"), *"10.2");
|
||||
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A9"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A10"), *"10");
|
||||
assert_eq!(model._get_text("A11"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A12"), *"#ERROR!");
|
||||
}
|
||||
39
base/src/test/test_now.rs
Normal file
39
base/src/test/test_now.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{mock_time, test::util::new_empty_model};
|
||||
|
||||
// 14:44 20 Mar 2023 Berlin
|
||||
const TIMESTAMP_2023: i64 = 1679319865208;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
mock_time::set_mock_time(TIMESTAMP_2023);
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A1", "=NOW(1, 1)");
|
||||
model._set("A2", "=NOW(\"Europe/Berlin\")");
|
||||
model._set("A3", "=NOW(\"faketimezone\")");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"#ERROR!",
|
||||
"Wrong number of arguments"
|
||||
);
|
||||
assert_eq!(model._get_text("A2"), *"20/03/2023 14:44:25");
|
||||
assert_eq!(
|
||||
model._get_text("A3"),
|
||||
"#VALUE!",
|
||||
"Invalid timezone: faketimezone"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_date_time() {
|
||||
mock_time::set_mock_time(TIMESTAMP_2023);
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=NOW()");
|
||||
model.evaluate();
|
||||
let text = model._get_text("A1");
|
||||
assert_eq!(text, *"20/03/2023 13:44:25");
|
||||
}
|
||||
@@ -8,6 +8,15 @@ fn test_simple_format() {
|
||||
assert_eq!(formatted.text, "2.3".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maximum_zeros() {
|
||||
let formatted = format_number(1.0 / 3.0, "#,##0.0000000000000000000", "en");
|
||||
assert_eq!(formatted.text, "0.3333333333333330000".to_string());
|
||||
|
||||
let formatted = format_number(1234.0 + 1.0 / 3.0, "#,##0.0000000000000000000", "en");
|
||||
assert_eq!(formatted.text, "1,234.3333333333300000000".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "not yet implemented"]
|
||||
fn test_wrong_locale() {
|
||||
|
||||
17
base/src/test/test_sumsq.rs
Normal file
17
base/src/test/test_sumsq.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=SUMSQ()");
|
||||
model._set("A2", "=SUMSQ(2)");
|
||||
model._set("A3", "=SUMSQ(1, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), *"4");
|
||||
assert_eq!(model._get_text("A3"), *"5");
|
||||
}
|
||||
@@ -33,7 +33,8 @@ fn now_basic_utc() {
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"20/03/2023");
|
||||
assert_eq!(model._get_text("A2"), *"45005.572511574");
|
||||
// 45005.572511574
|
||||
assert_eq!(model._get_text("A2"), *"20/03/2023 13:44:25");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -46,5 +47,5 @@ fn now_basic_europe_berlin() {
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"20/03/2023");
|
||||
// This is UTC + 1 hour: 45005.572511574 + 1/24
|
||||
assert_eq!(model._get_text("A2"), *"45005.614178241");
|
||||
assert_eq!(model._get_text("A2"), *"20/03/2023 14:44:25");
|
||||
}
|
||||
|
||||
@@ -96,3 +96,14 @@ fn test_fn_tan_pi2() {
|
||||
// This is consistent with IEEE 754 but inconsistent with Excel
|
||||
assert_eq!(model._get_text("A1"), *"1.63312E+16");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigonometric_identity() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=COTH(1)*CSCH(1)");
|
||||
model._set("A2", "=COSH(1)/(SINH(1))^2");
|
||||
model._set("A3", "=A1=A2");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"TRUE");
|
||||
}
|
||||
|
||||
53
base/src/test/test_trigonometric_reciprocals.rs
Normal file
53
base/src/test/test_trigonometric_reciprocals.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn fn_arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=CSC()");
|
||||
model._set("A2", "=SEC()");
|
||||
model._set("A3", "=COT()");
|
||||
|
||||
model._set("A4", "=CSCH()");
|
||||
model._set("A5", "=SECH()");
|
||||
model._set("A6", "=COTH()");
|
||||
|
||||
model._set("A7", "=ACOT()");
|
||||
model._set("A8", "=ACOTH()");
|
||||
|
||||
model._set("B1", "=CSC(1, 2)");
|
||||
model._set("B2", "=SEC(1, 2)");
|
||||
model._set("B3", "=COT(1, 2)");
|
||||
|
||||
model._set("B4", "=CSCH(1, 2)");
|
||||
model._set("B5", "=SECH(1, 2)");
|
||||
model._set("B6", "=COTH(1, 2)");
|
||||
|
||||
model._set("B7", "=ACOT(1, 2)");
|
||||
model._set("B8", "=ACOTH(1, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A3"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B3"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("B4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B5"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B6"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("B7"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B8"), *"#ERROR!");
|
||||
}
|
||||
@@ -26,8 +26,8 @@ fn test_yearfrac_basis_2_actual_360() {
|
||||
panic!("Expected numeric value in A2");
|
||||
}
|
||||
|
||||
// Negative symmetric of A1
|
||||
assert_eq!(model._get_text("A3"), *"-1");
|
||||
// always positive A1
|
||||
assert_eq!(model._get_text("A3"), *"1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -254,19 +254,19 @@ fn invalid_names() {
|
||||
// spaces
|
||||
assert_eq!(
|
||||
model.new_defined_name("A real", None, "Sheet1!$A$1"),
|
||||
Err("Invalid defined name".to_string())
|
||||
Err("Name: Invalid defined name".to_string())
|
||||
);
|
||||
|
||||
// Starts with number
|
||||
assert_eq!(
|
||||
model.new_defined_name("2real", None, "Sheet1!$A$1"),
|
||||
Err("Invalid defined name".to_string())
|
||||
Err("Name: Invalid defined name".to_string())
|
||||
);
|
||||
|
||||
// Updating also fails
|
||||
assert_eq!(
|
||||
model.update_defined_name("MyName", None, "My Name", None, "Sheet1!$A$1"),
|
||||
Err("Invalid defined name".to_string())
|
||||
Err("Name: Invalid defined name".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -284,13 +284,13 @@ fn already_existing() {
|
||||
// Can't create a new name with the same name
|
||||
assert_eq!(
|
||||
model.new_defined_name("MyName", None, "Sheet1!$A$2"),
|
||||
Err("Defined name already exists".to_string())
|
||||
Err("Name: Defined name already exists".to_string())
|
||||
);
|
||||
|
||||
// Can't update one into an existing
|
||||
assert_eq!(
|
||||
model.update_defined_name("Another", None, "MyName", None, "Sheet1!$A$1"),
|
||||
Err("Defined name already exists".to_string())
|
||||
Err("Name: Defined name already exists".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -304,17 +304,17 @@ fn invalid_sheet() {
|
||||
|
||||
assert_eq!(
|
||||
model.new_defined_name("Mything", Some(2), "Sheet1!$A$1"),
|
||||
Err("Invalid sheet index".to_string())
|
||||
Err("Scope: Invalid sheet index".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.update_defined_name("MyName", None, "MyName", Some(2), "Sheet1!$A$1"),
|
||||
Err("Invalid sheet index".to_string())
|
||||
Err("Scope: Invalid sheet index".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.update_defined_name("MyName", Some(9), "YourName", None, "Sheet1!$A$1"),
|
||||
Err("Invalid sheet index".to_string())
|
||||
Err("General: Failed to get old name".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ fn invalid_sheet() {
|
||||
fn invalid_formula() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.set_user_input(0, 1, 1, "Hello").unwrap();
|
||||
model.new_defined_name("MyName", None, "A1").unwrap();
|
||||
assert!(model.new_defined_name("MyName", None, "A1").is_err());
|
||||
|
||||
model.set_user_input(0, 1, 2, "=MyName").unwrap();
|
||||
|
||||
|
||||
@@ -445,11 +445,13 @@ impl Default for Fill {
|
||||
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[derive(Default)]
|
||||
pub enum HorizontalAlignment {
|
||||
Center,
|
||||
CenterContinuous,
|
||||
Distributed,
|
||||
Fill,
|
||||
#[default]
|
||||
General,
|
||||
Justify,
|
||||
Left,
|
||||
@@ -457,11 +459,6 @@ pub enum HorizontalAlignment {
|
||||
}
|
||||
|
||||
// Note that alignment in "General" depends on type
|
||||
impl Default for HorizontalAlignment {
|
||||
fn default() -> Self {
|
||||
Self::General
|
||||
}
|
||||
}
|
||||
|
||||
impl HorizontalAlignment {
|
||||
fn is_default(&self) -> bool {
|
||||
@@ -487,7 +484,9 @@ impl Display for HorizontalAlignment {
|
||||
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[derive(Default)]
|
||||
pub enum VerticalAlignment {
|
||||
#[default]
|
||||
Bottom,
|
||||
Center,
|
||||
Distributed,
|
||||
@@ -501,12 +500,6 @@ impl VerticalAlignment {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VerticalAlignment {
|
||||
fn default() -> Self {
|
||||
Self::Bottom
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for VerticalAlignment {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
|
||||
@@ -329,6 +329,7 @@ impl Model {
|
||||
Function::Tbillyield => self.units_fn_percentage_2(args, cell),
|
||||
Function::Date => self.units_fn_dates(args, cell),
|
||||
Function::Today => self.units_fn_dates(args, cell),
|
||||
Function::Now => self.units_fn_date_times(args, cell),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -375,4 +376,8 @@ impl Model {
|
||||
// TODO: update locale and use it here
|
||||
Some(Units::Date("dd/mm/yyyy".to_string()))
|
||||
}
|
||||
|
||||
fn units_fn_date_times(&self, _args: &[Node], _cell: &CellReferenceIndex) -> Option<Units> {
|
||||
Some(Units::Date("dd/mm/yyyy hh:mm:ss".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2001,7 +2001,10 @@ impl UserModel {
|
||||
new_scope: Option<u32>,
|
||||
new_formula: &str,
|
||||
) -> Result<(), String> {
|
||||
let old_formula = self.model.get_defined_name_formula(name, scope)?;
|
||||
let old_formula = self
|
||||
.model
|
||||
.get_defined_name_formula(name, scope)
|
||||
.map_err(|_| "General: Failed to get old name")?;
|
||||
let diff_list = vec![Diff::UpdateDefinedName {
|
||||
name: name.to_string(),
|
||||
scope,
|
||||
@@ -2017,6 +2020,16 @@ impl UserModel {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// validates a new defined name
|
||||
pub fn is_valid_defined_name(
|
||||
&self,
|
||||
name: &str,
|
||||
scope: Option<u32>,
|
||||
formula: &str,
|
||||
) -> Result<Option<u32>, String> {
|
||||
self.model.is_valid_defined_name(name, scope, formula)
|
||||
}
|
||||
|
||||
// **** Private methods ****** //
|
||||
|
||||
pub(crate) fn push_diff_list(&mut self, diff_list: DiffList) {
|
||||
|
||||
@@ -775,4 +775,17 @@ impl Model {
|
||||
.get_first_non_empty_in_row_after_column(sheet, row, column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "isValidDefinedName")]
|
||||
pub fn is_valid_defined_name(
|
||||
&self,
|
||||
name: &str,
|
||||
scope: Option<u32>,
|
||||
formula: &str,
|
||||
) -> Result<(), JsError> {
|
||||
match self.model.is_valid_defined_name(name, scope, formula) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(to_js_error(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2036,6 +2036,10 @@ export default defineConfig({
|
||||
text: "How to contribute",
|
||||
link: "/contributing/how-to-contribute",
|
||||
},
|
||||
{
|
||||
text: "Function documentation guide",
|
||||
link: "/contributing/function-documentation-guide",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
389
docs/src/contributing/function-documentation-guide.md
Normal file
389
docs/src/contributing/function-documentation-guide.md
Normal file
@@ -0,0 +1,389 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
lang: en-US
|
||||
---
|
||||
|
||||
# Function Documentation Guide
|
||||
|
||||
This guide explains how to document IronCalc functions following our established format and style conventions.
|
||||
|
||||
## File Structure
|
||||
|
||||
Function documentation files should be placed in the appropriate category directory under `src/functions/`. For example:
|
||||
|
||||
- Financial functions: `src/functions/financial/function-name.md`
|
||||
- Text functions: `src/functions/text/function-name.md`
|
||||
- Logical functions: `src/functions/logical/function-name.md`
|
||||
|
||||
## Required Frontmatter
|
||||
|
||||
Every function documentation file must start with this frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
lang: en-US
|
||||
---
|
||||
```
|
||||
|
||||
## Document Structure
|
||||
|
||||
A complete function documentation should include the following sections in order:
|
||||
|
||||
### 1. Title
|
||||
|
||||
The title should be the function name followed by the word "function":
|
||||
|
||||
```markdown
|
||||
# FV function
|
||||
```
|
||||
|
||||
The function name should be written in uppercase when mentioned in the documentation.
|
||||
|
||||
### 2. Draft Warning (Optional)
|
||||
|
||||
If the function hasn't been implemented, include this warning box:
|
||||
|
||||
```markdown
|
||||
::: warning
|
||||
**Note:** This draft page is under construction 🚧
|
||||
:::
|
||||
```
|
||||
|
||||
If the function has been implemented but not documented, include this warning box:
|
||||
|
||||
```markdown
|
||||
::: 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).
|
||||
:::
|
||||
```
|
||||
|
||||
### 3. Overview
|
||||
|
||||
Provide a brief, clear description of what the function does. If the function name is an acronym, expand it using underlined text:
|
||||
|
||||
```markdown
|
||||
## Overview
|
||||
|
||||
FV (<u>F</u>uture <u>V</u>alue) is a function of the Financial category that can be used to predict the future value of an investment or asset based on its present value.
|
||||
```
|
||||
|
||||
Include:
|
||||
|
||||
- Category (Financial, Text, Logical, etc.)
|
||||
- Primary purpose
|
||||
- Key use cases (if helpful)
|
||||
|
||||
### 4. Usage
|
||||
|
||||
This section contains multiple subsections:
|
||||
|
||||
#### 4.1 Syntax
|
||||
|
||||
Format the function syntax with color-coded argument types. Use the following color scheme:
|
||||
|
||||
- **Numbers**: `#2F80ED` (blue)
|
||||
- **Booleans**: `#27AE60` (green)
|
||||
- **Text/Strings**: `#2F80ED` (orange)
|
||||
- **Arrays/Ranges**: `#EB5757` (red)
|
||||
|
||||
**Format:**
|
||||
|
||||
```markdown
|
||||
### Syntax
|
||||
|
||||
**FUNCTION_NAME(<span title="Type" style="color:#HEXCODE">arg1</span>, <span title="Type" style="color:#HEXCODE">arg2</span>=default, ...) => <span title="ReturnType" style="color:#HEXCODE">return_value</span>**
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
**FV(<span title="Number" style="color:#2F80ED">rate</span>, <span title="Number" style="color:#2F80ED">nper</span>, <span title="Number" style="color:#2F80ED">pmt</span>, [<span title="Number" style="color:#2F80ED">pv</span>], [<span title="Boolean" style="color:#27AE60">type</span>] => <span title="Number" style="color:#2F80ED">fv</span>**
|
||||
|
||||
```markdown
|
||||
### Syntax
|
||||
|
||||
**FV(<span title="Number" style="color:#2F80ED">rate</span>, <span title="Number" style="color:#2F80ED">nper</span>, <span title="Number" style="color:#2F80ED">pmt</span>, <span title="Number" style="color:#2F80ED">pv</span>=0, <span title="Boolean" style="color:#27AE60">type</span>=FALSE) => <span title="Number" style="color:#2F80ED">fv</span>**
|
||||
```
|
||||
|
||||
**Guidelines:**
|
||||
|
||||
- Use `title` attribute to specify the data type
|
||||
- Use `style="color:#HEXCODE"` for syntax highlighting
|
||||
- Use square brackets for optional arguments
|
||||
- Show the return type after `=>`
|
||||
- Make the entire syntax **bold**
|
||||
|
||||
#### 4.2 Argument Descriptions
|
||||
|
||||
List each argument with:
|
||||
|
||||
- Argument name in _italics_
|
||||
- Data type link (e.g., `[number](/features/value-types#numbers)`)
|
||||
- Required or optional indicator
|
||||
- Description
|
||||
|
||||
**Format:**
|
||||
|
||||
```markdown
|
||||
### Argument descriptions
|
||||
|
||||
- _argname_ ([datatype](/features/value-types#datatype), [required|optional](/features/optional-arguments.md)). Description of the argument.
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```markdown
|
||||
### Argument descriptions
|
||||
|
||||
- _rate_ ([number](/features/value-types#numbers), required). The fixed percentage interest rate or yield per period.
|
||||
- _pv_ ([number](/features/value-types#numbers), [optional](/features/optional-arguments.md)). "pv" is the <u>p</u>resent <u>v</u>alue or starting amount of the asset (default 0).
|
||||
- _type_ ([Boolean](/features/value-types#booleans), [optional](/features/optional-arguments.md)). A logical value indicating whether the payment due dates are at the end (FALSE or 0) of the compounding periods or at the beginning (TRUE or any non-zero value). The default is FALSE when omitted.
|
||||
```
|
||||
|
||||
**Guidelines:**
|
||||
|
||||
- Use bullet points (`*`)
|
||||
- Italicize argument names with `*argname*`
|
||||
- Link to value types documentation
|
||||
- Link to optional arguments page when applicable
|
||||
- Expand acronyms in descriptions using `<u>` tags if helpful
|
||||
- Mention default values for optional arguments
|
||||
|
||||
#### 4.3 Additional Guidance
|
||||
|
||||
Provide tips, best practices and important notes about using the function:
|
||||
|
||||
```markdown
|
||||
### Additional guidance
|
||||
|
||||
- Make sure that the _rate_ argument specifies the interest rate or yield applicable to the compounding period.
|
||||
- The _pmt_ and _pv_ arguments should be expressed in the same currency unit.
|
||||
- To ensure a worthwhile result, one of the _pmt_ and _pv_ arguments should be non-zero.
|
||||
```
|
||||
|
||||
#### 4.4 Returned Value
|
||||
|
||||
Describe what the function returns:
|
||||
|
||||
```markdown
|
||||
### Returned value
|
||||
|
||||
FV returns a [number](/features/value-types#numbers) representing the future value expressed in the same [currency unit](/features/units) that was used for the _pmt_ and _pv_ arguments.
|
||||
```
|
||||
|
||||
Include:
|
||||
|
||||
- Return type (with link to value types if applicable)
|
||||
- Units or format if relevant
|
||||
- Any important characteristics
|
||||
|
||||
#### 4.5 Error Conditions
|
||||
|
||||
List all error scenarios the function may encounter:
|
||||
|
||||
```markdown
|
||||
### Error conditions
|
||||
|
||||
- In common with many other IronCalc functions, FV propagates errors that are found in any of its arguments.
|
||||
- If too few or too many arguments are supplied, FV returns the [`#ERROR!`](/features/error-types.md#error) error.
|
||||
- If the value of any of the _rate_, _nper_, _pmt_ or _pv_ arguments is not (or cannot be converted to) a [number](/features/value-types#numbers), then FV returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||
- If the value of the _type_ argument is not (or cannot be converted to) a [Boolean](/features/value-types#booleans), then FV again returns the [`#VALUE!`](/features/error-types.md#value) error.
|
||||
- For some combinations of valid argument values, FV may return a [`#NUM!`](/features/error-types.md#num) error or a [`#DIV/0!`](/features/error-types.md#div-0) error.
|
||||
```
|
||||
|
||||
**Guidelines:**
|
||||
|
||||
- Use bullet points
|
||||
- Format error types using backticks and link to the error types page: `` [`#ERROR!`](/features/error-types.md#error) ``
|
||||
- Reference argument names in italics when discussing specific arguments
|
||||
- Add the include directive at the end if using the error details snippet:
|
||||
|
||||
```markdown
|
||||
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||
```
|
||||
|
||||
### 5. Details (Optional but Recommended)
|
||||
|
||||
For functions with mathematical formulas or complex behavior, include a Details section. This section can also include plots, graphs or charts to help clarify the function's behavior.
|
||||
|
||||
```markdown
|
||||
## Details
|
||||
|
||||
- If $\text{type} \neq 0$, $\text{fv}$ is given by the equation:
|
||||
$$ \text{fv} = -\text{pv} \times (1 + \text{rate})^\text{nper} - \dfrac{\text{pmt}\times\big({(1+\text{rate})^\text{nper}-1}\big) \times(1+\text{rate})}{\text{rate}}$$
|
||||
|
||||
- If $\text{type} = 0$, $\text{fv}$ is given by the equation:
|
||||
$$ \text{fv} = -\text{pv} \times (1 + \text{rate})^{\text{nper}} - \dfrac{\text{pmt}\times\big({(1+\text{rate})^\text{nper}-1}\big)}{\text{rate}}$$
|
||||
```
|
||||
|
||||
**Guidelines:**
|
||||
|
||||
- Use LaTeX math notation with `$` for inline and `$$` for block equations
|
||||
- Use `\text{}` for variable names in equations
|
||||
- Explain special cases or edge conditions
|
||||
|
||||
### 6. Examples
|
||||
|
||||
Link to interactive examples in IronCalc:
|
||||
|
||||
```markdown
|
||||
## Examples
|
||||
|
||||
[See some examples in IronCalc](https://app.ironcalc.com/?example=functionname).
|
||||
```
|
||||
|
||||
Replace `functionname` with the actual function name (lowercase).
|
||||
|
||||
### 7. Links
|
||||
|
||||
Provide external references and related functions:
|
||||
|
||||
```markdown
|
||||
## Links
|
||||
|
||||
- For more information about the concept of "future value" in finance, visit Wikipedia's [Future value](https://en.wikipedia.org/wiki/Future_value) page.
|
||||
- See also IronCalc's [NPER](/functions/financial/nper), [PMT](/functions/financial/pmt), [PV](/functions/financial/pv) and [RATE](/functions/financial/rate) functions.
|
||||
- Visit Microsoft Excel's [FV function](https://support.microsoft.com/en-gb/office/fv-function-2eef9f44-a084-4c61-bdd8-4fe4bb1b71b3) page.
|
||||
- Both [Google Sheets](https://support.google.com/docs/answer/3093224) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/FV) provide versions of the FV function.
|
||||
```
|
||||
|
||||
**Guidelines:**
|
||||
|
||||
- Include Wikipedia links for concepts when available
|
||||
- Link to related IronCalc functions in the same category
|
||||
- Include links to equivalent functions in Excel, Google Sheets, and LibreOffice Calc
|
||||
- Use bullet points
|
||||
|
||||
## Syntax Coloring Reference
|
||||
|
||||
### Color Codes
|
||||
|
||||
| Data Type | Hex Color | Usage |
|
||||
| ----------- | --------- | --------------------------------------- |
|
||||
| Number | `#2F80ED` | All numeric arguments and return values |
|
||||
| Boolean | `#27AE60` | TRUE/FALSE arguments |
|
||||
| Text/String | `#F2994A` | Text arguments |
|
||||
| Array/Range | `#EB5757` | Array or range arguments |
|
||||
|
||||
### Syntax Highlighting Template
|
||||
|
||||
```html
|
||||
<span title="Type" style="color:#HEXCODE">argument_name</span>
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
- Number: `<span title="Number" style="color:#2F80ED">rate</span>`
|
||||
- Boolean: `<span title="Boolean" style="color:#27AE60">type</span>`
|
||||
- Text: `<span title="Text" style="color:#F2994A">text</span>`
|
||||
|
||||
## Formatting Conventions
|
||||
|
||||
### Text Formatting
|
||||
|
||||
- **Function names**: Use exact case as in IronCalc
|
||||
- **Argument names**: Use _italics_ when referencing in prose
|
||||
- **Acronyms**: Expand using `<u>` tags: `<u>F</u>uture <u>V</u>alue`
|
||||
- **Code/values**: Use backticks for error codes: `` `#ERROR!` ``
|
||||
- **Links**: Use descriptive link text, not raw URLs
|
||||
|
||||
### Section Headers
|
||||
|
||||
- Use `#` for the page title with the function name (e.g., FV Function)
|
||||
- Use `##` for main sections (Overview, Usage, Details, Examples, Links)
|
||||
- Use `###` for subsections (Syntax, Argument descriptions, etc.)
|
||||
|
||||
### Lists
|
||||
|
||||
- Use bullet points (`*`) for argument descriptions and error conditions
|
||||
- Use numbered lists only when order matters
|
||||
|
||||
## Checklist
|
||||
|
||||
Before submitting a function documentation, ensure:
|
||||
|
||||
- [ ] Frontmatter is correct
|
||||
- [ ] Title follows the format "FUNCTION_NAME function"
|
||||
- [ ] Overview clearly explains the function's purpose
|
||||
- [ ] Syntax is color-coded correctly
|
||||
- [ ] All arguments are documented with correct types
|
||||
- [ ] Required vs optional arguments are clearly marked
|
||||
- [ ] Return value is described
|
||||
- [ ] Error conditions are comprehensive
|
||||
- [ ] Examples link is included
|
||||
- [ ] Links section includes relevant references
|
||||
- [ ] Mathematical formulas (if any) use proper LaTeX syntax
|
||||
- [ ] All internal links use relative paths
|
||||
- [ ] Spelling and grammar are correct
|
||||
|
||||
## Example Template
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
lang: en-US
|
||||
---
|
||||
|
||||
# FUNCTION_NAME function
|
||||
|
||||
::: warning
|
||||
**Note:** This draft page is under construction 🚧
|
||||
:::
|
||||
|
||||
## Overview
|
||||
|
||||
FUNCTION_NAME (<u>A</u>cronym <u>E</u>xplanation) is a function of the [Category] category that can be used to [primary purpose].
|
||||
|
||||
[Additional context about when to use this function or related functions.]
|
||||
|
||||
## Usage
|
||||
|
||||
### Syntax
|
||||
|
||||
**FUNCTION_NAME(<span title="Type" style="color:#2F80ED">arg1</span>, [<span title="Type" style="color:#2F80ED">arg2</span>], [<span title="Boolean" style="color:#27AE60">arg3</span>]) => <span title="Type" style="color:#2F80ED">return_value</span>**
|
||||
|
||||
### Argument descriptions
|
||||
|
||||
- _arg1_ ([type](/features/value-types#type), required). Description.
|
||||
- _arg2_ ([type](/features/value-types#type), [optional](/features/optional-arguments.md)). Description (default value).
|
||||
- _arg3_ ([Boolean](/features/value-types#booleans), [optional](/features/optional-arguments.md)). Description (default FALSE).
|
||||
|
||||
### Additional guidance
|
||||
|
||||
- Tip or best practice.
|
||||
- Another important note.
|
||||
|
||||
### Returned value
|
||||
|
||||
FUNCTION_NAME returns a [type](/features/value-types#type) representing [description].
|
||||
|
||||
### Error conditions
|
||||
|
||||
- General error propagation note.
|
||||
- Specific error condition with [`#ERROR!`](/features/error-types.md#error) link.
|
||||
- Another error condition.
|
||||
|
||||
<!--@include: ../markdown-snippets/error-type-details.txt-->
|
||||
|
||||
## Details
|
||||
|
||||
[Mathematical formulas or detailed explanations if needed]
|
||||
|
||||
## Examples
|
||||
|
||||
[See some examples in IronCalc](https://app.ironcalc.com/?example=functionname).
|
||||
|
||||
## Links
|
||||
|
||||
- Wikipedia link if applicable.
|
||||
- Related IronCalc functions.
|
||||
- Microsoft Excel documentation.
|
||||
- Google Sheets and LibreOffice Calc links.
|
||||
```
|
||||
|
||||
## Questions?
|
||||
|
||||
If you have questions about documenting functions, reach out on our [Discord Channel](https://discord.com/invite/zZYWfh3RHJ) or check existing function documentation for examples.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user