Compare commits

..

54 Commits

Author SHA1 Message Date
Nicolás Hatcher
b071b95a02 UPDATE: Uses shorter date and time on "Last updated"
Also typo "Updated at" => "Updated on"
2025-12-10 18:42:11 +01:00
Nicolás Hatcher
2383ce4d27 UPDATE: Updates docs dependencies 2025-12-10 18:40:45 +01:00
Daniel González-Albo
ba75ffcf4f Merge pull request #598 from elsaminsut/testfixes
fix: remove duplicate xlsx tests
2025-12-10 00:48:27 +01:00
Daniel
b5c977d3aa fix: comments 2025-12-09 17:15:39 +01:00
Daniel
4029441cea fix: tiny details in styling 2025-12-09 17:15:39 +01:00
Daniel
cd47c609a0 update: remove drawer button from toolbar, small fixes 2025-12-09 17:15:39 +01:00
Daniel
ae6acdcdd5 fix: styles 2025-12-09 17:15:39 +01:00
Daniel
c196db2115 update: click on list items selects cells and ranges 2025-12-09 17:15:39 +01:00
Daniel
a3c201e4e4 update: list ranges in menu 2025-12-09 17:15:39 +01:00
Daniel
126e62957a update: allow opening nm drawer from menu 2025-12-09 17:15:39 +01:00
Daniel
294a651ae5 update: add a name manager menu in formula bar 2025-12-09 17:15:39 +01:00
Elsa Minsut
6f8a1e0da6 fix: syntax fixes in unit tests 2025-12-01 20:23:32 +01:00
Elsa Minsut
205ba6ee2d fix: removes failing currency edge case 2025-12-01 19:53:18 +01:00
Elsa Minsut
547b331773 fix: xlsx test without array formulas 2025-12-01 19:44:15 +01:00
Nicolás Hatcher
f96612cf23 FIX: NOW test cases 2025-11-28 20:52:34 +01:00
Nicolás Hatcher
745435b950 FIX: Copilot requests 2025-11-28 20:52:34 +01:00
Nicolás Hatcher
4ca996cd3f UPDATE(easter egg): Add an argument for NOW
This shows the time in different timezones
2025-11-28 20:52:34 +01:00
Elsa Minsut
3fbb91c414 fix: deletes old xlsx test with failing cases 2025-11-28 20:26:34 +01:00
Elsa Minsut
93c9c42607 fix: uploads xlsx tests split per function 2025-11-28 20:26:34 +01:00
Elsa Minsut
11edc2378e fix: deletes old test file and replaces it with detailed one 2025-11-28 20:26:34 +01:00
Elsa Minsut
962e70c834 fix: adds new line at end of file 2025-11-28 20:26:34 +01:00
Elsa Minsut
f803dad0a3 fix: updates test to remove failing edge cases 2025-11-28 20:26:34 +01:00
Elsa Minsut
19580fc1ad update: warning message shows the function as implemented 2025-11-28 20:26:34 +01:00
Elsa Minsut
e760b2d08e update: sets status as available for implemented functions 2025-11-28 20:26:34 +01:00
Elsa Minsut
0e6ded7154 update: adds unit test for CELL, INFO, N and SHEETS 2025-11-28 20:26:34 +01:00
Elsa Minsut
db26403432 docs: available status for implemented functions 2025-11-28 20:23:52 +01:00
Elsa Minsut
9193479cce update: adds xlsx test for SUMSQ 2025-11-28 20:23:52 +01:00
Elsa Minsut
f814a75ae5 update: adds unit test for SUMSQ 2025-11-28 20:23:52 +01:00
Elsa Minsut
c8da5efb5f update: removes old xlsx test file 2025-11-28 20:23:52 +01:00
Daniel
522e734395 update: use different header styling for full column or row selection 2025-11-28 20:10:24 +01:00
tolgakaan12
2a7d59e512 FIX: Floating-point precision bug in FLOOR functions
Fixes #571

- Add EXCEL_PRECISION constant (15 significant digits)
- Fix FLOOR(7.1, 0.1) returning 7.0 instead of 7.1
- Apply to_excel_precision to ratio before floor/ceil operations
- Affects FLOOR, FLOOR.MATH, and FLOOR.PRECISE functions
- Add test_floor with 6 test cases
2025-11-28 20:05:31 +01:00
Nicolás Hatcher
c4142d4bf8 UPDATE: Adds 12 more statistical functions:
* GAUSS
* HARMEAN
* KURT
* MAXA
* MEDIAN
* MINA
* RANK.EQ
* RANK.AVG
* SKEW
* SKEW.P
* SMALL
* LARGE
2025-11-28 19:55:43 +01:00
Daniel González-Albo
885d344b5b Merge pull request #581 from blueboy93/column-documentation
Edited Column Documentation
2025-11-27 23:44:18 +01:00
Tom
bed6f007cd FIX: Typos adjusted. Thanks Elsa! 2025-11-27 20:26:28 +01:00
Tom
dbd1b2df60 FIX: Edits after Elsa's review and further tests 2025-11-27 18:49:32 +01:00
Elsa Minsut
db552047c8 fix: format fixes 2025-11-26 23:57:31 +01:00
Elsa Minsut
bcbacdb0a3 fix: adds missing file format and missing reference in mod 2025-11-26 23:44:42 +01:00
Elsa Minsut
d0f37854d9 fix: removes duplicate COMBIN and COMBINA tests 2025-11-26 23:36:59 +01:00
Elsa Minsut
99b03f70c3 fix: removes duplicate database functions test 2025-11-26 23:35:31 +01:00
Elsa Minsut
3e1605a494 fix: removes a bunch of duplicate math functions tests 2025-11-26 23:33:13 +01:00
Elsa Minsut
d6aad08e73 fix: remove duplicate MROUND, TRUNC, INT test 2025-11-26 23:31:37 +01:00
Nicolás Hatcher
8597d14a4e UPDATE: Implements CORREL, SLOPE, INTERCEPT, RSQ and STEYX
These are all functions that follow a very simmilar path code
2025-11-26 22:33:49 +01:00
Nicolás Hatcher
01b19b9c35 FIX: Add comments. Thank you copilot! 2025-11-26 20:09:58 +01:00
Nicolás Hatcher
4649a0c78c UPDATE: Adds SUMX2MY2, SUMX2PY2 and SUMXMY2 mathematical functions 2025-11-26 20:09:58 +01:00
Daniel González-Albo
cd0baf5ba7 Merge pull request #591 from ironcalc/dani/app/mobile-adjustments
fix: mobile issues in app
2025-11-26 00:51:08 +01:00
Daniel
167d169f1a chore: use a constant instead of a number 2025-11-26 00:44:06 +01:00
Nicolás Hatcher
080574b112 UPDATE: Implement FTEST function 2025-11-26 00:30:37 +01:00
Daniel
6056b8f122 fix: mobile issues 2025-11-26 00:01:12 +01:00
Nicolás Hatcher
e61b15655a UPDATE: Adds a bunch of tests 2025-11-25 01:20:03 +01:00
Nicolás Hatcher
6822505602 UPDATE: Adds 56 functions in the Statistical section
Uses statrs for numerical functions

REFACTOR: Put statistical functions on its own module

This might seem counter-intuitive but the wasm build after this refactor
is 1528 bytes smaller :)
2025-11-25 01:20:03 +01:00
Tom
25f7891343 FIX: added missing comma (Thanks copilot!) 2025-11-24 18:36:58 +01:00
Tom
bdd0af0a39 FIX: Fix mispelled word (Thanks Copilot) 2025-11-24 18:27:38 +01:00
Tom
261924396d Edited Column Documentation 2025-11-24 18:12:34 +01:00
Nicolás Hatcher
67ef3bcf87 FIX: Correct number of arguments for functions 2025-11-23 21:02:59 +01:00
139 changed files with 10141 additions and 1743 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,
}
}

View File

@@ -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,

View File

@@ -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) {

View 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)
}
}

View File

@@ -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),
}
}
}

View File

@@ -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))
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

File diff suppressed because it is too large Load Diff

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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))
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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;

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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())
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View File

@@ -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;

View 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;

View 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");
}

View 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!");
}

View 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!");
}

View 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");
}

View 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!");
}

View 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");
}

View 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");
}

View 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!");
}

View 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!");
}

View 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!");
}

View 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!");
}

View 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!");
}

View 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!");
}

View 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!");
}

View 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!");
}

View 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");
}

View 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!");
}

View 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");
}

View 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");
}

View 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!");
}

View 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");
}

View 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");
}

View 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!");
}

View 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");
}

View File

@@ -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)");

View 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!");
}

View File

@@ -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();
@@ -24,4 +24,4 @@ fn arguments() {
assert_eq!(model._get_text("A6"), *"#ERROR!");
assert_eq!(model._get_text("A7"), *"#ERROR!");
assert_eq!(model._get_text("A8"), *"#ERROR!");
}
}

123
base/src/test/test_floor.rs Normal file
View 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");
}

View File

@@ -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!");
}

View File

@@ -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!");

View File

@@ -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"
);
}

View 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");
}

830
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,8 @@
"preview": "vitepress preview src"
},
"devDependencies": {
"markdown-it-mathjax3": "^4.3.2",
"vitepress": "^v2.0.0-alpha.12",
"vue": "^3.5.17"
"markdown-it-mathjax3": "^4",
"vitepress": "^v2.0.0-alpha.15",
"vue": "^3.5.25"
}
}

View File

@@ -2050,10 +2050,10 @@ export default defineConfig({
},
lastUpdated: {
text: "Updated at",
text: "Updated on",
formatOptions: {
dateStyle: "full",
timeStyle: "medium",
dateStyle: "medium",
timeStyle: "short",
},
},

View File

@@ -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" /> | |

View File

@@ -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).
:::

View File

@@ -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).
:::

View File

@@ -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).
:::

View File

@@ -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".
## Links
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.

View File

@@ -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" /> | |

View File

@@ -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).
:::

View File

@@ -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")`
@@ -123,20 +145,43 @@ const Container = styled("div")`
height: ${FORMULA_BAR_HEIGHT}px;
`;
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")`

View 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;

View File

@@ -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")({

View File

@@ -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;
}
`;

View File

@@ -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"

View File

@@ -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}
@@ -764,7 +765,7 @@ const WorksheetAreaLeft = styled("div")<WorksheetAreaLeftProps>(
position: "absolute",
top: `${TOOLBAR_HEIGHT + 1}px`,
width: `calc(100% - ${$drawerWidth}px)`,
height: `calc(100% - ${TOOLBAR_HEIGHT + 1}px)`,
height: `calc(100% - ${TOOLBAR_HEIGHT}px)`,
}),
);

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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()"
}
}
}

View File

@@ -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"
}
}
}
}

View File

@@ -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) {

View File

@@ -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: {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More