Compare commits
35 Commits
fact_gamma
...
dani/widge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d1d338c51 | ||
|
|
a78920216a | ||
|
|
b96c10ab8d | ||
|
|
ab8aaea2bf | ||
|
|
f96612cf23 | ||
|
|
745435b950 | ||
|
|
4ca996cd3f | ||
|
|
3fbb91c414 | ||
|
|
93c9c42607 | ||
|
|
11edc2378e | ||
|
|
962e70c834 | ||
|
|
f803dad0a3 | ||
|
|
19580fc1ad | ||
|
|
e760b2d08e | ||
|
|
0e6ded7154 | ||
|
|
db26403432 | ||
|
|
9193479cce | ||
|
|
f814a75ae5 | ||
|
|
c8da5efb5f | ||
|
|
522e734395 | ||
|
|
2a7d59e512 | ||
|
|
c4142d4bf8 | ||
|
|
885d344b5b | ||
|
|
bed6f007cd | ||
|
|
dbd1b2df60 | ||
|
|
8597d14a4e | ||
|
|
01b19b9c35 | ||
|
|
4649a0c78c | ||
|
|
cd0baf5ba7 | ||
|
|
167d169f1a | ||
|
|
080574b112 | ||
|
|
6056b8f122 | ||
|
|
25f7891343 | ||
|
|
bdd0af0a39 | ||
|
|
261924396d |
@@ -12,6 +12,9 @@ pub(crate) const DEFAULT_WINDOW_WIDTH: i64 = 800;
|
||||
pub(crate) const LAST_COLUMN: i32 = 16_384;
|
||||
pub(crate) const LAST_ROW: i32 = 1_048_576;
|
||||
|
||||
// Excel uses 15 significant digits of precision for all numeric calculations.
|
||||
pub(crate) const EXCEL_PRECISION: usize = 15;
|
||||
|
||||
// 693_594 is computed as:
|
||||
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
||||
// The 2 days offset is because of Excel 1900 bug
|
||||
|
||||
@@ -471,6 +471,20 @@ impl Parser {
|
||||
Node::NumberKind(s) => ArrayNode::Number(s),
|
||||
Node::StringKind(s) => ArrayNode::String(s),
|
||||
Node::ErrorKind(kind) => ArrayNode::Error(kind),
|
||||
Node::UnaryKind {
|
||||
kind: OpUnary::Minus,
|
||||
right,
|
||||
} => {
|
||||
if let Node::NumberKind(n) = *right {
|
||||
ArrayNode::Number(-n)
|
||||
} else {
|
||||
return Err(Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
message: "Invalid value in array".to_string(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
});
|
||||
}
|
||||
}
|
||||
error @ Node::ParseErrorKind { .. } => return Err(error),
|
||||
_ => {
|
||||
return Err(Node::ParseErrorKind {
|
||||
@@ -490,6 +504,20 @@ impl Parser {
|
||||
Node::NumberKind(s) => ArrayNode::Number(s),
|
||||
Node::StringKind(s) => ArrayNode::String(s),
|
||||
Node::ErrorKind(kind) => ArrayNode::Error(kind),
|
||||
Node::UnaryKind {
|
||||
kind: OpUnary::Minus,
|
||||
right,
|
||||
} => {
|
||||
if let Node::NumberKind(n) = *right {
|
||||
ArrayNode::Number(-n)
|
||||
} else {
|
||||
return Err(Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
message: "Invalid value in array".to_string(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
});
|
||||
}
|
||||
}
|
||||
error @ Node::ParseErrorKind { .. } => return Err(error),
|
||||
_ => {
|
||||
return Err(Node::ParseErrorKind {
|
||||
|
||||
@@ -872,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],
|
||||
@@ -890,7 +888,6 @@ 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),
|
||||
@@ -929,6 +926,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
||||
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),
|
||||
@@ -989,6 +987,26 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
||||
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],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1014,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),
|
||||
@@ -1061,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),
|
||||
@@ -1253,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),
|
||||
@@ -1266,7 +1282,6 @@ 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,
|
||||
@@ -1287,6 +1302,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
||||
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,
|
||||
@@ -1322,5 +1338,25 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
||||
Function::VarA => StaticResult::Scalar,
|
||||
Function::WeibullDist => StaticResult::Scalar,
|
||||
Function::ZTest => StaticResult::Scalar,
|
||||
Function::Sumx2my2 => StaticResult::Scalar,
|
||||
Function::Sumx2py2 => StaticResult::Scalar,
|
||||
Function::Sumxmy2 => StaticResult::Scalar,
|
||||
Function::Correl => StaticResult::Scalar,
|
||||
Function::Rsq => StaticResult::Scalar,
|
||||
Function::Intercept => StaticResult::Scalar,
|
||||
Function::Slope => StaticResult::Scalar,
|
||||
Function::Steyx => StaticResult::Scalar,
|
||||
Function::Gauss => StaticResult::Scalar,
|
||||
Function::Harmean => StaticResult::Scalar,
|
||||
Function::Kurt => StaticResult::Scalar,
|
||||
Function::Large => StaticResult::Scalar,
|
||||
Function::MaxA => StaticResult::Scalar,
|
||||
Function::Median => StaticResult::Scalar,
|
||||
Function::MinA => StaticResult::Scalar,
|
||||
Function::RankAvg => StaticResult::Scalar,
|
||||
Function::RankEq => StaticResult::Scalar,
|
||||
Function::Skew => StaticResult::Scalar,
|
||||
Function::SkewP => StaticResult::Scalar,
|
||||
Function::Small => StaticResult::Scalar,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use chrono::Months;
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::NaiveTime;
|
||||
use chrono::Timelike;
|
||||
use chrono_tz::Tz;
|
||||
|
||||
const SECONDS_PER_DAY: i32 = 86_400;
|
||||
const SECONDS_PER_DAY_F64: f64 = SECONDS_PER_DAY as f64;
|
||||
@@ -770,12 +771,12 @@ impl Model {
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
// Returns the current date/time as an Excel serial number in the model's configured timezone.
|
||||
// Returns the current date/time as an Excel serial number in the given timezone.
|
||||
// Used by TODAY() and NOW().
|
||||
fn current_excel_serial(&self) -> Option<f64> {
|
||||
pub(crate) fn current_excel_serial_with_timezone(&self, tz: Tz) -> Option<f64> {
|
||||
let seconds = get_milliseconds_since_epoch() / 1000;
|
||||
DateTime::from_timestamp(seconds, 0).map(|dt| {
|
||||
let local_time = dt.with_timezone(&self.tz);
|
||||
let local_time = dt.with_timezone(&tz);
|
||||
let days_from_1900 = local_time.num_days_from_ce() - EXCEL_DATE_BASE;
|
||||
let fraction = (local_time.num_seconds_from_midnight() as f64) / (60.0 * 60.0 * 24.0);
|
||||
days_from_1900 as f64 + fraction
|
||||
@@ -978,7 +979,7 @@ impl Model {
|
||||
message: "Wrong number of arguments".to_string(),
|
||||
};
|
||||
}
|
||||
match self.current_excel_serial() {
|
||||
match self.current_excel_serial_with_timezone(self.tz) {
|
||||
Some(serial) => CalcResult::Number(serial.floor()),
|
||||
None => CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
@@ -989,14 +990,35 @@ impl Model {
|
||||
}
|
||||
|
||||
pub(crate) fn fn_now(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if !args.is_empty() {
|
||||
if args.len() > 1 {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Wrong number of arguments".to_string(),
|
||||
};
|
||||
}
|
||||
match self.current_excel_serial() {
|
||||
let tz = match args.first() {
|
||||
Some(arg0) => {
|
||||
// Parse timezone argument
|
||||
let tz_str = match self.get_string(arg0, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let tz: Tz = match tz_str.parse() {
|
||||
Ok(tz) => tz,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: format!("Invalid timezone: {}", &tz_str),
|
||||
}
|
||||
}
|
||||
};
|
||||
tz
|
||||
}
|
||||
None => self.tz,
|
||||
};
|
||||
match self.current_excel_serial_with_timezone(tz) {
|
||||
Some(serial) => CalcResult::Number(serial),
|
||||
None => CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
|
||||
@@ -1,81 +1,15 @@
|
||||
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,
|
||||
};
|
||||
use statrs::function::gamma::ln_gamma;
|
||||
use std::f64::consts::LN_2;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
const FACT_TABLE: [f64; 26] = [
|
||||
1.0, // 0!
|
||||
1.0, // 1!
|
||||
2.0, // 2!
|
||||
6.0, // 3!
|
||||
24.0, // 4!
|
||||
120.0, // 5!
|
||||
720.0, // 6!
|
||||
5040.0, // 7!
|
||||
40320.0, // 8!
|
||||
362880.0, // 9!
|
||||
3628800.0, // 10!
|
||||
39916800.0, // 11!
|
||||
479001600.0, // 12!
|
||||
6227020800.0, // 13!
|
||||
87178291200.0, // 14!
|
||||
1307674368000.0, // 15!
|
||||
20922789888000.0, // 16!
|
||||
355687428096000.0, // 17!
|
||||
6402373705728000.0, // 18!
|
||||
121645100408832000.0, // 19!
|
||||
2432902008176640000.0, // 20!
|
||||
51090942171709440000.0, // 21!
|
||||
1124000727777607680000.0, // 22!
|
||||
25852016738884976640000.0, // 23!
|
||||
620448401733239439360000.0, // 24!
|
||||
15511210043330985984000000.0, // 25!
|
||||
];
|
||||
|
||||
const FACTDOUBLE_TABLE: [f64; 32] = [
|
||||
1.0,
|
||||
1.0,
|
||||
2.0,
|
||||
3.0,
|
||||
8.0,
|
||||
15.0,
|
||||
48.0,
|
||||
105.0,
|
||||
384.0,
|
||||
945.0,
|
||||
3840.0,
|
||||
10395.0,
|
||||
46080.0,
|
||||
135135.0,
|
||||
645120.0,
|
||||
2027025.0,
|
||||
10321920.0,
|
||||
34459425.0,
|
||||
185794560.0,
|
||||
654729075.0,
|
||||
3715891200.0,
|
||||
13749310575.0,
|
||||
81749606400.0,
|
||||
316234143225.0,
|
||||
1961990553600.0,
|
||||
7905853580625.0,
|
||||
51011754393600.0,
|
||||
213458046676875.0,
|
||||
1428329123020800.0,
|
||||
6190283353629370.0,
|
||||
42849873690624000.0,
|
||||
191898783962511000.0,
|
||||
];
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn random() -> f64 {
|
||||
rand::random()
|
||||
@@ -1050,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)
|
||||
}
|
||||
|
||||
@@ -1187,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)
|
||||
}
|
||||
}
|
||||
@@ -1220,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)
|
||||
}
|
||||
|
||||
@@ -1385,78 +1327,35 @@ impl Model {
|
||||
single_number_fn!(fn_sec, |f| Ok(1.0 / f64::cos(f)));
|
||||
single_number_fn!(fn_sech, |f| Ok(1.0 / f64::cosh(f)));
|
||||
single_number_fn!(fn_exp, |f: f64| Ok(f64::exp(f)));
|
||||
|
||||
single_number_fn!(fn_fact, |x: f64| {
|
||||
let x = x.floor();
|
||||
|
||||
if x < 0.0 {
|
||||
return Err(Error::NUM);
|
||||
}
|
||||
|
||||
if x == 0.0 {
|
||||
return Ok(1.0);
|
||||
let mut acc = 1.0;
|
||||
let mut k = 2.0;
|
||||
while k <= x {
|
||||
acc *= k;
|
||||
k += 1.0;
|
||||
}
|
||||
if x < FACT_TABLE.len() as f64 {
|
||||
return Ok(FACT_TABLE[x as usize]);
|
||||
}
|
||||
|
||||
// Use ln Γ(x+1) to avoid overflow while deciding.
|
||||
let ln_val = ln_gamma(x + 1.0);
|
||||
|
||||
// If gamma overflows or is invalid, map to NUM
|
||||
if !ln_val.is_finite() || ln_val > f64::MAX.ln() {
|
||||
return Err(Error::NUM);
|
||||
}
|
||||
|
||||
Ok(ln_val.exp())
|
||||
Ok(acc)
|
||||
});
|
||||
|
||||
single_number_fn!(fn_factdouble, |x: f64| {
|
||||
let x = x.floor();
|
||||
if x <= -1.0 {
|
||||
if x < -1.0 {
|
||||
return Err(Error::NUM);
|
||||
}
|
||||
|
||||
if x <= 1.0 {
|
||||
if x < 0.0 {
|
||||
return Ok(1.0);
|
||||
}
|
||||
|
||||
// From here x > 1 and integer
|
||||
let n = x as i64;
|
||||
|
||||
if n < FACTDOUBLE_TABLE.len() as i64 {
|
||||
return Ok(FACTDOUBLE_TABLE[n as usize]);
|
||||
let mut acc = 1.0;
|
||||
let mut k = if x % 2.0 == 0.0 { 2.0 } else { 1.0 };
|
||||
while k <= x {
|
||||
acc *= k;
|
||||
k += 2.0;
|
||||
}
|
||||
|
||||
// n!! grows very fast, so we compute it using gamma in log-space:
|
||||
//
|
||||
// If n is even, n = 2k:
|
||||
// n!! = 2^k * k!
|
||||
// If n is odd, n = 2k + 1:
|
||||
// n!! = (2k+1)! / (2^k * k!)
|
||||
//
|
||||
// and we use ln_gamma for factorials.
|
||||
|
||||
let ln_val = if n % 2 == 0 {
|
||||
// even n = 2k
|
||||
let k = (n / 2) as f64;
|
||||
// ln(n!!) = k * ln(2) + ln(k!)
|
||||
k * LN_2 + ln_gamma(k + 1.0)
|
||||
} else {
|
||||
// odd n = 2k + 1
|
||||
let k = ((n - 1) / 2) as f64;
|
||||
let nn = n as f64;
|
||||
// ln(n!!) = ln((2k+1)!) - (k * ln(2) + ln(k!))
|
||||
ln_gamma(nn + 1.0) - (k * LN_2 + ln_gamma(k + 1.0))
|
||||
};
|
||||
|
||||
if !ln_val.is_finite() || ln_val > f64::MAX.ln() {
|
||||
return Err(Error::NUM);
|
||||
}
|
||||
|
||||
Ok(ln_val.exp())
|
||||
Ok(acc)
|
||||
});
|
||||
|
||||
single_number_fn!(fn_sign, |f| {
|
||||
if f == 0.0 {
|
||||
Ok(0.0)
|
||||
|
||||
230
base/src/functions/mathematical_sum.rs
Normal file
230
base/src/functions/mathematical_sum.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
type TwoMatricesResult = (i32, i32, Vec<Option<f64>>, Vec<Option<f64>>);
|
||||
|
||||
// Helper to check if two shapes are the same or compatible 1D shapes
|
||||
fn is_same_shape_or_1d(rows1: i32, cols1: i32, rows2: i32, cols2: i32) -> bool {
|
||||
(rows1 == rows2 && cols1 == cols2)
|
||||
|| (rows1 == 1 && cols2 == 1 && cols1 == rows2)
|
||||
|| (rows2 == 1 && cols1 == 1 && cols2 == rows1)
|
||||
}
|
||||
|
||||
impl Model {
|
||||
// SUMX2MY2(array_x, array_y) - Returns the sum of the difference of squares
|
||||
pub(crate) fn fn_sumx2my2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let result = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let (_, _, values_left, values_right) = result;
|
||||
|
||||
let mut sum = 0.0;
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
let x = x_opt.unwrap_or(0.0);
|
||||
let y = y_opt.unwrap_or(0.0);
|
||||
sum += x * x - y * y;
|
||||
}
|
||||
|
||||
CalcResult::Number(sum)
|
||||
}
|
||||
|
||||
// SUMX2PY2(array_x, array_y) - Returns the sum of the sum of squares
|
||||
pub(crate) fn fn_sumx2py2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let result = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let (_rows, _cols, values_left, values_right) = result;
|
||||
|
||||
let mut sum = 0.0;
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
let x = x_opt.unwrap_or(0.0);
|
||||
let y = y_opt.unwrap_or(0.0);
|
||||
sum += x * x + y * y;
|
||||
}
|
||||
|
||||
CalcResult::Number(sum)
|
||||
}
|
||||
|
||||
// SUMXMY2(array_x, array_y) - Returns the sum of squares of differences
|
||||
pub(crate) fn fn_sumxmy2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let result = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let (_, _, values_left, values_right) = result;
|
||||
|
||||
let mut sum = 0.0;
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
let x = x_opt.unwrap_or(0.0);
|
||||
let y = y_opt.unwrap_or(0.0);
|
||||
let diff = x - y;
|
||||
sum += diff * diff;
|
||||
}
|
||||
|
||||
CalcResult::Number(sum)
|
||||
}
|
||||
|
||||
// Helper function to extract and validate two matrices (ranges or arrays) with compatible shapes.
|
||||
// Returns (rows, cols, values_left, values_right) or an error.
|
||||
pub(crate) fn fn_get_two_matrices(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> Result<TwoMatricesResult, CalcResult> {
|
||||
if args.len() != 2 {
|
||||
return Err(CalcResult::new_args_number_error(cell));
|
||||
}
|
||||
let x_range = self.evaluate_node_in_context(&args[0], cell);
|
||||
let y_range = self.evaluate_node_in_context(&args[1], cell);
|
||||
|
||||
let result = match (x_range, y_range) {
|
||||
(
|
||||
CalcResult::Range {
|
||||
left: l1,
|
||||
right: r1,
|
||||
},
|
||||
CalcResult::Range {
|
||||
left: l2,
|
||||
right: r2,
|
||||
},
|
||||
) => {
|
||||
if l1.sheet != l2.sheet {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
));
|
||||
}
|
||||
let rows1 = r1.row - l1.row + 1;
|
||||
let cols1 = r1.column - l1.column + 1;
|
||||
let rows2 = r2.row - l2.row + 1;
|
||||
let cols2 = r2.column - l2.column + 1;
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = self.values_from_range(l1, r1)?;
|
||||
let values_right = self.values_from_range(l2, r2)?;
|
||||
(rows1, cols1, values_left, values_right)
|
||||
}
|
||||
(
|
||||
CalcResult::Array(left),
|
||||
CalcResult::Range {
|
||||
left: l2,
|
||||
right: r2,
|
||||
},
|
||||
) => {
|
||||
let rows2 = r2.row - l2.row + 1;
|
||||
let cols2 = r2.column - l2.column + 1;
|
||||
|
||||
let rows1 = left.len() as i32;
|
||||
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Array and range must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = match self.values_from_array(left) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in first array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
let values_right = self.values_from_range(l2, r2)?;
|
||||
(rows2, cols2, values_left, values_right)
|
||||
}
|
||||
(
|
||||
CalcResult::Range {
|
||||
left: l1,
|
||||
right: r1,
|
||||
},
|
||||
CalcResult::Array(right),
|
||||
) => {
|
||||
let rows1 = r1.row - l1.row + 1;
|
||||
let cols1 = r1.column - l1.column + 1;
|
||||
|
||||
let rows2 = right.len() as i32;
|
||||
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Range and array must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = self.values_from_range(l1, r1)?;
|
||||
let values_right = match self.values_from_array(right) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
(rows1, cols1, values_left, values_right)
|
||||
}
|
||||
(CalcResult::Array(left), CalcResult::Array(right)) => {
|
||||
let rows1 = left.len() as i32;
|
||||
let rows2 = right.len() as i32;
|
||||
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
|
||||
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
|
||||
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Arrays must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = match self.values_from_array(left) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in first array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
let values_right = match self.values_from_array(right) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
(rows1, cols1, values_left, values_right)
|
||||
}
|
||||
_ => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Both arguments must be ranges or arrays".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ mod lookup_and_reference;
|
||||
mod macros;
|
||||
mod math_util;
|
||||
mod mathematical;
|
||||
mod mathematical_sum;
|
||||
mod statistical;
|
||||
mod subtotal;
|
||||
mod text;
|
||||
@@ -76,6 +77,9 @@ pub enum Function {
|
||||
Sum,
|
||||
Sumif,
|
||||
Sumifs,
|
||||
Sumx2my2,
|
||||
Sumx2py2,
|
||||
Sumxmy2,
|
||||
Tan,
|
||||
Tanh,
|
||||
Acot,
|
||||
@@ -203,7 +207,6 @@ pub enum Function {
|
||||
ChisqTest,
|
||||
ConfidenceNorm,
|
||||
ConfidenceT,
|
||||
// Correl,
|
||||
CovarianceP,
|
||||
CovarianceS,
|
||||
Devsq,
|
||||
@@ -212,7 +215,7 @@ pub enum Function {
|
||||
FDistRT,
|
||||
FInv,
|
||||
FInvRT,
|
||||
// FTest,
|
||||
FTest,
|
||||
Fisher,
|
||||
FisherInv,
|
||||
// Forecast,
|
||||
@@ -221,20 +224,18 @@ pub enum Function {
|
||||
GammaInv,
|
||||
GammaLn,
|
||||
GammaLnPrecise,
|
||||
// Gauss,
|
||||
// Growth,
|
||||
// Harmean,
|
||||
Gauss,
|
||||
Harmean,
|
||||
HypGeomDist,
|
||||
// Intercept,
|
||||
// Kurt,
|
||||
// Large,
|
||||
Kurt,
|
||||
Large,
|
||||
// Linest,
|
||||
// Logest,
|
||||
LogNormDist,
|
||||
LogNormInv,
|
||||
// MaxA,
|
||||
// Median,
|
||||
// MinA,
|
||||
MaxA,
|
||||
Median,
|
||||
MinA,
|
||||
// ModeMult,
|
||||
// ModeSingl,
|
||||
NegbinomDist,
|
||||
@@ -254,19 +255,16 @@ pub enum Function {
|
||||
// Prob,
|
||||
// QuartileExc,
|
||||
// QuartileInc,
|
||||
// RankAvg,
|
||||
// RankEq,
|
||||
// Rsq
|
||||
// Skew,
|
||||
// SkewP,
|
||||
// Slope,
|
||||
// Small,
|
||||
RankAvg,
|
||||
RankEq,
|
||||
Skew,
|
||||
SkewP,
|
||||
Small,
|
||||
Standardize,
|
||||
StDevP,
|
||||
StDevS,
|
||||
Stdeva,
|
||||
Stdevpa,
|
||||
// Steyx,
|
||||
TDist,
|
||||
TDist2T,
|
||||
TDistRT,
|
||||
@@ -417,10 +415,16 @@ pub enum Function {
|
||||
Dvar,
|
||||
Dvarp,
|
||||
Dstdevp,
|
||||
|
||||
Correl,
|
||||
Rsq,
|
||||
Intercept,
|
||||
Slope,
|
||||
Steyx,
|
||||
}
|
||||
|
||||
impl Function {
|
||||
pub fn into_iter() -> IntoIter<Function, 324> {
|
||||
pub fn into_iter() -> IntoIter<Function, 345> {
|
||||
[
|
||||
Function::And,
|
||||
Function::False,
|
||||
@@ -497,6 +501,9 @@ impl Function {
|
||||
Function::Sum,
|
||||
Function::Sumif,
|
||||
Function::Sumifs,
|
||||
Function::Sumx2my2,
|
||||
Function::Sumx2py2,
|
||||
Function::Sumxmy2,
|
||||
Function::Choose,
|
||||
Function::Column,
|
||||
Function::Columns,
|
||||
@@ -711,6 +718,7 @@ impl Function {
|
||||
Function::FDistRT,
|
||||
Function::FInv,
|
||||
Function::FInvRT,
|
||||
Function::FTest,
|
||||
Function::Fisher,
|
||||
Function::FisherInv,
|
||||
Function::Gamma,
|
||||
@@ -746,6 +754,23 @@ impl Function {
|
||||
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()
|
||||
}
|
||||
@@ -837,6 +862,7 @@ impl Function {
|
||||
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(),
|
||||
|
||||
@@ -867,6 +893,9 @@ impl Function {
|
||||
|
||||
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(),
|
||||
}
|
||||
@@ -1186,6 +1215,7 @@ impl Function {
|
||||
"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),
|
||||
@@ -1221,6 +1251,28 @@ impl Function {
|
||||
"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,
|
||||
}
|
||||
@@ -1483,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"),
|
||||
@@ -1500,7 +1551,6 @@ 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"),
|
||||
@@ -1523,6 +1573,7 @@ impl fmt::Display for Function {
|
||||
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"),
|
||||
@@ -1556,6 +1607,27 @@ impl fmt::Display for Function {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1875,6 +1947,7 @@ impl Model {
|
||||
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),
|
||||
@@ -1908,6 +1981,26 @@ impl Model {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,6 @@ use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
// Helper to check if two shapes are the same or compatible 1D shapes
|
||||
pub(crate) 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 {
|
||||
// CHISQ.DIST(x, deg_freedom, cumulative)
|
||||
pub(crate) fn fn_chisq_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
@@ -310,171 +303,10 @@ impl Model {
|
||||
|
||||
// CHISQ.TEST(actual_range, expected_range)
|
||||
pub(crate) fn fn_chisq_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let actual_range = self.evaluate_node_in_context(&args[0], cell);
|
||||
let expected_range = self.evaluate_node_in_context(&args[1], cell);
|
||||
|
||||
let (width, height, values_left, values_right) = match (actual_range, expected_range) {
|
||||
(
|
||||
CalcResult::Range {
|
||||
left: l1,
|
||||
right: r1,
|
||||
},
|
||||
CalcResult::Range {
|
||||
left: l2,
|
||||
right: r2,
|
||||
},
|
||||
) => {
|
||||
if l1.sheet != l2.sheet {
|
||||
return 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 CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges must be of the same shape".to_string(),
|
||||
);
|
||||
}
|
||||
let values_left = match self.values_from_range(l1, r1) {
|
||||
Err(error) => {
|
||||
return error;
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
let values_right = match self.values_from_range(l2, r2) {
|
||||
Err(error) => {
|
||||
return error;
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
(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 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 CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in first array: {:?}", error),
|
||||
);
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
let values_right = match self.values_from_range(l2, r2) {
|
||||
Err(error) => {
|
||||
return error;
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
(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 CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Range and array must be of the same shape".to_string(),
|
||||
);
|
||||
}
|
||||
let values_left = match self.values_from_range(l1, r1) {
|
||||
Err(error) => {
|
||||
return error;
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
let values_right = match self.values_from_array(right) {
|
||||
Err(error) => {
|
||||
return 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 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 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 CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
);
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
(rows1, cols1, values_left, values_right)
|
||||
}
|
||||
_ => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Both arguments must be ranges or arrays".to_string(),
|
||||
);
|
||||
}
|
||||
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());
|
||||
|
||||
227
base/src/functions/statistical/correl.rs
Normal file
227
base/src/functions/statistical/correl.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// CORREL(array1, array2) - Returns the correlation coefficient of two data sets
|
||||
pub(crate) fn fn_correl(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_, _, values_left, values_right) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let mut n = 0.0;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_x2 = 0.0;
|
||||
let mut sum_y2 = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
if let (Some(x), Some(y)) = (x_opt, y_opt) {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_y2 += y * y;
|
||||
sum_xy += x * y;
|
||||
}
|
||||
}
|
||||
|
||||
// Need at least 2 valid pairs
|
||||
if n < 2.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"CORREL requires at least two numeric data points in each range".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let denom_x = n * sum_x2 - sum_x * sum_x;
|
||||
let denom_y = n * sum_y2 - sum_y * sum_y;
|
||||
let denom = (denom_x * denom_y).sqrt();
|
||||
|
||||
if denom == 0.0 || !denom.is_finite() {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Division by zero in CORREL".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let r = num / denom;
|
||||
CalcResult::Number(r)
|
||||
}
|
||||
|
||||
// SLOPE(known_y's, known_x's) - Returns the slope of the linear regression line
|
||||
pub(crate) fn fn_slope(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let mut n = 0.0;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_x2 = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
|
||||
let len = values_y.len().min(values_x.len());
|
||||
for i in 0..len {
|
||||
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_xy += x * y;
|
||||
}
|
||||
}
|
||||
|
||||
if n < 2.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"SLOPE requires at least two numeric data points".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let denom = n * sum_x2 - sum_x * sum_x;
|
||||
if denom == 0.0 || !denom.is_finite() {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Division by zero in SLOPE".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let slope = num / denom;
|
||||
|
||||
CalcResult::Number(slope)
|
||||
}
|
||||
|
||||
// INTERCEPT(known_y's, known_x's) - Returns the y-intercept of the linear regression line
|
||||
pub(crate) fn fn_intercept(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let mut n = 0.0;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_x2 = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
|
||||
let len = values_y.len().min(values_x.len());
|
||||
for i in 0..len {
|
||||
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_xy += x * y;
|
||||
}
|
||||
}
|
||||
|
||||
if n < 2.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"INTERCEPT requires at least two numeric data points".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let denom = n * sum_x2 - sum_x * sum_x;
|
||||
if denom == 0.0 || !denom.is_finite() {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Division by zero in INTERCEPT".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let slope = num / denom;
|
||||
let intercept = (sum_y - slope * sum_x) / n;
|
||||
|
||||
CalcResult::Number(intercept)
|
||||
}
|
||||
|
||||
// STEYX(known_y's, known_x's) - Returns the standard error of the predicted y-values
|
||||
pub(crate) fn fn_steyx(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let mut n = 0.0;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_x2 = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
|
||||
// We need the actual pairs again later for residuals
|
||||
let mut pairs: Vec<(f64, f64)> = Vec::new();
|
||||
|
||||
let len = values_y.len().min(values_x.len());
|
||||
for i in 0..len {
|
||||
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_xy += x * y;
|
||||
pairs.push((x, y));
|
||||
}
|
||||
}
|
||||
|
||||
// Need at least 3 points for STEYX (n - 2 in denominator)
|
||||
if n < 3.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"STEYX requires at least three numeric data points".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let denom = n * sum_x2 - sum_x * sum_x;
|
||||
if denom == 0.0 || !denom.is_finite() {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Division by zero in STEYX".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let slope = num / denom;
|
||||
let intercept = (sum_y - slope * sum_x) / n;
|
||||
|
||||
// Sum of squared residuals: Σ (y - ŷ)^2, ŷ = intercept + slope * x
|
||||
let mut sse = 0.0;
|
||||
for (x, y) in pairs {
|
||||
let y_hat = intercept + slope * x;
|
||||
let diff = y - y_hat;
|
||||
sse += diff * diff;
|
||||
}
|
||||
|
||||
let dof = n - 2.0;
|
||||
if dof <= 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"STEYX has non-positive degrees of freedom".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let sey = (sse / dof).sqrt();
|
||||
if !sey.is_finite() {
|
||||
return CalcResult::new_error(Error::DIV, cell, "Numerical error in STEYX".to_string());
|
||||
}
|
||||
|
||||
CalcResult::Number(sey)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::parser::ArrayNode;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
@@ -6,77 +8,219 @@ use crate::{
|
||||
};
|
||||
|
||||
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;
|
||||
fn for_each_value<F>(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
mut f: F,
|
||||
) -> Result<(), CalcResult>
|
||||
where
|
||||
F: FnMut(f64),
|
||||
{
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
sum += value;
|
||||
f(value);
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
} else {
|
||||
sum += if b { 1.0 } else { 0.0 };
|
||||
count += 1.0;
|
||||
CalcResult::Boolean(value) => {
|
||||
if !matches!(arg, Node::ReferenceKind { .. }) {
|
||||
f(if value { 1.0 } else { 0.0 });
|
||||
}
|
||||
}
|
||||
CalcResult::String(value) => {
|
||||
if !matches!(arg, Node::ReferenceKind { .. }) {
|
||||
if let Some(parsed) = self.cast_number(&value) {
|
||||
f(parsed);
|
||||
} else {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Argument cannot be cast into number".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Array(array) => {
|
||||
for row in array {
|
||||
for value in row {
|
||||
match value {
|
||||
ArrayNode::Number(value) => {
|
||||
f(value);
|
||||
}
|
||||
ArrayNode::Boolean(b) => {
|
||||
f(if b { 1.0 } else { 0.0 });
|
||||
}
|
||||
ArrayNode::Error(error) => {
|
||||
return Err(CalcResult::Error {
|
||||
error,
|
||||
origin: cell,
|
||||
message: "Error in array".to_string(),
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
// ignore non-numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
return Err(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) {
|
||||
|
||||
for row in left.row..=right.row {
|
||||
for column in left.column..=right.column {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
sum += value;
|
||||
f(value);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
error @ CalcResult::Error { .. } => return Err(error),
|
||||
CalcResult::Range { .. } => {
|
||||
return CalcResult::new_error(
|
||||
return Err(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(),
|
||||
};
|
||||
error @ CalcResult::Error { .. } => return Err(error),
|
||||
// Everything else is ignored
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn for_each_value_a<F>(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
mut f: F,
|
||||
) -> Result<(), CalcResult>
|
||||
where
|
||||
F: FnMut(f64),
|
||||
{
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
f(value);
|
||||
}
|
||||
CalcResult::Boolean(value) => {
|
||||
if !matches!(arg, Node::ReferenceKind { .. }) {
|
||||
f(if value { 1.0 } else { 0.0 });
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Ignore everything else
|
||||
CalcResult::String(value) => {
|
||||
if !matches!(arg, Node::ReferenceKind { .. }) {
|
||||
if let Some(parsed) = self.cast_number(&value) {
|
||||
f(parsed);
|
||||
} else {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Argument cannot be cast into number".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
CalcResult::Array(array) => {
|
||||
for row in array {
|
||||
for value in row {
|
||||
match value {
|
||||
ArrayNode::Number(value) => {
|
||||
f(value);
|
||||
}
|
||||
ArrayNode::Boolean(b) => {
|
||||
f(if b { 1.0 } else { 0.0 });
|
||||
}
|
||||
ArrayNode::String(_) => {
|
||||
f(0.0);
|
||||
}
|
||||
ArrayNode::Error(error) => {
|
||||
return Err(CalcResult::Error {
|
||||
error,
|
||||
origin: cell,
|
||||
message: "Error in array".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
for row in left.row..=right.row {
|
||||
for column in left.column..=right.column {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
f(value);
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
f(if b { 1.0 } else { 0.0 });
|
||||
}
|
||||
CalcResult::String(_) => {
|
||||
f(0.0);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return Err(error),
|
||||
CalcResult::Range { .. } => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
"Unexpected Range".to_string(),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return Err(error),
|
||||
// Everything else is ignored
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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;
|
||||
if let Err(e) = self.for_each_value(args, cell, |f| {
|
||||
count += 1.0;
|
||||
sum += f;
|
||||
}) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if count == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
@@ -86,6 +230,7 @@ impl Model {
|
||||
}
|
||||
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);
|
||||
@@ -443,4 +588,484 @@ impl Model {
|
||||
|
||||
CalcResult::Number(sum_abs_dev / n)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_median(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut values: Vec<f64> = Vec::new();
|
||||
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if values.is_empty() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "No numeric values for MEDIAN".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
|
||||
|
||||
let n = values.len();
|
||||
let median = if n % 2 == 1 {
|
||||
// odd
|
||||
values[n / 2]
|
||||
} else {
|
||||
// even: average of the two middle values
|
||||
let a = values[(n / 2) - 1];
|
||||
let b = values[n / 2];
|
||||
(a + b) / 2.0
|
||||
};
|
||||
|
||||
CalcResult::Number(median)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_harmean(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut values: Vec<f64> = Vec::new();
|
||||
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if values.is_empty() {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Division by Zero".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// Excel HARMEAN: all values must be > 0
|
||||
if values.iter().any(|&v| v <= 0.0) {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "HARMEAN requires strictly positive values".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let n = values.len() as f64;
|
||||
let sum_recip: f64 = values.iter().map(|v| 1.0 / v).sum();
|
||||
|
||||
if sum_recip == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Division by Zero".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(n / sum_recip)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_mina(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut mina: Option<f64> = None;
|
||||
if let Err(e) = self.for_each_value_a(args, cell, |f| {
|
||||
if let Some(m) = mina {
|
||||
mina = Some(m.min(f));
|
||||
} else {
|
||||
mina = Some(f);
|
||||
}
|
||||
}) {
|
||||
return e;
|
||||
}
|
||||
if let Some(mina) = mina {
|
||||
CalcResult::Number(mina)
|
||||
} else {
|
||||
CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "No numeric values for MINA".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fn_maxa(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut maxa: Option<f64> = None;
|
||||
if let Err(e) = self.for_each_value_a(args, cell, |f| {
|
||||
if let Some(m) = maxa {
|
||||
maxa = Some(m.max(f));
|
||||
} else {
|
||||
maxa = Some(f);
|
||||
}
|
||||
}) {
|
||||
return e;
|
||||
}
|
||||
if let Some(maxa) = maxa {
|
||||
CalcResult::Number(maxa)
|
||||
} else {
|
||||
CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "No numeric values for MAXA".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fn_skew(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
// Sample skewness (Excel SKEW)
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut values: Vec<f64> = Vec::new();
|
||||
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
|
||||
return e;
|
||||
}
|
||||
|
||||
let n = values.len();
|
||||
if n < 3 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "SKEW requires at least 3 data points".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let n_f = n as f64;
|
||||
let mean = values.iter().sum::<f64>() / n_f;
|
||||
|
||||
let mut m2 = 0.0;
|
||||
for &x in &values {
|
||||
let d = x - mean;
|
||||
m2 += d * d;
|
||||
}
|
||||
|
||||
if m2 == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Zero variance in SKEW".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let s = (m2 / (n_f - 1.0)).sqrt();
|
||||
|
||||
let mut sum_cubed = 0.0;
|
||||
for &x in &values {
|
||||
let z = (x - mean) / s;
|
||||
sum_cubed += z * z * z;
|
||||
}
|
||||
|
||||
let skew = (n_f / ((n_f - 1.0) * (n_f - 2.0))) * sum_cubed;
|
||||
CalcResult::Number(skew)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_skew_p(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
// Population skewness (Excel SKEW.P)
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut values: Vec<f64> = Vec::new();
|
||||
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
|
||||
return e;
|
||||
}
|
||||
|
||||
let n = values.len();
|
||||
if n < 2 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "SKEW.P requires at least 2 data points".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let n_f = n as f64;
|
||||
let mean = values.iter().sum::<f64>() / n_f;
|
||||
|
||||
let mut m2 = 0.0;
|
||||
for &x in &values {
|
||||
let d = x - mean;
|
||||
m2 += d * d;
|
||||
}
|
||||
|
||||
if m2 == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Zero variance in SKEW.P".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let sigma = (m2 / n_f).sqrt();
|
||||
|
||||
let mut sum_cubed = 0.0;
|
||||
for &x in &values {
|
||||
let z = (x - mean) / sigma;
|
||||
sum_cubed += z * z * z;
|
||||
}
|
||||
|
||||
let skew_p = sum_cubed / n_f;
|
||||
CalcResult::Number(skew_p)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_kurt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut values: Vec<f64> = Vec::new();
|
||||
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
|
||||
return e;
|
||||
}
|
||||
|
||||
let n = values.len();
|
||||
if n < 4 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "KURT requires at least 4 data points".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let n_f = n as f64;
|
||||
let mean = values.iter().sum::<f64>() / n_f;
|
||||
|
||||
let mut m2 = 0.0;
|
||||
for &x in &values {
|
||||
let d = x - mean;
|
||||
m2 += d * d;
|
||||
}
|
||||
|
||||
if m2 == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Zero variance in KURT".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let s = (m2 / (n_f - 1.0)).sqrt();
|
||||
|
||||
let mut sum_fourth = 0.0;
|
||||
for &x in &values {
|
||||
let z = (x - mean) / s;
|
||||
sum_fourth += z * z * z * z;
|
||||
}
|
||||
|
||||
let term1 = (n_f * (n_f + 1.0)) / ((n_f - 1.0) * (n_f - 2.0) * (n_f - 3.0)) * sum_fourth;
|
||||
let term2 = 3.0 * (n_f - 1.0) * (n_f - 1.0) / ((n_f - 2.0) * (n_f - 3.0));
|
||||
|
||||
let kurt = term1 - term2;
|
||||
CalcResult::Number(kurt)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_large(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let values = match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::Array(array) => match self.values_from_array(array) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: format!("Unsupported array argument: {}", e),
|
||||
}
|
||||
}
|
||||
},
|
||||
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return e,
|
||||
},
|
||||
CalcResult::Boolean(value) => {
|
||||
if !matches!(args[0], Node::ReferenceKind { .. }) {
|
||||
vec![Some(if value { 1.0 } else { 0.0 })]
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
CalcResult::Number(value) => {
|
||||
if !matches!(args[0], Node::ReferenceKind { .. }) {
|
||||
vec![Some(value)]
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
CalcResult::String(value) => {
|
||||
if !matches!(args[0], Node::ReferenceKind { .. }) {
|
||||
if let Some(parsed) = self.cast_number(&value) {
|
||||
vec![Some(parsed)]
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let k = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(s) => return s,
|
||||
};
|
||||
if k < 1.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "K must be >= 1".to_string(),
|
||||
};
|
||||
}
|
||||
let mut numeric_values: Vec<f64> = values.into_iter().flatten().collect();
|
||||
if numeric_values.is_empty() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "No numeric values for LARGE".to_string(),
|
||||
};
|
||||
}
|
||||
numeric_values.sort_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
let k_usize = k as usize;
|
||||
if k_usize > numeric_values.len() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "K is larger than the number of data points".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(numeric_values[k_usize - 1])
|
||||
}
|
||||
|
||||
pub(crate) fn fn_small(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let values = match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::Array(array) => match self.values_from_array(array) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: format!("Unsupported array argument: {}", e),
|
||||
}
|
||||
}
|
||||
},
|
||||
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return e,
|
||||
},
|
||||
CalcResult::Boolean(value) => {
|
||||
if !matches!(args[0], Node::ReferenceKind { .. }) {
|
||||
vec![Some(if value { 1.0 } else { 0.0 })]
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
CalcResult::Number(value) => {
|
||||
if !matches!(args[0], Node::ReferenceKind { .. }) {
|
||||
vec![Some(value)]
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
CalcResult::String(value) => {
|
||||
if !matches!(args[0], Node::ReferenceKind { .. }) {
|
||||
if let Some(parsed) = self.cast_number(&value) {
|
||||
vec![Some(parsed)]
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let k = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
if k < 1.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "K must be >= 1".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut numeric_values: Vec<f64> = values.into_iter().flatten().collect();
|
||||
if numeric_values.is_empty() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "No numeric values for SMALL".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// For SMALL, sort ascending
|
||||
numeric_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
|
||||
|
||||
let k_usize = k as usize;
|
||||
if k_usize > numeric_values.len() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "K is larger than the number of data points".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(numeric_values[k_usize - 1])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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,
|
||||
};
|
||||
@@ -296,4 +297,122 @@ impl Model {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
39
base/src/functions/statistical/gauss.rs
Normal file
39
base/src/functions/statistical/gauss.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use statrs::distribution::{ContinuousCDF, Normal};
|
||||
|
||||
use crate::expressions::token::Error;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{calc_result::CalcResult, expressions::parser::Node, model::Model};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_gauss(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let z = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let dist = match Normal::new(0.0, 1.0) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Failed to construct standard normal distribution".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let result = dist.cdf(z) - 0.5;
|
||||
|
||||
if !result.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for GAUSS".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
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;
|
||||
@@ -15,6 +17,7 @@ mod normal;
|
||||
mod pearson;
|
||||
mod phi;
|
||||
mod poisson;
|
||||
mod rank_eq_avg;
|
||||
mod standard_dev;
|
||||
mod standardize;
|
||||
mod t_dist;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::functions::statistical::chisq::is_same_shape_or_1d;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
@@ -7,176 +6,9 @@ use crate::{
|
||||
impl Model {
|
||||
// PEARSON(array1, array2)
|
||||
pub(crate) fn fn_pearson(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let left_arg = self.evaluate_node_in_context(&args[0], cell);
|
||||
let right_arg = self.evaluate_node_in_context(&args[1], cell);
|
||||
|
||||
let (values_left, values_right) = match (left_arg, right_arg) {
|
||||
(
|
||||
CalcResult::Range {
|
||||
left: l1,
|
||||
right: r1,
|
||||
},
|
||||
CalcResult::Range {
|
||||
left: l2,
|
||||
right: r2,
|
||||
},
|
||||
) => {
|
||||
if l1.sheet != l2.sheet {
|
||||
return 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 CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges must be of the same shape".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let values_left = match self.values_from_range(l1, r1) {
|
||||
Err(error) => return error,
|
||||
Ok(v) => v,
|
||||
};
|
||||
let values_right = match self.values_from_range(l2, r2) {
|
||||
Err(error) => return error,
|
||||
Ok(v) => v,
|
||||
};
|
||||
|
||||
(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 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 CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in first array: {:?}", error),
|
||||
);
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
let values_right = match self.values_from_range(l2, r2) {
|
||||
Err(error) => return error,
|
||||
Ok(v) => v,
|
||||
};
|
||||
|
||||
(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 CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Range and array must be of the same shape".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let values_left = match self.values_from_range(l1, r1) {
|
||||
Err(error) => return error,
|
||||
Ok(v) => v,
|
||||
};
|
||||
let values_right = match self.values_from_array(right) {
|
||||
Err(error) => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
);
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
|
||||
(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 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 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 CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
);
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
|
||||
(values_left, values_right)
|
||||
}
|
||||
_ => {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Both arguments must be ranges or arrays".to_string(),
|
||||
);
|
||||
}
|
||||
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)
|
||||
@@ -228,8 +60,54 @@ impl Model {
|
||||
}
|
||||
|
||||
let denom = (denom_x * denom_y).sqrt();
|
||||
let r = num / denom;
|
||||
|
||||
CalcResult::Number(r)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
202
base/src/functions/statistical/rank_eq_avg.rs
Normal file
202
base/src/functions/statistical/rank_eq_avg.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// Helper to collect numeric values from the 2nd argument of RANK.*
|
||||
fn collect_rank_values(
|
||||
&mut self,
|
||||
arg: &Node,
|
||||
cell: CellReferenceIndex,
|
||||
) -> Result<Vec<f64>, CalcResult> {
|
||||
let values = match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Array(array) => match self.values_from_array(array) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: format!("Unsupported array argument: {}", e),
|
||||
})
|
||||
}
|
||||
},
|
||||
CalcResult::Range { left, right } => self.values_from_range(left, right)?,
|
||||
CalcResult::Boolean(value) => {
|
||||
if !matches!(arg, Node::ReferenceKind { .. }) {
|
||||
vec![Some(if value { 1.0 } else { 0.0 })]
|
||||
} else {
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let numeric_values: Vec<f64> = values.into_iter().flatten().collect();
|
||||
Ok(numeric_values)
|
||||
}
|
||||
|
||||
// RANK.EQ(number, ref, [order])
|
||||
pub(crate) fn fn_rank_eq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if !(2..=3).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
// number
|
||||
let number = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// ref
|
||||
let mut values = match self.collect_rank_values(&args[1], cell) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if values.is_empty() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "No numeric values for RANK.EQ".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// order: default 0 (descending)
|
||||
let order = if args.len() == 2 {
|
||||
0.0
|
||||
} else {
|
||||
match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
}
|
||||
};
|
||||
|
||||
values.retain(|v| !v.is_nan());
|
||||
|
||||
// "better" = greater (descending) or smaller (ascending)
|
||||
let mut better = 0;
|
||||
let mut equal = 0;
|
||||
|
||||
if order == 0.0 {
|
||||
// descending
|
||||
for v in &values {
|
||||
if *v > number {
|
||||
better += 1;
|
||||
} else if *v == number {
|
||||
equal += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ascending
|
||||
for v in &values {
|
||||
if *v < number {
|
||||
better += 1;
|
||||
} else if *v == number {
|
||||
equal += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if equal == 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Number not found in reference for RANK.EQ".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let rank = (better as f64) + 1.0;
|
||||
CalcResult::Number(rank)
|
||||
}
|
||||
|
||||
// RANK.AVG(number, ref, [order])
|
||||
pub(crate) fn fn_rank_avg(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if !(2..=3).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
// number
|
||||
let number = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// ref
|
||||
let mut values = match self.collect_rank_values(&args[1], cell) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if values.is_empty() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "No numeric values for RANK.AVG".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// order: default 0 (descending)
|
||||
let order = if args.len() == 2 {
|
||||
0.0
|
||||
} else {
|
||||
match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
}
|
||||
};
|
||||
|
||||
values.retain(|v| !v.is_nan());
|
||||
|
||||
// > or < depending on order
|
||||
let mut better = 0;
|
||||
let mut equal = 0;
|
||||
|
||||
if order == 0.0 {
|
||||
// descending
|
||||
for v in &values {
|
||||
if *v > number {
|
||||
better += 1;
|
||||
} else if *v == number {
|
||||
equal += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ascending
|
||||
for v in &values {
|
||||
if *v < number {
|
||||
better += 1;
|
||||
} else if *v == number {
|
||||
equal += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if equal == 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Number not found in reference for RANK.AVG".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// For ties, average of the ranks. If the equal values occupy positions
|
||||
// (better+1) ..= (better+equal), the average is:
|
||||
// better + (equal + 1) / 2
|
||||
let better_f = better as f64;
|
||||
let equal_f = equal as f64;
|
||||
let rank = better_f + (equal_f + 1.0) / 2.0;
|
||||
|
||||
CalcResult::Number(rank)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ fn mean(xs: &[f64]) -> f64 {
|
||||
s / (n as f64)
|
||||
}
|
||||
|
||||
fn sample_var(xs: &[f64]) -> f64 {
|
||||
pub(crate) fn sample_var(xs: &[f64]) -> f64 {
|
||||
let n = xs.len();
|
||||
if n < 2 {
|
||||
return 0.0;
|
||||
|
||||
@@ -18,7 +18,6 @@ mod test_fn_concatenate;
|
||||
mod test_fn_count;
|
||||
mod test_fn_day;
|
||||
mod test_fn_exact;
|
||||
mod test_fn_fact;
|
||||
mod test_fn_financial;
|
||||
mod test_fn_formulatext;
|
||||
mod test_fn_if;
|
||||
@@ -46,6 +45,7 @@ mod test_set_user_input;
|
||||
mod test_sheet_markup;
|
||||
mod test_sheets;
|
||||
mod test_styles;
|
||||
mod test_sumsq;
|
||||
mod test_trigonometric;
|
||||
mod test_true_false;
|
||||
mod test_weekday_return_types;
|
||||
@@ -66,6 +66,7 @@ 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;
|
||||
|
||||
@@ -7,7 +7,9 @@ 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;
|
||||
|
||||
35
base/src/test/statistical/test_fn_f_test.rs
Normal file
35
base/src/test/statistical/test_fn_f_test.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_f_test_sanity() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid call
|
||||
model._set("A1", "=F.TEST(A2:A7, B2:B7)");
|
||||
model._set("A2", "9");
|
||||
model._set("A3", "12");
|
||||
model._set("A4", "14");
|
||||
model._set("A5", "16");
|
||||
model._set("A6", "18");
|
||||
model._set("A7", "20");
|
||||
model._set("B2", "11");
|
||||
model._set("B3", "10");
|
||||
model._set("B4", "15");
|
||||
model._set("B5", "17");
|
||||
model._set("B6", "19");
|
||||
model._set("B7", "21");
|
||||
|
||||
// Too few args
|
||||
model._set("A8", "=F.TEST(A2:A7)");
|
||||
|
||||
// Too many args
|
||||
model._set("A9", "=F.TEST(A2:A7, B2:B7, C2:C7)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.859284302");
|
||||
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A9"), *"#ERROR!");
|
||||
}
|
||||
35
base/src/test/statistical/test_fn_gauss.rs
Normal file
35
base/src/test/statistical/test_fn_gauss.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_gauss_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=GAUSS(-3)");
|
||||
model._set("A2", "=GAUSS(-2.3)");
|
||||
model._set("A3", "=GAUSS(-1.7)");
|
||||
model._set("A4", "=GAUSS(0)");
|
||||
model._set("A5", "=GAUSS(0.5)");
|
||||
model._set("A6", "=GAUSS(1)");
|
||||
model._set("A7", "=GAUSS(1.3)");
|
||||
model._set("A8", "=GAUSS(3)");
|
||||
model._set("A9", "=GAUSS(4)");
|
||||
|
||||
model._set("G6", "=GAUSS()");
|
||||
model._set("G7", "=GAUSS(1, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"-0.498650102");
|
||||
assert_eq!(model._get_text("A2"), *"-0.48927589");
|
||||
assert_eq!(model._get_text("A3"), *"-0.455434537");
|
||||
assert_eq!(model._get_text("A4"), *"0");
|
||||
assert_eq!(model._get_text("A5"), *"0.191462461");
|
||||
assert_eq!(model._get_text("A6"), *"0.341344746");
|
||||
assert_eq!(model._get_text("A7"), *"0.403199515");
|
||||
assert_eq!(model._get_text("A8"), *"0.498650102");
|
||||
assert_eq!(model._get_text("A9"), *"0.499968329");
|
||||
|
||||
assert_eq!(model._get_text("G6"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("G7"), *"#ERROR!");
|
||||
}
|
||||
35
base/src/test/test_cell_info_n_sheets
Normal file
35
base/src/test/test_cell_info_n_sheets
Normal file
@@ -0,0 +1,35 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=CELL("address",A1)");
|
||||
model._set("A2", "=CELL()");
|
||||
|
||||
model._set("A3", "=INFO("system")");
|
||||
model._set("A4", "=INFO()");
|
||||
|
||||
model._set("A5", "=N(TRUE)");
|
||||
model._set("A6", "=N()");
|
||||
model._set("A7", "=N(1, 2)");
|
||||
|
||||
model._set("A8", "=SHEETS()");
|
||||
model._set("A9", "=SHEETS(1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"$A$1");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#N/IMPL!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A5"), *"1");
|
||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A8"), *"1");
|
||||
assert_eq!(model._get_text("A9"), *"#N/IMPL!");
|
||||
}
|
||||
123
base/src/test/test_floor.rs
Normal file
123
base/src/test/test_floor.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_floor_floating_point_precision() {
|
||||
// This test specifically checks the floating-point precision bug fix
|
||||
// Bug: FLOOR(7.1, 0.1) was returning 7.0 instead of 7.1
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// FLOOR tests
|
||||
model._set("C5", "=FLOOR(7.1, 0.1)");
|
||||
model._set("H7", "=FLOOR(-7.1, -0.1)");
|
||||
|
||||
// FLOOR.PRECISE tests
|
||||
model._set("C53", "=FLOOR.PRECISE(7.1, 0.1)");
|
||||
model._set("H53", "=FLOOR.PRECISE(7.1, -0.1)");
|
||||
|
||||
// FLOOR.MATH tests
|
||||
model._set("C101", "=FLOOR.MATH(7.1, 0.1)");
|
||||
model._set("H101", "=FLOOR.MATH(7.1, -0.1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// All should return 7.1
|
||||
assert_eq!(model._get_text("C5"), *"7.1");
|
||||
assert_eq!(model._get_text("H7"), *"-7.1");
|
||||
assert_eq!(model._get_text("C53"), *"7.1");
|
||||
assert_eq!(model._get_text("H53"), *"7.1");
|
||||
assert_eq!(model._get_text("C101"), *"7.1");
|
||||
assert_eq!(model._get_text("H101"), *"7.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_additional_precision_cases() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=FLOOR(7.9, 0.1)");
|
||||
model._set("A2", "=FLOOR(2.6, 0.5)");
|
||||
model._set("A3", "=FLOOR(0.3, 0.1)"); // 0.1 + 0.2 type scenario
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"7.9");
|
||||
assert_eq!(model._get_text("A2"), *"2.5");
|
||||
assert_eq!(model._get_text("A3"), *"0.3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_basic_cases() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=FLOOR(3.7, 2)");
|
||||
model._set("A2", "=FLOOR(3.2, 1)");
|
||||
model._set("A3", "=FLOOR(10, 3)");
|
||||
model._set("A4", "=FLOOR(7, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"2");
|
||||
assert_eq!(model._get_text("A2"), *"3");
|
||||
assert_eq!(model._get_text("A3"), *"9");
|
||||
assert_eq!(model._get_text("A4"), *"6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_negative_numbers() {
|
||||
let mut model = new_empty_model();
|
||||
// Both negative: rounds toward zero
|
||||
model._set("A1", "=FLOOR(-2.5, -2)");
|
||||
model._set("A2", "=FLOOR(-11, -3)");
|
||||
|
||||
// Negative number, positive significance: rounds away from zero
|
||||
model._set("A3", "=FLOOR(-11, 3)");
|
||||
model._set("A4", "=FLOOR(-2.5, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"-2");
|
||||
assert_eq!(model._get_text("A2"), *"-9");
|
||||
assert_eq!(model._get_text("A3"), *"-12");
|
||||
assert_eq!(model._get_text("A4"), *"-4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_error_cases() {
|
||||
let mut model = new_empty_model();
|
||||
// Positive number with negative significance should error
|
||||
model._set("A1", "=FLOOR(2.5, -2)");
|
||||
model._set("A2", "=FLOOR(10, -3)");
|
||||
|
||||
// Division by zero
|
||||
model._set("A3", "=FLOOR(5, 0)");
|
||||
|
||||
// Wrong number of arguments
|
||||
model._set("A4", "=FLOOR(5)");
|
||||
model._set("A5", "=FLOOR(5, 1, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A3"), *"#DIV/0!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_edge_cases() {
|
||||
let mut model = new_empty_model();
|
||||
// Zero value
|
||||
model._set("A1", "=FLOOR(0, 5)");
|
||||
model._set("A2", "=FLOOR(0, 0)");
|
||||
|
||||
// Exact multiples
|
||||
model._set("A3", "=FLOOR(10, 5)");
|
||||
model._set("A4", "=FLOOR(9, 3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0");
|
||||
assert_eq!(model._get_text("A2"), *"0");
|
||||
assert_eq!(model._get_text("A3"), *"10");
|
||||
assert_eq!(model._get_text("A4"), *"9");
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn large_numbers() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=FACT(170)");
|
||||
model._set("A2", "=FACTDOUBLE(36)");
|
||||
|
||||
model._set("B1", "=FACT(6)");
|
||||
model._set("B2", "=FACTDOUBLE(6)");
|
||||
|
||||
model._set("C3", "=FACTDOUBLE(15)");
|
||||
|
||||
model._set("F3", "=FACT(-0.1)");
|
||||
model._set("F4", "=FACT(0)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"7.25742E+306");
|
||||
assert_eq!(model._get_text("A2"), *"1.67834E+21");
|
||||
assert_eq!(model._get_text("B1"), *"720");
|
||||
assert_eq!(model._get_text("B2"), *"48");
|
||||
assert_eq!(model._get_text("C3"), *"2027025");
|
||||
|
||||
assert_eq!(model._get_text("F3"), *"#NUM!");
|
||||
assert_eq!(model._get_text("F4"), *"1");
|
||||
}
|
||||
@@ -7,15 +7,24 @@ const TIMESTAMP_2023: i64 = 1679319865208;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
mock_time::set_mock_time(TIMESTAMP_2023);
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A1", "=NOW(1)");
|
||||
model._set("A1", "=NOW(1, 1)");
|
||||
model._set("A2", "=NOW(\"Europe/Berlin\")");
|
||||
model._set("A3", "=NOW(\"faketimezone\")");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"#ERROR!",
|
||||
"NOW should not accept arguments"
|
||||
"Wrong number of arguments"
|
||||
);
|
||||
assert_eq!(model._get_text("A2"), *"20/03/2023 14:44:25");
|
||||
assert_eq!(
|
||||
model._get_text("A3"),
|
||||
"#VALUE!",
|
||||
"Invalid timezone: faketimezone"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
17
base/src/test/test_sumsq.rs
Normal file
17
base/src/test/test_sumsq.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=SUMSQ()");
|
||||
model._set("A2", "=SUMSQ(2)");
|
||||
model._set("A3", "=SUMSQ(1, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), *"4");
|
||||
assert_eq!(model._get_text("A3"), *"5");
|
||||
}
|
||||
@@ -11,7 +11,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
||||
|
||||
| Function | Status | Documentation |
|
||||
| ---------- | ---------------------------------------------- | ------------- |
|
||||
| CELL | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| CELL | <Badge type="tip" text="Available" /> | – |
|
||||
| ERROR.TYPE | <Badge type="tip" text="Available" /> | – |
|
||||
| INFO | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| ISBLANK | <Badge type="tip" text="Available" /> | – |
|
||||
@@ -20,7 +20,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
||||
| ISEVEN | <Badge type="tip" text="Available" /> | – |
|
||||
| ISFORMULA | <Badge type="tip" text="Available" /> | – |
|
||||
| ISLOGICAL | <Badge type="tip" text="Available" /> | – |
|
||||
| ISNA | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| ISNA | <Badge type="tip" text="Available" /> | – |
|
||||
| ISNONTEXT | <Badge type="tip" text="Available" /> | – |
|
||||
| ISNUMBER | <Badge type="tip" text="Available" /> | – |
|
||||
| ISODD | <Badge type="tip" text="Available" /> | – |
|
||||
@@ -30,5 +30,5 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
||||
| N | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| NA | <Badge type="tip" text="Available" /> | – |
|
||||
| SHEET | <Badge type="tip" text="Available" /> | – |
|
||||
| SHEETS | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| SHEETS | <Badge type="tip" text="Available" /> | – |
|
||||
| TYPE | <Badge type="tip" text="Available" /> | – |
|
||||
|
||||
@@ -7,6 +7,5 @@ lang: en-US
|
||||
# CELL
|
||||
|
||||
::: warning
|
||||
🚧 This function is not yet available in IronCalc.
|
||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||
:::
|
||||
@@ -7,6 +7,5 @@ lang: en-US
|
||||
# N
|
||||
|
||||
::: warning
|
||||
🚧 This function is not yet available in IronCalc.
|
||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||
:::
|
||||
@@ -7,6 +7,5 @@ lang: en-US
|
||||
# SHEETS
|
||||
|
||||
::: warning
|
||||
🚧 This function is not yet available in IronCalc.
|
||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||
:::
|
||||
@@ -6,26 +6,33 @@ lang: en-US
|
||||
|
||||
# COLUMN function
|
||||
## Overview
|
||||
The COLUMN Function in IronCalc is a lookup & reference formula that is used to query and return the column number of a referenced Column or Cell.
|
||||
The COLUMN Function in IronCalc is a lookup & reference formula that is used to query and return the column number of a referenced column or cell.
|
||||
## Usage
|
||||
### Syntax
|
||||
**COLUMN(<span title="Reference" style="color:#1E88E5">reference</span>) => <span title="Number" style="color:#1E88E5">column</span>**
|
||||
### Argument descriptions
|
||||
* *reference* ([cell](/features/value-types#references), [optional](/features/optional-arguments.md)). The number of the cell you wish to reference the column number of.
|
||||
* *reference* ([cell](/features/value-types#references), [optional](/features/optional-arguments.md)). The cell, column, range, or [Named Range](/web-application/name-manager.html) for which you wish to find the column number.
|
||||
### Additional guidance
|
||||
* When referencing a range of cells, only the column number of the left most cell will be returned.
|
||||
* When referencing a range of cells, only the column number of the leftmost cell will be returned.
|
||||
* You are also able to reference complete columns instead of individual cells.
|
||||
* When using a Named Range as a reference, the reference is not case sensitive.
|
||||
* IronCalc supports the use of both *Absolute* ($A$1) and *Relative* (A1) references.
|
||||
* Cross-sheet references are also supported.
|
||||
### Returned value
|
||||
COLUMN returns the [number](/features/value-types#numbers) of the specific cell or column which is being referenced.
|
||||
COLUMN returns the [number](/features/value-types#numbers) of the specific cell or column which is being referenced. If no reference is included, the column number of the cell where the formula is entered will be returned.
|
||||
### Error conditions
|
||||
* IronCalc currently does not support the referencing of cells with names.
|
||||
* A [#NAME?](/features/error-types.html#name) error is returned if a Named Range being referenced is deleted.
|
||||
* A [#REF!](/features/error-types.html#ref) error is returned if a cell being referenced is deleted.
|
||||
* A [#VALUE!](/features/error-types.html#value) error is returned if a column being referenced is deleted.
|
||||
## Details
|
||||
The COLUMN Function can only be used to display the correlating number of a single column within a Sheet. If you wish to show the number of columns used within a specific range, you can use the COLUMNS Function.
|
||||
The COLUMN Function can only be used to display the correlating number of a single column within a Sheet. If you wish to show the number of columns used within a specific range, you can use the [COLUMNS](/functions/lookup_and_reference/columns) Function.
|
||||
## Examples
|
||||
### No Cell Reference
|
||||
When no cell reference is made, the formula uses **=COLUMN()**. This will then output the column number of the cell where the formula is placed.<br><br>For example, if the formula is placed in cell A1, then "1" will be displayed.
|
||||
When no cell reference is made, the formula uses **=COLUMN()**. This will output the column number of the cell where the formula is entered.<br><br>For example, if the formula is placed in cell A1, then "1" will be displayed.
|
||||
### With Cell Reference
|
||||
When a cell reference is made, the formula uses **=COLUMN([Referenced Cell])**. This will then output the column number of the referenced cell, regardless of where the formula is placed in the sheet.<br><br>For example, if the cell B1 is the referenced cell, "2" will be the output of the formula no matter where it is placed in the sheet.<br><br>**Note:** references do not always have to be specific cells, you can also reference complete columns. For example, **=COLUMN(B:B)** would also result in an output of "2".
|
||||
When a cell reference is made, the formula uses **=COLUMN(<span title="Reference" style="color:#1E88E5">Referenced Cell</span>)**. This will then output the column number of the referenced cell, regardless of where the formula is placed in the sheet.<br><br>For example, if B1 is the referenced cell, then "2" will be the output of the formula, regardless of where the formula is placed in the sheet.<br><br>**Note:** references do not have to be specific cells, you can also reference complete columns. For example, **=COLUMN(B:B)** would also result in an output of "2".
|
||||
### Range References
|
||||
The COLUMN function can also be used to reference a range of Cells or Columns. In this case only the most left-hand column will be the resulting output.<br><br>For example, **=COLUMN(A1:J1)** will result in the ouput of "1".
|
||||
## 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.
|
||||
@@ -86,7 +86,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
||||
| SUMIF | <Badge type="tip" text="Available" /> | – |
|
||||
| SUMIFS | <Badge type="tip" text="Available" /> | – |
|
||||
| SUMPRODUCT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| SUMSQ | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| SUMSQ | <Badge type="tip" text="Available" /> | – |
|
||||
| SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| SUMX2PY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
|
||||
@@ -7,6 +7,5 @@ lang: en-US
|
||||
# SUMSQ
|
||||
|
||||
::: warning
|
||||
🚧 This function is not yet available in IronCalc.
|
||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||
:::
|
||||
@@ -194,6 +194,10 @@ function getFormulaHTML(
|
||||
} else {
|
||||
html = [<span key="single">{text}</span>];
|
||||
}
|
||||
// Add a trailing character if text ends with newline to ensure selector's height grows
|
||||
if (text.endsWith("\n")) {
|
||||
html.push(<span key="trailing-newline">{"\n"}</span>);
|
||||
}
|
||||
return { html, activeRanges };
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
LAST_ROW,
|
||||
outlineBackgroundColor,
|
||||
outlineColor,
|
||||
outlineEditingColor,
|
||||
ROW_HEIGH_SCALE,
|
||||
} from "../WorksheetCanvas/constants";
|
||||
import WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas";
|
||||
@@ -226,12 +227,14 @@ const Worksheet = forwardRef(
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
workbookState.setSelecting(true);
|
||||
const { row, column } = cell;
|
||||
model.onAreaSelecting(row, column);
|
||||
canvas.renderSheet();
|
||||
refresh();
|
||||
},
|
||||
onAreaSelected: () => {
|
||||
workbookState.setSelecting(false);
|
||||
const styles = workbookState.getCopyStyles();
|
||||
if (styles?.length) {
|
||||
model.onPasteStyles(styles);
|
||||
@@ -505,8 +508,8 @@ const RowResizeGuide = styled("div")`
|
||||
|
||||
const AreaOutline = styled("div")`
|
||||
position: absolute;
|
||||
border: 1px solid ${outlineColor};
|
||||
border-radius: 3px;
|
||||
border: 0px solid ${outlineColor};
|
||||
border-radius: 1px;
|
||||
background-color: ${outlineBackgroundColor};
|
||||
`;
|
||||
|
||||
@@ -517,6 +520,7 @@ const CellOutline = styled("div")`
|
||||
word-break: break-word;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
box-shadow: inset 0 0 0 1px white;
|
||||
`;
|
||||
|
||||
const ExtendToOutline = styled("div")`
|
||||
@@ -536,6 +540,8 @@ const EditorWrapper = styled("div")`
|
||||
vertical-align: bottom;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
outline: 3px solid ${outlineEditingColor};
|
||||
z-index: 1000;
|
||||
span {
|
||||
min-width: 1px;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export const gridSeparatorColor = "#E0E0E0";
|
||||
export const defaultTextColor = "#2E414D";
|
||||
|
||||
export const outlineColor = "#F2994A";
|
||||
export const outlineEditingColor = "#FBE0C9";
|
||||
export const outlineBackgroundColor = "#F2994A1A";
|
||||
|
||||
export const LAST_COLUMN = 16_384;
|
||||
|
||||
@@ -26,6 +26,7 @@ export function attachOutlineHandle(
|
||||
background: outlineColor,
|
||||
cursor: "crosshair",
|
||||
borderRadius: "1px",
|
||||
border: `1px solid white`,
|
||||
});
|
||||
|
||||
// cell handle events
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1523,7 +1560,9 @@ export default class WorksheetCanvas {
|
||||
return;
|
||||
}
|
||||
cellOutline.style.visibility = "visible";
|
||||
cellOutlineHandle.style.visibility = "visible";
|
||||
cellOutlineHandle.style.visibility = this.workbookState.isSelecting()
|
||||
? "hidden"
|
||||
: "visible";
|
||||
areaOutline.style.visibility = "visible";
|
||||
|
||||
const [selectedSheet, selectedRow, selectedColumn] =
|
||||
@@ -1582,7 +1621,9 @@ export default class WorksheetCanvas {
|
||||
handleY += this.getRowHeight(selectedSheet, rowStart);
|
||||
} else {
|
||||
areaOutline.style.visibility = "visible";
|
||||
cellOutlineHandle.style.visibility = "visible";
|
||||
cellOutlineHandle.style.visibility = this.workbookState.isSelecting()
|
||||
? "hidden"
|
||||
: "visible";
|
||||
const [areaX, areaY] = this.getCoordinatesByCell(rowStart, columnStart);
|
||||
const [areaWidth, areaHeight] = this.getAreaDimensions(
|
||||
rowStart,
|
||||
@@ -1592,10 +1633,13 @@ export default class WorksheetCanvas {
|
||||
);
|
||||
handleX = areaX + areaWidth;
|
||||
handleY = areaY + areaHeight;
|
||||
const isSelecting = this.workbookState.isSelecting();
|
||||
// Add 1px when selecting to compensate for missing border
|
||||
const borderCompensation = isSelecting ? 1 : 0;
|
||||
areaOutline.style.left = `${areaX - padding - 1}px`;
|
||||
areaOutline.style.top = `${areaY - padding - 1}px`;
|
||||
areaOutline.style.width = `${areaWidth + 2 * padding + 1}px`;
|
||||
areaOutline.style.height = `${areaHeight + 2 * padding + 1}px`;
|
||||
areaOutline.style.width = `${areaWidth + 2 * padding + 1 + borderCompensation}px`;
|
||||
areaOutline.style.height = `${areaHeight + 2 * padding + 1 + borderCompensation}px`;
|
||||
const clipLeft = rowStart < topLeftCell.row && rowStart > frozenRows;
|
||||
const clipTop =
|
||||
columnStart < topLeftCell.column && columnStart > frozenColumns;
|
||||
@@ -1607,7 +1651,9 @@ export default class WorksheetCanvas {
|
||||
clipLeft,
|
||||
clipTop,
|
||||
);
|
||||
areaOutline.style.border = `1px solid ${outlineColor}`;
|
||||
areaOutline.style.border = isSelecting
|
||||
? "none"
|
||||
: `1px solid ${outlineColor}`;
|
||||
// hide the handle if it is out of the visible area
|
||||
if (
|
||||
(rowEnd > frozenRows && rowEnd < topLeftCell.row - 1) ||
|
||||
|
||||
@@ -92,6 +92,7 @@ export class WorkbookState {
|
||||
private copyStyles: AreaStyles | null;
|
||||
private cell: EditingCell | null;
|
||||
private cutRange: CutRange | null;
|
||||
private selecting: boolean;
|
||||
|
||||
constructor() {
|
||||
// the extendTo area is the area we are covering
|
||||
@@ -99,6 +100,15 @@ export class WorkbookState {
|
||||
this.copyStyles = null;
|
||||
this.cell = null;
|
||||
this.cutRange = null;
|
||||
this.selecting = false;
|
||||
}
|
||||
|
||||
isSelecting(): boolean {
|
||||
return this.selecting;
|
||||
}
|
||||
|
||||
setSelecting(value: boolean): void {
|
||||
this.selecting = value;
|
||||
}
|
||||
|
||||
getExtendToArea(): Area | null {
|
||||
|
||||
25
webapp/app.ironcalc.com/frontend/package-lock.json
generated
25
webapp/app.ironcalc.com/frontend/package-lock.json
generated
@@ -91,6 +91,7 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -570,6 +571,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -613,6 +615,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
|
||||
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -2026,6 +2029,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -2122,6 +2126,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.25",
|
||||
"caniuse-lite": "^1.0.30001754",
|
||||
@@ -2691,6 +2696,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -2758,6 +2764,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -2767,6 +2774,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -2978,6 +2986,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -3023,6 +3032,7 @@
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -3113,21 +3123,6 @@
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
|
||||
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,16 +227,19 @@ const Wrapper = styled("div")`
|
||||
`;
|
||||
|
||||
const DRAWER_WIDTH = 264;
|
||||
const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 440;
|
||||
export const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 768;
|
||||
|
||||
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
|
||||
margin-left: ${({ isDrawerOpen }) => (isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`)};
|
||||
width: ${({ isDrawerOpen }) => (isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : "100%")};
|
||||
margin-left: ${({ isDrawerOpen }) =>
|
||||
isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`};
|
||||
width: ${({ isDrawerOpen }) =>
|
||||
isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : "100%"};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
@media (max-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px) {
|
||||
${({ isDrawerOpen }) => isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`}
|
||||
${({ isDrawerOpen }) =>
|
||||
isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -247,7 +250,7 @@ const MobileOverlay = styled("div")`
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
z-index: 1;
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
|
||||
@media (min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE + 1}px) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Model } from "@ironcalc/workbook";
|
||||
import { IconButton, Tooltip } from "@mui/material";
|
||||
import { CloudOff, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
import { MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE } from "../App";
|
||||
import { FileMenu } from "./FileMenu";
|
||||
import { HelpMenu } from "./HelpMenu";
|
||||
import { downloadModel } from "./rpc";
|
||||
@@ -77,7 +78,7 @@ export function FileBar(properties: {
|
||||
{properties.isDrawerOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
|
||||
</DrawerButton>
|
||||
</Tooltip>
|
||||
{width > 440 && (
|
||||
{width > MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE && (
|
||||
<FileMenu
|
||||
newModel={properties.newModel}
|
||||
newModelFromTemplate={properties.newModelFromTemplate}
|
||||
@@ -92,7 +93,7 @@ export function FileBar(properties: {
|
||||
onDelete={properties.onDelete}
|
||||
/>
|
||||
)}
|
||||
{width > 440 && <HelpMenu />}
|
||||
{width > MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE && <HelpMenu />}
|
||||
<WorkbookTitleWrapper>
|
||||
<WorkbookTitle
|
||||
name={properties.model.getName()}
|
||||
@@ -112,7 +113,8 @@ export function FileBar(properties: {
|
||||
<div style={{ fontWeight: "bold" }}>{cloudWarningText2}</div>
|
||||
</div>
|
||||
}
|
||||
placement="bottom-start"
|
||||
placement="bottom"
|
||||
enterTouchDelay={0}
|
||||
enterDelay={500}
|
||||
slotProps={{
|
||||
popper: {
|
||||
|
||||
BIN
xlsx/tests/calc_tests/CELL.xlsx
Normal file
BIN
xlsx/tests/calc_tests/CELL.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/MINA_MAXA.xlsx
Normal file
BIN
xlsx/tests/calc_tests/MINA_MAXA.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/N.xlsx
Normal file
BIN
xlsx/tests/calc_tests/N.xlsx
Normal file
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/calc_tests/RANK_EQ_RANK_AVG.xlsx
Normal file
BIN
xlsx/tests/calc_tests/RANK_EQ_RANK_AVG.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/SHEET_SHEETS.xlsx
Normal file
BIN
xlsx/tests/calc_tests/SHEET_SHEETS.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/SMALL_LARGE.xlsx
Normal file
BIN
xlsx/tests/calc_tests/SMALL_LARGE.xlsx
Normal file
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/calc_tests/SUMX2MY2_SUMX2PY2_SUMXMY2.xlsx
Normal file
BIN
xlsx/tests/calc_tests/SUMX2MY2_SUMX2PY2_SUMXMY2.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/statistical/CORREL_SLOPE_INTERCEPT_RSQ_STEYX.xlsx
Normal file
BIN
xlsx/tests/statistical/CORREL_SLOPE_INTERCEPT_RSQ_STEYX.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/statistical/F_TEST.xlsx
Normal file
BIN
xlsx/tests/statistical/F_TEST.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/statistical/MEADIAN_KURT_SKEW_HARMEAN.xlsx
Normal file
BIN
xlsx/tests/statistical/MEADIAN_KURT_SKEW_HARMEAN.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user