Compare commits
53 Commits
dani/widge
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
203d640dbd | ||
|
|
ba75ffcf4f | ||
|
|
b5c977d3aa | ||
|
|
4029441cea | ||
|
|
cd47c609a0 | ||
|
|
ae6acdcdd5 | ||
|
|
c196db2115 | ||
|
|
a3c201e4e4 | ||
|
|
126e62957a | ||
|
|
294a651ae5 | ||
|
|
6f8a1e0da6 | ||
|
|
205ba6ee2d | ||
|
|
547b331773 | ||
|
|
f96612cf23 | ||
|
|
745435b950 | ||
|
|
4ca996cd3f | ||
|
|
3fbb91c414 | ||
|
|
93c9c42607 | ||
|
|
11edc2378e | ||
|
|
962e70c834 | ||
|
|
f803dad0a3 | ||
|
|
19580fc1ad | ||
|
|
e760b2d08e | ||
|
|
0e6ded7154 | ||
|
|
db26403432 | ||
|
|
9193479cce | ||
|
|
f814a75ae5 | ||
|
|
c8da5efb5f | ||
|
|
522e734395 | ||
|
|
2a7d59e512 | ||
|
|
c4142d4bf8 | ||
|
|
885d344b5b | ||
|
|
bed6f007cd | ||
|
|
dbd1b2df60 | ||
|
|
db552047c8 | ||
|
|
bcbacdb0a3 | ||
|
|
d0f37854d9 | ||
|
|
99b03f70c3 | ||
|
|
3e1605a494 | ||
|
|
d6aad08e73 | ||
|
|
8597d14a4e | ||
|
|
01b19b9c35 | ||
|
|
4649a0c78c | ||
|
|
cd0baf5ba7 | ||
|
|
167d169f1a | ||
|
|
080574b112 | ||
|
|
6056b8f122 | ||
|
|
e61b15655a | ||
|
|
6822505602 | ||
|
|
25f7891343 | ||
|
|
bdd0af0a39 | ||
|
|
261924396d | ||
|
|
67ef3bcf87 |
@@ -12,6 +12,9 @@ pub(crate) const DEFAULT_WINDOW_WIDTH: i64 = 800;
|
||||
pub(crate) const LAST_COLUMN: i32 = 16_384;
|
||||
pub(crate) const LAST_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,12 +872,10 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
||||
Function::Combin => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::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],
|
||||
@@ -889,6 +888,125 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
||||
Function::Dvar => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||
Function::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],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -914,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),
|
||||
@@ -961,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),
|
||||
@@ -990,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),
|
||||
@@ -1152,7 +1270,6 @@ 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),
|
||||
@@ -1165,5 +1282,81 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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;
|
||||
@@ -770,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
|
||||
@@ -978,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,
|
||||
@@ -989,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,
|
||||
|
||||
@@ -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,
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1209,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) {
|
||||
|
||||
230
base/src/functions/mathematical_sum.rs
Normal file
230
base/src/functions/mathematical_sum.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
type TwoMatricesResult = (i32, i32, Vec<Option<f64>>, Vec<Option<f64>>);
|
||||
|
||||
// Helper to check if two shapes are the same or compatible 1D shapes
|
||||
fn is_same_shape_or_1d(rows1: i32, cols1: i32, rows2: i32, cols2: i32) -> bool {
|
||||
(rows1 == rows2 && cols1 == cols2)
|
||||
|| (rows1 == 1 && cols2 == 1 && cols1 == rows2)
|
||||
|| (rows2 == 1 && cols1 == 1 && cols2 == rows1)
|
||||
}
|
||||
|
||||
impl Model {
|
||||
// SUMX2MY2(array_x, array_y) - Returns the sum of the difference of squares
|
||||
pub(crate) fn fn_sumx2my2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let result = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let (_, _, values_left, values_right) = result;
|
||||
|
||||
let mut sum = 0.0;
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
let x = x_opt.unwrap_or(0.0);
|
||||
let y = y_opt.unwrap_or(0.0);
|
||||
sum += x * x - y * y;
|
||||
}
|
||||
|
||||
CalcResult::Number(sum)
|
||||
}
|
||||
|
||||
// SUMX2PY2(array_x, array_y) - Returns the sum of the sum of squares
|
||||
pub(crate) fn fn_sumx2py2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let result = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let (_rows, _cols, values_left, values_right) = result;
|
||||
|
||||
let mut sum = 0.0;
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
let x = x_opt.unwrap_or(0.0);
|
||||
let y = y_opt.unwrap_or(0.0);
|
||||
sum += x * x + y * y;
|
||||
}
|
||||
|
||||
CalcResult::Number(sum)
|
||||
}
|
||||
|
||||
// SUMXMY2(array_x, array_y) - Returns the sum of squares of differences
|
||||
pub(crate) fn fn_sumxmy2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let result = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let (_, _, values_left, values_right) = result;
|
||||
|
||||
let mut sum = 0.0;
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
let x = x_opt.unwrap_or(0.0);
|
||||
let y = y_opt.unwrap_or(0.0);
|
||||
let diff = x - y;
|
||||
sum += diff * diff;
|
||||
}
|
||||
|
||||
CalcResult::Number(sum)
|
||||
}
|
||||
|
||||
// Helper function to extract and validate two matrices (ranges or arrays) with compatible shapes.
|
||||
// Returns (rows, cols, values_left, values_right) or an error.
|
||||
pub(crate) fn fn_get_two_matrices(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> Result<TwoMatricesResult, CalcResult> {
|
||||
if args.len() != 2 {
|
||||
return Err(CalcResult::new_args_number_error(cell));
|
||||
}
|
||||
let x_range = self.evaluate_node_in_context(&args[0], cell);
|
||||
let y_range = self.evaluate_node_in_context(&args[1], cell);
|
||||
|
||||
let result = match (x_range, y_range) {
|
||||
(
|
||||
CalcResult::Range {
|
||||
left: l1,
|
||||
right: r1,
|
||||
},
|
||||
CalcResult::Range {
|
||||
left: l2,
|
||||
right: r2,
|
||||
},
|
||||
) => {
|
||||
if l1.sheet != l2.sheet {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
));
|
||||
}
|
||||
let rows1 = r1.row - l1.row + 1;
|
||||
let cols1 = r1.column - l1.column + 1;
|
||||
let rows2 = r2.row - l2.row + 1;
|
||||
let cols2 = r2.column - l2.column + 1;
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = self.values_from_range(l1, r1)?;
|
||||
let values_right = self.values_from_range(l2, r2)?;
|
||||
(rows1, cols1, values_left, values_right)
|
||||
}
|
||||
(
|
||||
CalcResult::Array(left),
|
||||
CalcResult::Range {
|
||||
left: l2,
|
||||
right: r2,
|
||||
},
|
||||
) => {
|
||||
let rows2 = r2.row - l2.row + 1;
|
||||
let cols2 = r2.column - l2.column + 1;
|
||||
|
||||
let rows1 = left.len() as i32;
|
||||
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Array and range must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = match self.values_from_array(left) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in first array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
let values_right = self.values_from_range(l2, r2)?;
|
||||
(rows2, cols2, values_left, values_right)
|
||||
}
|
||||
(
|
||||
CalcResult::Range {
|
||||
left: l1,
|
||||
right: r1,
|
||||
},
|
||||
CalcResult::Array(right),
|
||||
) => {
|
||||
let rows1 = r1.row - l1.row + 1;
|
||||
let cols1 = r1.column - l1.column + 1;
|
||||
|
||||
let rows2 = right.len() as i32;
|
||||
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Range and array must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = self.values_from_range(l1, r1)?;
|
||||
let values_right = match self.values_from_array(right) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
(rows1, cols1, values_left, values_right)
|
||||
}
|
||||
(CalcResult::Array(left), CalcResult::Array(right)) => {
|
||||
let rows1 = left.len() as i32;
|
||||
let rows2 = right.len() as i32;
|
||||
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
|
||||
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
|
||||
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Arrays must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = match self.values_from_array(left) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in first array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
let values_right = match self.values_from_array(right) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
(rows1, cols1, values_left, values_right)
|
||||
}
|
||||
_ => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Both arguments must be ranges or arrays".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ mod lookup_and_reference;
|
||||
mod macros;
|
||||
mod math_util;
|
||||
mod mathematical;
|
||||
mod mathematical_sum;
|
||||
mod statistical;
|
||||
mod subtotal;
|
||||
mod text;
|
||||
@@ -76,6 +77,9 @@ pub enum Function {
|
||||
Sum,
|
||||
Sumif,
|
||||
Sumifs,
|
||||
Sumx2my2,
|
||||
Sumx2py2,
|
||||
Sumxmy2,
|
||||
Tan,
|
||||
Tanh,
|
||||
Acot,
|
||||
@@ -190,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,
|
||||
@@ -325,10 +415,16 @@ pub enum Function {
|
||||
Dvar,
|
||||
Dvarp,
|
||||
Dstdevp,
|
||||
|
||||
Correl,
|
||||
Rsq,
|
||||
Intercept,
|
||||
Slope,
|
||||
Steyx,
|
||||
}
|
||||
|
||||
impl Function {
|
||||
pub fn into_iter() -> IntoIter<Function, 268> {
|
||||
pub fn into_iter() -> IntoIter<Function, 345> {
|
||||
[
|
||||
Function::And,
|
||||
Function::False,
|
||||
@@ -405,6 +501,9 @@ impl Function {
|
||||
Function::Sum,
|
||||
Function::Sumif,
|
||||
Function::Sumifs,
|
||||
Function::Sumx2my2,
|
||||
Function::Sumx2py2,
|
||||
Function::Sumxmy2,
|
||||
Function::Choose,
|
||||
Function::Column,
|
||||
Function::Columns,
|
||||
@@ -453,6 +552,7 @@ impl Function {
|
||||
Function::Type,
|
||||
Function::Sheet,
|
||||
Function::Average,
|
||||
Function::Avedev,
|
||||
Function::Averagea,
|
||||
Function::Averageif,
|
||||
Function::Averageifs,
|
||||
@@ -598,6 +698,79 @@ impl Function {
|
||||
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()
|
||||
}
|
||||
@@ -659,6 +832,70 @@ impl Function {
|
||||
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(),
|
||||
}
|
||||
@@ -811,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),
|
||||
@@ -957,6 +1195,85 @@ impl Function {
|
||||
"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,
|
||||
}
|
||||
}
|
||||
@@ -1065,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"),
|
||||
@@ -1217,7 +1535,6 @@ impl fmt::Display for Function {
|
||||
Function::Combin => write!(f, "COMBIN"),
|
||||
Function::Combina => write!(f, "COMBINA"),
|
||||
Function::Sumsq => write!(f, "SUMSQ"),
|
||||
|
||||
Function::N => write!(f, "N"),
|
||||
Function::Cell => write!(f, "CELL"),
|
||||
Function::Info => write!(f, "INFO"),
|
||||
@@ -1234,6 +1551,83 @@ impl fmt::Display for Function {
|
||||
Function::Dvar => write!(f, "DVAR"),
|
||||
Function::Dvarp => write!(f, "DVARP"),
|
||||
Function::Dstdevp => write!(f, "DSTDEVP"),
|
||||
Function::BetaDist => write!(f, "BETA.DIST"),
|
||||
Function::BetaInv => write!(f, "BETA.INV"),
|
||||
Function::BinomDist => write!(f, "BINOM.DIST"),
|
||||
Function::BinomDistRange => write!(f, "BINOM.DIST.RANGE"),
|
||||
Function::BinomInv => write!(f, "BINOM.INV"),
|
||||
Function::ChisqDist => write!(f, "CHISQ.DIST"),
|
||||
Function::ChisqDistRT => write!(f, "CHISQ.DIST.RT"),
|
||||
Function::ChisqInv => write!(f, "CHISQ.INV"),
|
||||
Function::ChisqInvRT => write!(f, "CHISQ.INV.RT"),
|
||||
Function::ChisqTest => write!(f, "CHISQ.TEST"),
|
||||
Function::ConfidenceNorm => write!(f, "CONFIDENCE.NORM"),
|
||||
Function::ConfidenceT => write!(f, "CONFIDENCE.T"),
|
||||
Function::CovarianceP => write!(f, "COVARIANCE.P"),
|
||||
Function::CovarianceS => write!(f, "COVARIANCE.S"),
|
||||
Function::Devsq => write!(f, "DEVSQ"),
|
||||
Function::ExponDist => write!(f, "EXPON.DIST"),
|
||||
Function::FDist => write!(f, "F.DIST"),
|
||||
Function::FDistRT => write!(f, "F.DIST.RT"),
|
||||
Function::FInv => write!(f, "F.INV"),
|
||||
Function::FInvRT => write!(f, "F.INV.RT"),
|
||||
Function::Fisher => write!(f, "FISHER"),
|
||||
Function::FisherInv => write!(f, "FISHERINV"),
|
||||
Function::FTest => write!(f, "F.TEST"),
|
||||
Function::Gamma => write!(f, "GAMMA"),
|
||||
Function::GammaDist => write!(f, "GAMMA.DIST"),
|
||||
Function::GammaInv => write!(f, "GAMMA.INV"),
|
||||
Function::GammaLn => write!(f, "GAMMALN"),
|
||||
Function::GammaLnPrecise => write!(f, "GAMMALN.PRECISE"),
|
||||
Function::HypGeomDist => write!(f, "HYPGEOM.DIST"),
|
||||
Function::LogNormDist => write!(f, "LOGNORM.DIST"),
|
||||
Function::LogNormInv => write!(f, "LOGNORM.INV"),
|
||||
Function::NegbinomDist => write!(f, "NEGBINOM.DIST"),
|
||||
Function::NormDist => write!(f, "NORM.DIST"),
|
||||
Function::NormInv => write!(f, "NORM.INV"),
|
||||
Function::NormSdist => write!(f, "NORM.S.DIST"),
|
||||
Function::NormSInv => write!(f, "NORM.S.INV"),
|
||||
Function::Pearson => write!(f, "PEARSON"),
|
||||
Function::Phi => write!(f, "PHI"),
|
||||
Function::PoissonDist => write!(f, "POISSON.DIST"),
|
||||
Function::Standardize => write!(f, "STANDARDIZE"),
|
||||
Function::StDevP => write!(f, "STDEV.P"),
|
||||
Function::StDevS => write!(f, "STDEV.S"),
|
||||
Function::Stdeva => write!(f, "STDEVA"),
|
||||
Function::Stdevpa => write!(f, "STDEVPA"),
|
||||
Function::TDist => write!(f, "T.DIST"),
|
||||
Function::TDist2T => write!(f, "T.DIST.2T"),
|
||||
Function::TDistRT => write!(f, "T.DIST.RT"),
|
||||
Function::TInv => write!(f, "T.INV"),
|
||||
Function::TInv2T => write!(f, "T.INV.2T"),
|
||||
Function::TTest => write!(f, "T.TEST"),
|
||||
Function::VarP => write!(f, "VAR.P"),
|
||||
Function::VarS => write!(f, "VAR.S"),
|
||||
Function::VarpA => write!(f, "VARPA"),
|
||||
Function::VarA => write!(f, "VARA"),
|
||||
Function::WeibullDist => write!(f, "WEIBULL.DIST"),
|
||||
Function::ZTest => write!(f, "Z.TEST"),
|
||||
Function::Sumx2my2 => write!(f, "SUMX2MY2"),
|
||||
Function::Sumx2py2 => write!(f, "SUMX2PY2"),
|
||||
Function::Sumxmy2 => write!(f, "SUMXMY2"),
|
||||
Function::Correl => write!(f, "CORREL"),
|
||||
Function::Rsq => write!(f, "RSQ"),
|
||||
Function::Intercept => write!(f, "INTERCEPT"),
|
||||
Function::Slope => write!(f, "SLOPE"),
|
||||
Function::Steyx => write!(f, "STEYX"),
|
||||
// new ones
|
||||
Function::Gauss => write!(f, "GAUSS"),
|
||||
Function::Harmean => write!(f, "HARMEAN"),
|
||||
Function::Kurt => write!(f, "KURT"),
|
||||
Function::Large => write!(f, "LARGE"),
|
||||
Function::MaxA => write!(f, "MAXA"),
|
||||
Function::Median => write!(f, "MEDIAN"),
|
||||
Function::MinA => write!(f, "MINA"),
|
||||
Function::RankAvg => write!(f, "RANK.AVG"),
|
||||
Function::RankEq => write!(f, "RANK.EQ"),
|
||||
Function::Skew => write!(f, "SKEW"),
|
||||
Function::SkewP => write!(f, "SKEW.P"),
|
||||
Function::Small => write!(f, "SMALL"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1354,6 +1748,7 @@ impl Model {
|
||||
Function::Sheet => self.fn_sheet(args, cell),
|
||||
Function::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),
|
||||
@@ -1530,6 +1925,82 @@ impl Model {
|
||||
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,4 +1,5 @@
|
||||
mod test_actions;
|
||||
mod test_arabic_roman;
|
||||
mod test_binary_search;
|
||||
mod test_cell;
|
||||
mod test_cell_clear_contents;
|
||||
@@ -39,12 +40,14 @@ mod test_metadata;
|
||||
mod test_model_cell_clear_all;
|
||||
mod test_model_is_empty_cell;
|
||||
mod test_move_formula;
|
||||
mod test_mround_trunc_int;
|
||||
mod test_quote_prefix;
|
||||
mod test_row_column_styles;
|
||||
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 +58,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;
|
||||
@@ -80,5 +89,6 @@ 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");
|
||||
}
|
||||
@@ -6,8 +6,8 @@ use crate::test::util::new_empty_model;
|
||||
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("A2", "=ARABIC(\"V\")");
|
||||
model._set("A3", "=ARABIC(\"V\", 2)");
|
||||
|
||||
model._set("A4", "=ROMAN()");
|
||||
model._set("A5", "=ROMAN(5)");
|
||||
35
base/src/test/test_cell_info_n_sheets
Normal file
35
base/src/test/test_cell_info_n_sheets
Normal file
@@ -0,0 +1,35 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=CELL("address",A1)");
|
||||
model._set("A2", "=CELL()");
|
||||
|
||||
model._set("A3", "=INFO("system")");
|
||||
model._set("A4", "=INFO()");
|
||||
|
||||
model._set("A5", "=N(TRUE)");
|
||||
model._set("A6", "=N()");
|
||||
model._set("A7", "=N(1, 2)");
|
||||
|
||||
model._set("A8", "=SHEETS()");
|
||||
model._set("A9", "=SHEETS(1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"$A$1");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#N/IMPL!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A5"), *"1");
|
||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A8"), *"1");
|
||||
assert_eq!(model._get_text("A9"), *"#N/IMPL!");
|
||||
}
|
||||
@@ -11,8 +11,8 @@ fn arguments() {
|
||||
model._set("A4", "=COMBINA()");
|
||||
model._set("A5", "=COMBIN(2)");
|
||||
model._set("A6", "=COMBINA(2)");
|
||||
model._set("A5", "=COMBIN(1, 2, 3)");
|
||||
model._set("A6", "=COMBINA(1, 2, 3)");
|
||||
model._set("A7", "=COMBIN(1, 2, 3)");
|
||||
model._set("A8", "=COMBINA(1, 2, 3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
123
base/src/test/test_floor.rs
Normal file
123
base/src/test/test_floor.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_floor_floating_point_precision() {
|
||||
// This test specifically checks the floating-point precision bug fix
|
||||
// Bug: FLOOR(7.1, 0.1) was returning 7.0 instead of 7.1
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// FLOOR tests
|
||||
model._set("C5", "=FLOOR(7.1, 0.1)");
|
||||
model._set("H7", "=FLOOR(-7.1, -0.1)");
|
||||
|
||||
// FLOOR.PRECISE tests
|
||||
model._set("C53", "=FLOOR.PRECISE(7.1, 0.1)");
|
||||
model._set("H53", "=FLOOR.PRECISE(7.1, -0.1)");
|
||||
|
||||
// FLOOR.MATH tests
|
||||
model._set("C101", "=FLOOR.MATH(7.1, 0.1)");
|
||||
model._set("H101", "=FLOOR.MATH(7.1, -0.1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// All should return 7.1
|
||||
assert_eq!(model._get_text("C5"), *"7.1");
|
||||
assert_eq!(model._get_text("H7"), *"-7.1");
|
||||
assert_eq!(model._get_text("C53"), *"7.1");
|
||||
assert_eq!(model._get_text("H53"), *"7.1");
|
||||
assert_eq!(model._get_text("C101"), *"7.1");
|
||||
assert_eq!(model._get_text("H101"), *"7.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_additional_precision_cases() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=FLOOR(7.9, 0.1)");
|
||||
model._set("A2", "=FLOOR(2.6, 0.5)");
|
||||
model._set("A3", "=FLOOR(0.3, 0.1)"); // 0.1 + 0.2 type scenario
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"7.9");
|
||||
assert_eq!(model._get_text("A2"), *"2.5");
|
||||
assert_eq!(model._get_text("A3"), *"0.3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_basic_cases() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=FLOOR(3.7, 2)");
|
||||
model._set("A2", "=FLOOR(3.2, 1)");
|
||||
model._set("A3", "=FLOOR(10, 3)");
|
||||
model._set("A4", "=FLOOR(7, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"2");
|
||||
assert_eq!(model._get_text("A2"), *"3");
|
||||
assert_eq!(model._get_text("A3"), *"9");
|
||||
assert_eq!(model._get_text("A4"), *"6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_negative_numbers() {
|
||||
let mut model = new_empty_model();
|
||||
// Both negative: rounds toward zero
|
||||
model._set("A1", "=FLOOR(-2.5, -2)");
|
||||
model._set("A2", "=FLOOR(-11, -3)");
|
||||
|
||||
// Negative number, positive significance: rounds away from zero
|
||||
model._set("A3", "=FLOOR(-11, 3)");
|
||||
model._set("A4", "=FLOOR(-2.5, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"-2");
|
||||
assert_eq!(model._get_text("A2"), *"-9");
|
||||
assert_eq!(model._get_text("A3"), *"-12");
|
||||
assert_eq!(model._get_text("A4"), *"-4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_error_cases() {
|
||||
let mut model = new_empty_model();
|
||||
// Positive number with negative significance should error
|
||||
model._set("A1", "=FLOOR(2.5, -2)");
|
||||
model._set("A2", "=FLOOR(10, -3)");
|
||||
|
||||
// Division by zero
|
||||
model._set("A3", "=FLOOR(5, 0)");
|
||||
|
||||
// Wrong number of arguments
|
||||
model._set("A4", "=FLOOR(5)");
|
||||
model._set("A5", "=FLOOR(5, 1, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A3"), *"#DIV/0!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_edge_cases() {
|
||||
let mut model = new_empty_model();
|
||||
// Zero value
|
||||
model._set("A1", "=FLOOR(0, 5)");
|
||||
model._set("A2", "=FLOOR(0, 0)");
|
||||
|
||||
// Exact multiples
|
||||
model._set("A3", "=FLOOR(10, 5)");
|
||||
model._set("A4", "=FLOOR(9, 3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0");
|
||||
assert_eq!(model._get_text("A2"), *"0");
|
||||
assert_eq!(model._get_text("A3"), *"10");
|
||||
assert_eq!(model._get_text("A4"), *"9");
|
||||
}
|
||||
@@ -7,8 +7,8 @@ fn datevalue_timevalue_arguments() {
|
||||
let mut model = new_empty_model();
|
||||
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!");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ fn arguments() {
|
||||
model._set("A11", "=INT(10.22, 1)");
|
||||
model._set("A12", "=INT(10.22, 1, 2)");
|
||||
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
@@ -29,7 +28,7 @@ fn arguments() {
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A6"), *"10");
|
||||
assert_eq!(model._get_text("A7"), *"10.2");
|
||||
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||
|
||||
@@ -7,15 +7,24 @@ 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)");
|
||||
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!",
|
||||
"NOW should not accept arguments"
|
||||
"Wrong number of arguments"
|
||||
);
|
||||
assert_eq!(model._get_text("A2"), *"20/03/2023 14:44:25");
|
||||
assert_eq!(
|
||||
model._get_text("A3"),
|
||||
"#VALUE!",
|
||||
"Invalid timezone: faketimezone"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
17
base/src/test/test_sumsq.rs
Normal file
17
base/src/test/test_sumsq.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=SUMSQ()");
|
||||
model._set("A2", "=SUMSQ(2)");
|
||||
model._set("A3", "=SUMSQ(1, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), *"4");
|
||||
assert_eq!(model._get_text("A3"), *"5");
|
||||
}
|
||||
@@ -11,7 +11,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
||||
|
||||
| Function | Status | Documentation |
|
||||
| ---------- | ---------------------------------------------- | ------------- |
|
||||
| CELL | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| CELL | <Badge type="tip" text="Available" /> | – |
|
||||
| ERROR.TYPE | <Badge type="tip" text="Available" /> | – |
|
||||
| INFO | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| ISBLANK | <Badge type="tip" text="Available" /> | – |
|
||||
@@ -20,7 +20,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
||||
| ISEVEN | <Badge type="tip" text="Available" /> | – |
|
||||
| ISFORMULA | <Badge type="tip" text="Available" /> | – |
|
||||
| ISLOGICAL | <Badge type="tip" text="Available" /> | – |
|
||||
| ISNA | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| ISNA | <Badge type="tip" text="Available" /> | – |
|
||||
| ISNONTEXT | <Badge type="tip" text="Available" /> | – |
|
||||
| ISNUMBER | <Badge type="tip" text="Available" /> | – |
|
||||
| ISODD | <Badge type="tip" text="Available" /> | – |
|
||||
@@ -30,5 +30,5 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
||||
| N | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| NA | <Badge type="tip" text="Available" /> | – |
|
||||
| SHEET | <Badge type="tip" text="Available" /> | – |
|
||||
| SHEETS | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| SHEETS | <Badge type="tip" text="Available" /> | – |
|
||||
| TYPE | <Badge type="tip" text="Available" /> | – |
|
||||
|
||||
@@ -7,6 +7,5 @@ lang: en-US
|
||||
# CELL
|
||||
|
||||
::: warning
|
||||
🚧 This function is not yet available in IronCalc.
|
||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||
🚧 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).
|
||||
:::
|
||||
@@ -7,6 +7,5 @@ lang: en-US
|
||||
# N
|
||||
|
||||
::: warning
|
||||
🚧 This function is not yet available in IronCalc.
|
||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||
🚧 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).
|
||||
:::
|
||||
@@ -7,6 +7,5 @@ lang: en-US
|
||||
# SHEETS
|
||||
|
||||
::: warning
|
||||
🚧 This function is not yet available in IronCalc.
|
||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||
🚧 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).
|
||||
:::
|
||||
@@ -6,26 +6,33 @@ lang: en-US
|
||||
|
||||
# COLUMN function
|
||||
## Overview
|
||||
The COLUMN Function in IronCalc is a lookup & reference formula that is used to query and return the column number of a referenced Column or Cell.
|
||||
The COLUMN Function in IronCalc is a lookup & reference formula that is used to query and return the column number of a referenced column or cell.
|
||||
## Usage
|
||||
### Syntax
|
||||
**COLUMN(<span title="Reference" style="color:#1E88E5">reference</span>) => <span title="Number" style="color:#1E88E5">column</span>**
|
||||
### Argument descriptions
|
||||
* *reference* ([cell](/features/value-types#references), [optional](/features/optional-arguments.md)). The number of the cell you wish to reference the column number of.
|
||||
* *reference* ([cell](/features/value-types#references), [optional](/features/optional-arguments.md)). The cell, column, range, or [Named Range](/web-application/name-manager.html) for which you wish to find the column number.
|
||||
### Additional guidance
|
||||
* When referencing a range of cells, only the column number of the left most cell will be returned.
|
||||
* When referencing a range of cells, only the column number of the leftmost cell will be returned.
|
||||
* You are also able to reference complete columns instead of individual cells.
|
||||
* When using a Named Range as a reference, the reference is not case sensitive.
|
||||
* IronCalc supports the use of both *Absolute* ($A$1) and *Relative* (A1) references.
|
||||
* Cross-sheet references are also supported.
|
||||
### Returned value
|
||||
COLUMN returns the [number](/features/value-types#numbers) of the specific cell or column which is being referenced.
|
||||
COLUMN returns the [number](/features/value-types#numbers) of the specific cell or column which is being referenced. If no reference is included, the column number of the cell where the formula is entered will be returned.
|
||||
### Error conditions
|
||||
* IronCalc currently does not support the referencing of cells with names.
|
||||
* A [#NAME?](/features/error-types.html#name) error is returned if a Named Range being referenced is deleted.
|
||||
* A [#REF!](/features/error-types.html#ref) error is returned if a cell being referenced is deleted.
|
||||
* A [#VALUE!](/features/error-types.html#value) error is returned if a column being referenced is deleted.
|
||||
## Details
|
||||
The COLUMN Function can only be used to display the correlating number of a single column within a Sheet. If you wish to show the number of columns used within a specific range, you can use the COLUMNS Function.
|
||||
The COLUMN Function can only be used to display the correlating number of a single column within a Sheet. If you wish to show the number of columns used within a specific range, you can use the [COLUMNS](/functions/lookup_and_reference/columns) Function.
|
||||
## Examples
|
||||
### No Cell Reference
|
||||
When no cell reference is made, the formula uses **=COLUMN()**. This will then output the column number of the cell where the formula is placed.<br><br>For example, if the formula is placed in cell A1, then "1" will be displayed.
|
||||
When no cell reference is made, the formula uses **=COLUMN()**. This will output the column number of the cell where the formula is entered.<br><br>For example, if the formula is placed in cell A1, then "1" will be displayed.
|
||||
### With Cell Reference
|
||||
When a cell reference is made, the formula uses **=COLUMN([Referenced Cell])**. This will then output the column number of the referenced cell, regardless of where the formula is placed in the sheet.<br><br>For example, if the cell B1 is the referenced cell, "2" will be the output of the formula no matter where it is placed in the sheet.<br><br>**Note:** references do not always have to be specific cells, you can also reference complete columns. For example, **=COLUMN(B:B)** would also result in an output of "2".
|
||||
When a cell reference is made, the formula uses **=COLUMN(<span title="Reference" style="color:#1E88E5">Referenced Cell</span>)**. This will then output the column number of the referenced cell, regardless of where the formula is placed in the sheet.<br><br>For example, if B1 is the referenced cell, then "2" will be the output of the formula, regardless of where the formula is placed in the sheet.<br><br>**Note:** references do not have to be specific cells, you can also reference complete columns. For example, **=COLUMN(B:B)** would also result in an output of "2".
|
||||
### Range References
|
||||
The COLUMN function can also be used to reference a range of Cells or Columns. In this case only the most left-hand column will be the resulting output.<br><br>For example, **=COLUMN(A1:J1)** will result in the ouput of "1".
|
||||
The COLUMN function can also be used to reference a range of cells or columns. In this case only the leftmost column will be the resulting output.<br><br>For example, **=COLUMN(A1:J1)** will result in the output of "1".
|
||||
## Links
|
||||
* Visit Microsoft Excel's [Column function](https://support.microsoft.com/en-us/office/column-function-44e8c754-711c-4df3-9da4-47a55042554b) page.
|
||||
* Both [Google Sheets](https://support.google.com/docs/answer/3093373) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/COLUMN) provide versions of the COLUMN function.
|
||||
@@ -86,7 +86,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
||||
| SUMIF | <Badge type="tip" text="Available" /> | – |
|
||||
| SUMIFS | <Badge type="tip" text="Available" /> | – |
|
||||
| SUMPRODUCT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| SUMSQ | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| SUMSQ | <Badge type="tip" text="Available" /> | – |
|
||||
| SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| SUMX2PY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
|
||||
@@ -7,6 +7,5 @@ lang: en-US
|
||||
# SUMSQ
|
||||
|
||||
::: warning
|
||||
🚧 This function is not yet available in IronCalc.
|
||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||
🚧 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).
|
||||
:::
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Model } from "@ironcalc/wasm";
|
||||
import { styled } from "@mui/material";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Fx } from "../../icons";
|
||||
import { theme } from "../../theme";
|
||||
import { FORMULA_BAR_HEIGHT } from "../constants";
|
||||
@@ -9,6 +11,7 @@ import {
|
||||
ROW_HEIGH_SCALE,
|
||||
} from "../WorksheetCanvas/constants";
|
||||
import type { WorkbookState } from "../workbookState";
|
||||
import FormulaBarMenu from "./FormulaBarMenu";
|
||||
|
||||
type FormulaBarProps = {
|
||||
cellAddress: string;
|
||||
@@ -17,6 +20,8 @@ type FormulaBarProps = {
|
||||
workbookState: WorkbookState;
|
||||
onChange: () => void;
|
||||
onTextUpdated: () => void;
|
||||
openDrawer: () => void;
|
||||
canEdit: boolean;
|
||||
};
|
||||
|
||||
function FormulaBar(properties: FormulaBarProps) {
|
||||
@@ -28,10 +33,27 @@ function FormulaBar(properties: FormulaBarProps) {
|
||||
onTextUpdated,
|
||||
workbookState,
|
||||
} = properties;
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
const handleMenuOpenChange = (isOpen: boolean): void => {
|
||||
setIsMenuOpen(isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<AddressContainer>
|
||||
<CellBarAddress>{cellAddress}</CellBarAddress>
|
||||
<AddressContainer $active={isMenuOpen}>
|
||||
<FormulaBarMenu
|
||||
onMenuOpenChange={handleMenuOpenChange}
|
||||
openDrawer={properties.openDrawer}
|
||||
canEdit={properties.canEdit}
|
||||
model={model}
|
||||
onUpdate={onChange}
|
||||
>
|
||||
<CellBarAddress>{cellAddress}</CellBarAddress>
|
||||
<StyledIcon>
|
||||
<ChevronDown size={16} />
|
||||
</StyledIcon>
|
||||
</FormulaBarMenu>
|
||||
</AddressContainer>
|
||||
<Divider />
|
||||
<FormulaContainer>
|
||||
@@ -101,7 +123,7 @@ const Divider = styled("div")`
|
||||
background-color: ${theme.palette.grey["300"]};
|
||||
min-width: 1px;
|
||||
height: 16px;
|
||||
margin: 0px 16px;
|
||||
margin: 0px 16px 0px 8px;
|
||||
`;
|
||||
|
||||
const FormulaContainer = styled("div")`
|
||||
@@ -121,22 +143,46 @@ const Container = styled("div")`
|
||||
background: ${(properties): string =>
|
||||
properties.theme.palette.background.default};
|
||||
height: ${FORMULA_BAR_HEIGHT}px;
|
||||
border-top: 1px solid ${theme.palette.grey["300"]};
|
||||
`;
|
||||
|
||||
const AddressContainer = styled("div")`
|
||||
padding-left: 16px;
|
||||
const AddressContainer = styled("div")<{ $active?: boolean }>`
|
||||
color: ${theme.palette.common.black};
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
flex-grow: row;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
background-color: ${(props) =>
|
||||
props.$active ? theme.palette.action.selected : "transparent"};
|
||||
&:hover {
|
||||
background-color: ${(props) =>
|
||||
props.$active ? theme.palette.action.selected : theme.palette.grey["100"]};
|
||||
}
|
||||
`;
|
||||
|
||||
const CellBarAddress = styled("div")`
|
||||
width: 100%;
|
||||
text-align: "center";
|
||||
box-sizing: border-box;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding-left: 8px;
|
||||
background-color: transparent;
|
||||
`;
|
||||
|
||||
const StyledIcon = styled("div")`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 2px;
|
||||
background-color: transparent;
|
||||
`;
|
||||
|
||||
const EditorWrapper = styled("div")`
|
||||
|
||||
170
webapp/IronCalc/src/components/FormulaBar/FormulaBarMenu.tsx
Normal file
170
webapp/IronCalc/src/components/FormulaBar/FormulaBarMenu.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import type { Model } from "@ironcalc/wasm";
|
||||
import { Menu, MenuItem, styled } from "@mui/material";
|
||||
import { Tag } from "lucide-react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { theme } from "../../theme";
|
||||
import { parseRangeInSheet } from "../Editor/util";
|
||||
|
||||
type FormulaBarMenuProps = {
|
||||
children: React.ReactNode;
|
||||
onMenuOpenChange: (isOpen: boolean) => void;
|
||||
openDrawer: () => void;
|
||||
canEdit: boolean;
|
||||
model: Model;
|
||||
onUpdate: () => void;
|
||||
};
|
||||
|
||||
const FormulaBarMenu = (properties: FormulaBarMenuProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [isMenuOpen, setMenuOpen] = useState(false);
|
||||
const anchorElement = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMenuOpen = useCallback((): void => {
|
||||
setMenuOpen(true);
|
||||
properties.onMenuOpenChange(true);
|
||||
}, [properties.onMenuOpenChange]);
|
||||
|
||||
const handleMenuClose = useCallback((): void => {
|
||||
setMenuOpen(false);
|
||||
properties.onMenuOpenChange(false);
|
||||
}, [properties.onMenuOpenChange]);
|
||||
|
||||
const definedNameList = properties.model.getDefinedNameList();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChildrenWrapper onClick={handleMenuOpen} ref={anchorElement}>
|
||||
{properties.children}
|
||||
</ChildrenWrapper>
|
||||
<StyledMenu
|
||||
open={isMenuOpen}
|
||||
onClose={handleMenuClose}
|
||||
anchorEl={anchorElement.current}
|
||||
marginThreshold={0}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
{definedNameList.length > 0 ? (
|
||||
<>
|
||||
{definedNameList.map((definedName) => {
|
||||
return (
|
||||
<MenuItemWrapper
|
||||
key={`${definedName.name}-${definedName.scope}`}
|
||||
disableRipple
|
||||
onClick={() => {
|
||||
// select the area corresponding to the defined name
|
||||
const formula = definedName.formula;
|
||||
const range = parseRangeInSheet(properties.model, formula);
|
||||
if (range) {
|
||||
const [
|
||||
sheetIndex,
|
||||
rowStart,
|
||||
columnStart,
|
||||
rowEnd,
|
||||
columnEnd,
|
||||
] = range;
|
||||
properties.model.setSelectedSheet(sheetIndex);
|
||||
properties.model.setSelectedCell(rowStart, columnStart);
|
||||
properties.model.setSelectedRange(
|
||||
rowStart,
|
||||
columnStart,
|
||||
rowEnd,
|
||||
columnEnd,
|
||||
);
|
||||
}
|
||||
properties.onUpdate();
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
<Tag />
|
||||
<MenuItemText>{definedName.name}</MenuItemText>
|
||||
<MenuItemExample>{definedName.formula}</MenuItemExample>
|
||||
</MenuItemWrapper>
|
||||
);
|
||||
})}
|
||||
<MenuDivider />
|
||||
</>
|
||||
) : null}
|
||||
<MenuItemWrapper
|
||||
onClick={() => {
|
||||
properties.openDrawer();
|
||||
handleMenuClose();
|
||||
}}
|
||||
disabled={!properties.canEdit}
|
||||
disableRipple
|
||||
>
|
||||
<MenuItemText>{t("formula_bar.manage_named_ranges")}</MenuItemText>
|
||||
</MenuItemWrapper>
|
||||
</StyledMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledMenu = styled(Menu)`
|
||||
top: 4px;
|
||||
min-width: 260px;
|
||||
max-width: 460px;
|
||||
& .MuiPaper-root {
|
||||
border-radius: 8px;
|
||||
padding: 4px 0px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
& .MuiList-root {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const MenuItemWrapper = styled(MenuItem)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
gap: 8px;
|
||||
width: calc(100% - 8px);
|
||||
min-width: 172px;
|
||||
margin: 0px 4px;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
height: 32px;
|
||||
& svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
color: ${theme.palette.grey[600]};
|
||||
}
|
||||
`;
|
||||
|
||||
const ChildrenWrapper = styled("div")`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const MenuDivider = styled("div")`
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
border-top: 1px solid ${theme.palette.grey[200]};
|
||||
`;
|
||||
|
||||
const MenuItemText = styled("div")`
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: ${theme.palette.common.black};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const MenuItemExample = styled("div")`
|
||||
color: ${theme.palette.grey[400]};
|
||||
margin-left: 12px;
|
||||
`;
|
||||
|
||||
export default FormulaBarMenu;
|
||||
@@ -398,7 +398,9 @@ const ListItem = styled("div")<{ $isSelected: boolean }>(({ $isSelected }) => ({
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
gap: "8px",
|
||||
padding: "8px 12px",
|
||||
cursor: "pointer",
|
||||
minHeight: "40px",
|
||||
boxSizing: "border-box",
|
||||
borderBottom: `1px solid ${theme.palette.grey[200]}`,
|
||||
@@ -438,6 +440,8 @@ const NameText = styled("span")({
|
||||
fontSize: "12px",
|
||||
color: theme.palette.common.black,
|
||||
fontWeight: 600,
|
||||
wordBreak: "break-all",
|
||||
overflowWrap: "break-word",
|
||||
});
|
||||
|
||||
const IconsWrapper = styled("div")({
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { styled, Tooltip } from "@mui/material";
|
||||
import { EllipsisVertical, Menu, Plus } from "lucide-react";
|
||||
import { Menu, Plus } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IronCalcLogo } from "../../icons";
|
||||
import { theme } from "../../theme";
|
||||
import { NAVIGATION_HEIGHT } from "../constants";
|
||||
import { StyledButton } from "../Toolbar/Toolbar";
|
||||
import WorkbookSettingsDialog from "../WorkbookSettings/WorkbookSettingsDialog";
|
||||
import type { WorkbookState } from "../workbookState";
|
||||
import SheetListMenu from "./SheetListMenu";
|
||||
import SheetTab from "./SheetTab";
|
||||
@@ -22,16 +21,12 @@ export interface SheetTabBarProps {
|
||||
onSheetRenamed: (name: string) => void;
|
||||
onSheetDeleted: () => void;
|
||||
onHideSheet: () => void;
|
||||
onOpenWorkbookSettings: () => void;
|
||||
initialLocale: string;
|
||||
initialTimezone: string;
|
||||
}
|
||||
|
||||
function SheetTabBar(props: SheetTabBarProps) {
|
||||
const { t } = useTranslation();
|
||||
const { workbookState, onSheetSelected, sheets, selectedIndex } = props;
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
|
||||
const [workbookSettingsOpen, setWorkbookSettingsOpen] = useState(false);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
@@ -100,17 +95,6 @@ function SheetTabBar(props: SheetTabBarProps) {
|
||||
<IronCalcLogo />
|
||||
</LogoLink>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("workbook_settings.open_settings")}>
|
||||
<StyledButton
|
||||
$pressed={false}
|
||||
onClick={() => {
|
||||
setWorkbookSettingsOpen(true);
|
||||
props.onOpenWorkbookSettings();
|
||||
}}
|
||||
>
|
||||
<EllipsisVertical />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
</RightContainer>
|
||||
<SheetListMenu
|
||||
anchorEl={anchorEl}
|
||||
@@ -123,12 +107,6 @@ function SheetTabBar(props: SheetTabBarProps) {
|
||||
}}
|
||||
selectedIndex={selectedIndex}
|
||||
/>
|
||||
<WorkbookSettingsDialog
|
||||
open={workbookSettingsOpen}
|
||||
onClose={() => setWorkbookSettingsOpen(false)}
|
||||
initialLocale={props.initialLocale}
|
||||
initialTimezone={props.initialTimezone}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -192,28 +170,22 @@ const RightContainer = styled("a")`
|
||||
color: ${theme.palette.primary.main};
|
||||
height: 100%;
|
||||
padding: 0px 8px;
|
||||
gap: 4px;
|
||||
@media (max-width: 769px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const LogoLink = styled("div")`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0px 4px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
max-height: 24px;
|
||||
min-height: 24px;
|
||||
cursor: pointer;
|
||||
svg {
|
||||
height: 14px;
|
||||
width: auto;
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${theme.palette.grey["100"]};
|
||||
transition: "all 0.2s";
|
||||
outline: 1px solid ${theme.palette.grey["200"]};
|
||||
}
|
||||
@media (max-width: 769px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
Redo2,
|
||||
RemoveFormatting,
|
||||
Strikethrough,
|
||||
Tags,
|
||||
Type,
|
||||
Underline,
|
||||
Undo2,
|
||||
@@ -87,7 +86,6 @@ type ToolbarProperties = {
|
||||
numFmt: string;
|
||||
showGridLines: boolean;
|
||||
onToggleShowGridLines: (show: boolean) => void;
|
||||
openDrawer: () => void;
|
||||
};
|
||||
|
||||
function Toolbar(properties: ToolbarProperties) {
|
||||
@@ -514,18 +512,6 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
{properties.showGridLines ? <Grid2x2Check /> : <Grid2x2X />}
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.named_ranges")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={() => {
|
||||
properties.openDrawer();
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
<Tags />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.selected_png")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
|
||||
@@ -665,9 +665,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
model.setShowGridLines(sheet, show);
|
||||
setRedrawId((id) => id + 1);
|
||||
}}
|
||||
openDrawer={() => {
|
||||
setDrawerOpen(true);
|
||||
}}
|
||||
/>
|
||||
<WorksheetAreaLeft $drawerWidth={isDrawerOpen ? drawerWidth : 0}>
|
||||
<FormulaBar
|
||||
@@ -682,6 +679,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
}}
|
||||
model={model}
|
||||
workbookState={workbookState}
|
||||
openDrawer={() => {
|
||||
setDrawerOpen(true);
|
||||
}}
|
||||
canEdit={true}
|
||||
/>
|
||||
<Worksheet
|
||||
model={model}
|
||||
@@ -762,9 +763,9 @@ type WorksheetAreaLeftProps = { $drawerWidth: number };
|
||||
const WorksheetAreaLeft = styled("div")<WorksheetAreaLeftProps>(
|
||||
({ $drawerWidth }) => ({
|
||||
position: "absolute",
|
||||
top: `${TOOLBAR_HEIGHT + 1}px`,
|
||||
top: `${TOOLBAR_HEIGHT}px`,
|
||||
width: `calc(100% - ${$drawerWidth}px)`,
|
||||
height: `calc(100% - ${TOOLBAR_HEIGHT + 1}px)`,
|
||||
height: `calc(100% - ${TOOLBAR_HEIGHT}px)`,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,429 +0,0 @@
|
||||
import styled from "@emotion/styled";
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
Dialog,
|
||||
FormControl,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { Check, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { theme } from "../../theme";
|
||||
|
||||
type WorkbookSettingsDialogProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
initialLocale: string;
|
||||
initialTimezone: string;
|
||||
onSave?: (locale: string, timezone: string) => void;
|
||||
};
|
||||
|
||||
const WorkbookSettingsDialog = (properties: WorkbookSettingsDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
const locales = ["en-US", "en-GB", "de-DE", "fr-FR", "es-ES"];
|
||||
const timezones = [
|
||||
"Berlin, Germany (GMT+1)",
|
||||
"New York, USA (GMT-5)",
|
||||
"Tokyo, Japan (GMT+9)",
|
||||
"London, UK (GMT+0)",
|
||||
"Sydney, Australia (GMT+10)",
|
||||
];
|
||||
const [selectedLocale, setSelectedLocale] = useState<string>(
|
||||
properties.initialLocale && locales.includes(properties.initialLocale)
|
||||
? properties.initialLocale
|
||||
: locales[0],
|
||||
);
|
||||
const [selectedTimezone, setSelectedTimezone] = useState<string>(
|
||||
properties.initialTimezone && timezones.includes(properties.initialTimezone)
|
||||
? properties.initialTimezone
|
||||
: timezones[0],
|
||||
);
|
||||
|
||||
const handleSave = () => {
|
||||
if (properties.onSave && selectedLocale && selectedTimezone) {
|
||||
properties.onSave(selectedLocale, selectedTimezone);
|
||||
}
|
||||
properties.onClose();
|
||||
};
|
||||
|
||||
// Ensure selectedLocale is always a valid locale
|
||||
const validSelectedLocale =
|
||||
selectedLocale && locales.includes(selectedLocale)
|
||||
? selectedLocale
|
||||
: locales[0];
|
||||
|
||||
// Ensure selectedTimezone is always a valid timezone
|
||||
const validSelectedTimezone =
|
||||
selectedTimezone && timezones.includes(selectedTimezone)
|
||||
? selectedTimezone
|
||||
: timezones[0];
|
||||
|
||||
return (
|
||||
<StyledDialog
|
||||
open={properties.open}
|
||||
onClose={(_event, reason) => {
|
||||
if (reason === "backdropClick" || reason === "escapeKeyDown") {
|
||||
properties.onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<StyledDialogTitle>
|
||||
{t("workbook_settings.title")}
|
||||
<Cross
|
||||
onClick={properties.onClose}
|
||||
tabIndex={0}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
properties.onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<X />
|
||||
</Cross>
|
||||
</StyledDialogTitle>
|
||||
|
||||
<StyledDialogContent
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<StyledSectionTitle>
|
||||
{t("workbook_settings.locale_and_timezone.title")}
|
||||
</StyledSectionTitle>
|
||||
<FieldWrapper>
|
||||
<StyledLabel htmlFor="locale">
|
||||
{t("workbook_settings.locale_and_timezone.locale_label")}
|
||||
</StyledLabel>
|
||||
<FormControl fullWidth>
|
||||
<StyledSelect
|
||||
id="locale"
|
||||
value={validSelectedLocale}
|
||||
onChange={(event) => {
|
||||
setSelectedLocale(event.target.value as string);
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
sx: menuPaperStyles,
|
||||
},
|
||||
TransitionProps: {
|
||||
timeout: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{locales.map((locale) => (
|
||||
<StyledMenuItem
|
||||
key={locale}
|
||||
value={locale}
|
||||
$isSelected={locale === selectedLocale}
|
||||
>
|
||||
{locale}
|
||||
</StyledMenuItem>
|
||||
))}
|
||||
</StyledSelect>
|
||||
<HelperBox>
|
||||
<Row>
|
||||
{t("workbook_settings.locale_and_timezone.locale_example1")}
|
||||
<RowValue>1,234.56</RowValue>
|
||||
</Row>
|
||||
<Row>
|
||||
{t("workbook_settings.locale_and_timezone.locale_example2")}
|
||||
<RowValue>12/31/2025</RowValue>
|
||||
</Row>
|
||||
<Row>
|
||||
{t("workbook_settings.locale_and_timezone.locale_example3")}
|
||||
<RowValue>11/23/2025 09:21:06 PM</RowValue>
|
||||
</Row>
|
||||
<Row>
|
||||
{t("workbook_settings.locale_and_timezone.locale_example4")}
|
||||
<RowValue>Monday</RowValue>
|
||||
</Row>
|
||||
</HelperBox>
|
||||
</FormControl>
|
||||
</FieldWrapper>
|
||||
<FieldWrapper>
|
||||
<StyledLabel htmlFor="timezone">
|
||||
{t("workbook_settings.locale_and_timezone.timezone_label")}
|
||||
</StyledLabel>
|
||||
<FormControl fullWidth>
|
||||
<StyledAutocomplete
|
||||
id="timezone"
|
||||
value={validSelectedTimezone}
|
||||
onChange={(_event, newValue) => {
|
||||
setSelectedTimezone((newValue as string) || "");
|
||||
}}
|
||||
options={timezones}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
renderOption={(props, option) => (
|
||||
<StyledMenuItem
|
||||
{...props}
|
||||
key={option as string}
|
||||
$isSelected={option === validSelectedTimezone}
|
||||
>
|
||||
{option as string}
|
||||
</StyledMenuItem>
|
||||
)}
|
||||
disableClearable
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: menuPaperStyles,
|
||||
},
|
||||
popper: {
|
||||
sx: {
|
||||
"& .MuiAutocomplete-paper": {
|
||||
transition: "none !important",
|
||||
},
|
||||
},
|
||||
},
|
||||
popupIndicator: {
|
||||
disableRipple: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<HelperBox>
|
||||
<Row>
|
||||
{t("workbook_settings.locale_and_timezone.timezone_example1")}
|
||||
<RowValue>23/11/2025</RowValue>
|
||||
</Row>
|
||||
<Row>
|
||||
{t("workbook_settings.locale_and_timezone.timezone_example2")}
|
||||
<RowValue>11/23/2025 09:21:06 PM</RowValue>
|
||||
</Row>
|
||||
</HelperBox>
|
||||
</FormControl>
|
||||
</FieldWrapper>
|
||||
</StyledDialogContent>
|
||||
|
||||
<DialogFooter>
|
||||
<StyledButton onClick={handleSave} tabIndex={0}>
|
||||
<Check
|
||||
style={{ width: "16px", height: "16px", marginRight: "8px" }}
|
||||
/>
|
||||
{t("num_fmt.save")}
|
||||
</StyledButton>
|
||||
</DialogFooter>
|
||||
</StyledDialog>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledDialog = styled(Dialog)`
|
||||
& .MuiPaper-root {
|
||||
max-width: 320px;
|
||||
width: 320px;
|
||||
min-width: 280px;
|
||||
border-radius: 8px;
|
||||
padding: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledDialogTitle = styled("div")`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: Inter;
|
||||
padding: 0px 12px;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid ${theme.palette.grey["300"]};
|
||||
`;
|
||||
|
||||
const Cross = styled("div")`
|
||||
&:hover {
|
||||
background-color: ${theme.palette.grey["50"]};
|
||||
}
|
||||
display: flex;
|
||||
border-radius: 4px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledDialogContent = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
margin: 12px;
|
||||
`;
|
||||
|
||||
const StyledSectionTitle = styled("h1")`
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-family: Inter;
|
||||
margin: 0px;
|
||||
color: ${theme.palette.text.primary};
|
||||
`;
|
||||
|
||||
const StyledSelect = styled(Select)`
|
||||
font-size: 12px;
|
||||
height: 32px;
|
||||
& .MuiInputBase-root {
|
||||
padding: 0px !important;
|
||||
}
|
||||
& .MuiInputBase-input {
|
||||
font-size: 12px;
|
||||
height: 20px;
|
||||
padding-right: 0px !important;
|
||||
margin: 0px;
|
||||
}
|
||||
& .MuiSelect-select {
|
||||
padding: 8px 32px 8px 8px !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
& .MuiSvgIcon-root {
|
||||
right: 4px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const HelperBox = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid ${theme.palette.grey["300"]};
|
||||
font-family: Inter;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: 8px;
|
||||
background-color: ${theme.palette.grey["100"]};
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
`;
|
||||
|
||||
const Row = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
color: ${theme.palette.grey[700]};
|
||||
`;
|
||||
|
||||
const RowValue = styled("span")`
|
||||
font-size: 12px;
|
||||
font-family: Inter;
|
||||
font-weight: normal;
|
||||
color: ${theme.palette.grey[500]};
|
||||
`;
|
||||
|
||||
const StyledAutocomplete = styled(Autocomplete)`
|
||||
& .MuiInputBase-root {
|
||||
padding: 0px !important;
|
||||
height: 32px;
|
||||
}
|
||||
& .MuiInputBase-input {
|
||||
font-size: 12px;
|
||||
height: 20px;
|
||||
padding: 0px;
|
||||
padding-right: 0px !important;
|
||||
margin: 0px;
|
||||
}
|
||||
& .MuiAutocomplete-popupIndicator:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
& .MuiAutocomplete-popupIndicator {
|
||||
& .MuiTouchRipple-root {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
& .MuiOutlinedInput-root .MuiAutocomplete-endAdornment {
|
||||
right: 4px;
|
||||
}
|
||||
& .MuiOutlinedInput-root .MuiAutocomplete-input {
|
||||
padding: 8px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const menuPaperStyles = {
|
||||
boxSizing: "border-box",
|
||||
marginTop: "4px",
|
||||
padding: "4px",
|
||||
borderRadius: "8px",
|
||||
transition: "none !important",
|
||||
"& .MuiList-padding": {
|
||||
padding: 0,
|
||||
},
|
||||
"& .MuiList-root": {
|
||||
padding: 0,
|
||||
},
|
||||
"& .MuiAutocomplete-noOptions": {
|
||||
padding: "8px",
|
||||
fontSize: "12px",
|
||||
fontFamily: "Inter",
|
||||
},
|
||||
"& .MuiMenuItem-root": {
|
||||
height: "32px !important",
|
||||
padding: "8px !important",
|
||||
minHeight: "32px !important",
|
||||
},
|
||||
};
|
||||
|
||||
const StyledMenuItem = styled(MenuItem)<{ $isSelected?: boolean }>`
|
||||
padding: 8px !important;
|
||||
height: 32px !important;
|
||||
min-height: 32px !important;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
background-color: ${({ $isSelected }) =>
|
||||
$isSelected ? theme.palette.grey[50] : "transparent"} !important;
|
||||
&:hover {
|
||||
background-color: ${theme.palette.grey[50]} !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const FieldWrapper = styled(Box)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 6px;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled("label")`
|
||||
font-size: 12px;
|
||||
font-family: "Inter";
|
||||
font-weight: 500;
|
||||
color: ${theme.palette.text.primary};
|
||||
display: block;
|
||||
`;
|
||||
|
||||
const DialogFooter = styled("div")`
|
||||
color: ${theme.palette.grey[700]};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 1px solid ${theme.palette.grey["300"]};
|
||||
font-family: Inter;
|
||||
justify-content: flex-end;
|
||||
padding: 12px;
|
||||
`;
|
||||
|
||||
const StyledButton = styled("div")`
|
||||
cursor: pointer;
|
||||
color: ${theme.palette.common.white};
|
||||
background: ${theme.palette.primary.main};
|
||||
padding: 0px 10px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: "Inter";
|
||||
font-size: 14px;
|
||||
&:hover {
|
||||
background: ${theme.palette.primary.dark};
|
||||
}
|
||||
`;
|
||||
|
||||
export default WorkbookSettingsDialog;
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CellStyle, Model } from "@ironcalc/wasm";
|
||||
import { columnNameFromNumber } from "@ironcalc/wasm";
|
||||
import { theme } from "../../theme";
|
||||
import { getColor } from "../Editor/util";
|
||||
import type { Cell } from "../types";
|
||||
import type { WorkbookState } from "../workbookState";
|
||||
@@ -721,7 +722,7 @@ export default class WorksheetCanvas {
|
||||
const style = this.model.getCellStyle(selectedSheet, row, column);
|
||||
|
||||
// first the background
|
||||
let backgroundColor = "#FFFFFF";
|
||||
let backgroundColor = theme.palette.common.white;
|
||||
if (style.fill.fg_color) {
|
||||
backgroundColor = style.fill.fg_color;
|
||||
}
|
||||
@@ -1036,14 +1037,21 @@ export default class WorksheetCanvas {
|
||||
width: number,
|
||||
div: HTMLDivElement,
|
||||
selected: boolean,
|
||||
isFullColumnSelected: boolean,
|
||||
): void {
|
||||
div.style.boxSizing = "border-box";
|
||||
div.style.width = `${width}px`;
|
||||
div.style.height = `${headerRowHeight}px`;
|
||||
div.style.backgroundColor = selected
|
||||
? headerSelectedBackground
|
||||
? isFullColumnSelected
|
||||
? theme.palette.primary.main
|
||||
: headerSelectedBackground
|
||||
: headerBackground;
|
||||
div.style.color = selected ? headerSelectedColor : headerTextColor;
|
||||
div.style.color = selected
|
||||
? isFullColumnSelected
|
||||
? theme.palette.common.white
|
||||
: headerSelectedColor
|
||||
: headerTextColor;
|
||||
div.style.fontWeight = "bold";
|
||||
div.style.borderLeft = `1px solid ${headerBorderColor}`;
|
||||
div.style.borderTop = `1px solid ${headerBorderColor}`;
|
||||
@@ -1071,9 +1079,15 @@ export default class WorksheetCanvas {
|
||||
const { sheet: selectedSheet, range } = this.model.getSelectedView();
|
||||
let rowStart = range[0];
|
||||
let rowEnd = range[2];
|
||||
let columnStart = range[1];
|
||||
let columnEnd = range[3];
|
||||
if (rowStart > rowEnd) {
|
||||
[rowStart, rowEnd] = [rowEnd, rowStart];
|
||||
}
|
||||
if (columnStart > columnEnd) {
|
||||
[columnStart, columnEnd] = [columnEnd, columnStart];
|
||||
}
|
||||
const isFullRowSelected = columnStart === 1 && columnEnd === LAST_COLUMN;
|
||||
const context = this.ctx;
|
||||
|
||||
let topLeftCornerY = headerRowHeight + 0.5;
|
||||
@@ -1085,7 +1099,9 @@ export default class WorksheetCanvas {
|
||||
context.fillStyle = headerBorderColor;
|
||||
context.fillRect(0.5, topLeftCornerY, headerColumnWidth, rowHeight);
|
||||
context.fillStyle = selected
|
||||
? headerSelectedBackground
|
||||
? isFullRowSelected
|
||||
? theme.palette.primary.main
|
||||
: headerSelectedBackground
|
||||
: headerBackground;
|
||||
context.fillRect(
|
||||
0.5,
|
||||
@@ -1097,7 +1113,11 @@ export default class WorksheetCanvas {
|
||||
context.fillStyle = outlineColor;
|
||||
context.fillRect(headerColumnWidth - 1, topLeftCornerY, 1, rowHeight);
|
||||
}
|
||||
context.fillStyle = selected ? headerSelectedColor : headerTextColor;
|
||||
context.fillStyle = selected
|
||||
? isFullRowSelected
|
||||
? theme.palette.common.white
|
||||
: headerSelectedColor
|
||||
: headerTextColor;
|
||||
context.font = `bold 12px ${defaultCellFontFamily}`;
|
||||
context.fillText(
|
||||
`${row}`,
|
||||
@@ -1122,11 +1142,17 @@ export default class WorksheetCanvas {
|
||||
const { columnHeaders } = this;
|
||||
let deltaX = 0;
|
||||
const { range } = this.model.getSelectedView();
|
||||
let rowStart = range[0];
|
||||
let rowEnd = range[2];
|
||||
let columnStart = range[1];
|
||||
let columnEnd = range[3];
|
||||
if (columnStart > columnEnd) {
|
||||
[columnStart, columnEnd] = [columnEnd, columnStart];
|
||||
}
|
||||
if (rowStart > rowEnd) {
|
||||
[rowStart, rowEnd] = [rowEnd, rowStart];
|
||||
}
|
||||
const isFullColumnSelected = rowStart === 1 && rowEnd === LAST_ROW;
|
||||
for (const header of columnHeaders.querySelectorAll(".column-header"))
|
||||
header.remove();
|
||||
for (const handle of columnHeaders.querySelectorAll(
|
||||
@@ -1146,7 +1172,12 @@ export default class WorksheetCanvas {
|
||||
// Frozen headers
|
||||
for (let column = 1; column <= frozenColumns; column += 1) {
|
||||
const selected = column >= columnStart && column <= columnEnd;
|
||||
deltaX += this.addColumnHeader(deltaX, column, selected);
|
||||
deltaX += this.addColumnHeader(
|
||||
deltaX,
|
||||
column,
|
||||
selected,
|
||||
isFullColumnSelected,
|
||||
);
|
||||
}
|
||||
|
||||
if (frozenColumns !== 0) {
|
||||
@@ -1162,7 +1193,12 @@ export default class WorksheetCanvas {
|
||||
|
||||
for (let column = firstColumn; column <= lastColumn; column += 1) {
|
||||
const selected = column >= columnStart && column <= columnEnd;
|
||||
deltaX += this.addColumnHeader(deltaX, column, selected);
|
||||
deltaX += this.addColumnHeader(
|
||||
deltaX,
|
||||
column,
|
||||
selected,
|
||||
isFullColumnSelected,
|
||||
);
|
||||
}
|
||||
|
||||
columnHeaders.style.width = `${deltaX}px`;
|
||||
@@ -1172,6 +1208,7 @@ export default class WorksheetCanvas {
|
||||
deltaX: number,
|
||||
column: number,
|
||||
selected: boolean,
|
||||
isFullColumnSelected: boolean,
|
||||
): number {
|
||||
const columnWidth = this.getColumnWidth(
|
||||
this.model.getSelectedSheet(),
|
||||
@@ -1182,7 +1219,7 @@ export default class WorksheetCanvas {
|
||||
div.textContent = columnNameFromNumber(column);
|
||||
this.columnHeaders.insertBefore(div, null);
|
||||
|
||||
this.styleColumnHeader(columnWidth, div, selected);
|
||||
this.styleColumnHeader(columnWidth, div, selected, isFullColumnSelected);
|
||||
this.addColumnResizeHandle(deltaX + columnWidth, column, columnWidth);
|
||||
return columnWidth;
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@
|
||||
"label": "Formula",
|
||||
"title": "Update formula"
|
||||
},
|
||||
"formula_bar": {
|
||||
"manage_named_ranges": "Manage Named Ranges"
|
||||
},
|
||||
"navigation": {
|
||||
"add_sheet": "Add sheet",
|
||||
"sheet_list": "Sheet list",
|
||||
@@ -164,21 +167,5 @@
|
||||
"right_drawer": {
|
||||
"close": "Close",
|
||||
"resize_drawer": "Resize drawer"
|
||||
},
|
||||
"workbook_settings": {
|
||||
"open_settings": "Open settings",
|
||||
"title": "Workbook Settings",
|
||||
"close": "Close dialog",
|
||||
"locale_and_timezone": {
|
||||
"title": "Locale & Timezone",
|
||||
"locale_label": "Locale",
|
||||
"locale_example1": "Number",
|
||||
"locale_example2": "Date",
|
||||
"locale_example3": "Date and Time",
|
||||
"locale_example4": "First day of the week",
|
||||
"timezone_label": "Timezone",
|
||||
"timezone_example1": "TODAY()",
|
||||
"timezone_example2": "NOW()"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
webapp/app.ironcalc.com/frontend/package-lock.json
generated
25
webapp/app.ironcalc.com/frontend/package-lock.json
generated
@@ -91,6 +91,7 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -570,6 +571,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -613,6 +615,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
|
||||
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -2026,6 +2029,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -2122,6 +2126,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.25",
|
||||
"caniuse-lite": "^1.0.30001754",
|
||||
@@ -2691,6 +2696,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -2758,6 +2764,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -2767,6 +2774,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -2978,6 +2986,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -3023,6 +3032,7 @@
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -3113,21 +3123,6 @@
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
|
||||
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,16 +227,19 @@ const Wrapper = styled("div")`
|
||||
`;
|
||||
|
||||
const DRAWER_WIDTH = 264;
|
||||
const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 440;
|
||||
export const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 768;
|
||||
|
||||
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
|
||||
margin-left: ${({ isDrawerOpen }) => (isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`)};
|
||||
width: ${({ isDrawerOpen }) => (isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : "100%")};
|
||||
margin-left: ${({ isDrawerOpen }) =>
|
||||
isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`};
|
||||
width: ${({ isDrawerOpen }) =>
|
||||
isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : "100%"};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
@media (max-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px) {
|
||||
${({ isDrawerOpen }) => isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`}
|
||||
${({ isDrawerOpen }) =>
|
||||
isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -247,7 +250,7 @@ const MobileOverlay = styled("div")`
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
z-index: 1;
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
|
||||
@media (min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE + 1}px) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Model } from "@ironcalc/workbook";
|
||||
import { IconButton, Tooltip } from "@mui/material";
|
||||
import { CloudOff, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
import { MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE } from "../App";
|
||||
import { FileMenu } from "./FileMenu";
|
||||
import { HelpMenu } from "./HelpMenu";
|
||||
import { downloadModel } from "./rpc";
|
||||
@@ -77,7 +78,7 @@ export function FileBar(properties: {
|
||||
{properties.isDrawerOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
|
||||
</DrawerButton>
|
||||
</Tooltip>
|
||||
{width > 440 && (
|
||||
{width > MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE && (
|
||||
<FileMenu
|
||||
newModel={properties.newModel}
|
||||
newModelFromTemplate={properties.newModelFromTemplate}
|
||||
@@ -92,7 +93,7 @@ export function FileBar(properties: {
|
||||
onDelete={properties.onDelete}
|
||||
/>
|
||||
)}
|
||||
{width > 440 && <HelpMenu />}
|
||||
{width > MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE && <HelpMenu />}
|
||||
<WorkbookTitleWrapper>
|
||||
<WorkbookTitle
|
||||
name={properties.model.getName()}
|
||||
@@ -112,7 +113,8 @@ export function FileBar(properties: {
|
||||
<div style={{ fontWeight: "bold" }}>{cloudWarningText2}</div>
|
||||
</div>
|
||||
}
|
||||
placement="bottom-start"
|
||||
placement="bottom"
|
||||
enterTouchDelay={0}
|
||||
enterDelay={500}
|
||||
slotProps={{
|
||||
popper: {
|
||||
|
||||
BIN
xlsx/tests/calc_tests/BETA_GAMMA.xlsx
Normal file
BIN
xlsx/tests/calc_tests/BETA_GAMMA.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/CELL.xlsx
Normal file
BIN
xlsx/tests/calc_tests/CELL.xlsx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/calc_tests/MINA_MAXA.xlsx
Normal file
BIN
xlsx/tests/calc_tests/MINA_MAXA.xlsx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/calc_tests/N.xlsx
Normal file
BIN
xlsx/tests/calc_tests/N.xlsx
Normal file
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user