Compare commits
1 Commits
dani/widge
...
fact_gamma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a00ecfdade |
@@ -12,9 +12,6 @@ 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,20 +471,6 @@ 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 {
|
||||
@@ -504,20 +490,6 @@ 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,10 +872,12 @@ 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],
|
||||
@@ -888,6 +890,7 @@ 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),
|
||||
@@ -926,7 +929,6 @@ 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),
|
||||
@@ -987,26 +989,6 @@ 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],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1032,7 +1014,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),
|
||||
Function::Choose => scalar_arguments(args), // static_analysis_choose(args, cell),
|
||||
Function::Column => not_implemented(args),
|
||||
Function::Columns => not_implemented(args),
|
||||
Function::Cos => scalar_arguments(args),
|
||||
@@ -1079,6 +1061,7 @@ 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),
|
||||
@@ -1270,6 +1253,7 @@ 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),
|
||||
@@ -1282,6 +1266,7 @@ 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,
|
||||
@@ -1302,7 +1287,6 @@ 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,
|
||||
@@ -1338,25 +1322,5 @@ 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,7 +4,6 @@ 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;
|
||||
@@ -771,12 +770,12 @@ impl Model {
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
// Returns the current date/time as an Excel serial number in the given timezone.
|
||||
// Returns the current date/time as an Excel serial number in the model's configured timezone.
|
||||
// Used by TODAY() and NOW().
|
||||
pub(crate) fn current_excel_serial_with_timezone(&self, tz: Tz) -> Option<f64> {
|
||||
fn current_excel_serial(&self) -> Option<f64> {
|
||||
let seconds = get_milliseconds_since_epoch() / 1000;
|
||||
DateTime::from_timestamp(seconds, 0).map(|dt| {
|
||||
let local_time = dt.with_timezone(&tz);
|
||||
let local_time = dt.with_timezone(&self.tz);
|
||||
let days_from_1900 = local_time.num_days_from_ce() - EXCEL_DATE_BASE;
|
||||
let fraction = (local_time.num_seconds_from_midnight() as f64) / (60.0 * 60.0 * 24.0);
|
||||
days_from_1900 as f64 + fraction
|
||||
@@ -979,7 +978,7 @@ impl Model {
|
||||
message: "Wrong number of arguments".to_string(),
|
||||
};
|
||||
}
|
||||
match self.current_excel_serial_with_timezone(self.tz) {
|
||||
match self.current_excel_serial() {
|
||||
Some(serial) => CalcResult::Number(serial.floor()),
|
||||
None => CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
@@ -990,35 +989,14 @@ impl Model {
|
||||
}
|
||||
|
||||
pub(crate) fn fn_now(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() > 1 {
|
||||
if !args.is_empty() {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Wrong number of arguments".to_string(),
|
||||
};
|
||||
}
|
||||
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) {
|
||||
match self.current_excel_serial() {
|
||||
Some(serial) => CalcResult::Number(serial),
|
||||
None => CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
|
||||
@@ -1,15 +1,81 @@
|
||||
use crate::cast::NumberOrArray;
|
||||
use crate::constants::{EXCEL_PRECISION, LAST_COLUMN, LAST_ROW};
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::parser::ArrayNode;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::functions::math_util::{from_roman, to_roman_with_form};
|
||||
use crate::number_format::{to_excel_precision, to_precision};
|
||||
use crate::number_format::to_precision;
|
||||
use crate::single_number_fn;
|
||||
use crate::{
|
||||
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()
|
||||
@@ -984,9 +1050,7 @@ impl Model {
|
||||
};
|
||||
}
|
||||
|
||||
// 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;
|
||||
let result = f64::floor(value / significance) * significance;
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
@@ -1123,14 +1187,10 @@ impl Model {
|
||||
}
|
||||
let significance = significance.abs();
|
||||
if value < 0.0 && mode != 0.0 {
|
||||
// Apply Excel precision to handle floating-point rounding errors
|
||||
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||
let result = f64::ceil(ratio) * significance;
|
||||
let result = f64::ceil(value / significance) * significance;
|
||||
CalcResult::Number(result)
|
||||
} else {
|
||||
// Apply Excel precision to handle floating-point rounding errors
|
||||
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||
let result = f64::floor(ratio) * significance;
|
||||
let result = f64::floor(value / significance) * significance;
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
}
|
||||
@@ -1160,9 +1220,7 @@ impl Model {
|
||||
return CalcResult::Number(0.0);
|
||||
}
|
||||
|
||||
// Apply Excel precision to handle floating-point rounding errors
|
||||
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||
let result = f64::floor(ratio) * significance;
|
||||
let result = f64::floor(value / significance) * significance;
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
@@ -1327,35 +1385,78 @@ 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);
|
||||
}
|
||||
let mut acc = 1.0;
|
||||
let mut k = 2.0;
|
||||
while k <= x {
|
||||
acc *= k;
|
||||
k += 1.0;
|
||||
}
|
||||
Ok(acc)
|
||||
});
|
||||
single_number_fn!(fn_factdouble, |x: f64| {
|
||||
let x = x.floor();
|
||||
if x < -1.0 {
|
||||
return Err(Error::NUM);
|
||||
}
|
||||
if x < 0.0 {
|
||||
|
||||
if x == 0.0 {
|
||||
return Ok(1.0);
|
||||
}
|
||||
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;
|
||||
if x < FACT_TABLE.len() as f64 {
|
||||
return Ok(FACT_TABLE[x as usize]);
|
||||
}
|
||||
Ok(acc)
|
||||
|
||||
// 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())
|
||||
});
|
||||
|
||||
single_number_fn!(fn_factdouble, |x: f64| {
|
||||
let x = x.floor();
|
||||
if x <= -1.0 {
|
||||
return Err(Error::NUM);
|
||||
}
|
||||
|
||||
if x <= 1.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]);
|
||||
}
|
||||
|
||||
// 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())
|
||||
});
|
||||
|
||||
single_number_fn!(fn_sign, |f| {
|
||||
if f == 0.0 {
|
||||
Ok(0.0)
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
type TwoMatricesResult = (i32, i32, Vec<Option<f64>>, Vec<Option<f64>>);
|
||||
|
||||
// Helper to check if two shapes are the same or compatible 1D shapes
|
||||
fn is_same_shape_or_1d(rows1: i32, cols1: i32, rows2: i32, cols2: i32) -> bool {
|
||||
(rows1 == rows2 && cols1 == cols2)
|
||||
|| (rows1 == 1 && cols2 == 1 && cols1 == rows2)
|
||||
|| (rows2 == 1 && cols1 == 1 && cols2 == rows1)
|
||||
}
|
||||
|
||||
impl Model {
|
||||
// SUMX2MY2(array_x, array_y) - Returns the sum of the difference of squares
|
||||
pub(crate) fn fn_sumx2my2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let result = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let (_, _, values_left, values_right) = result;
|
||||
|
||||
let mut sum = 0.0;
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
let x = x_opt.unwrap_or(0.0);
|
||||
let y = y_opt.unwrap_or(0.0);
|
||||
sum += x * x - y * y;
|
||||
}
|
||||
|
||||
CalcResult::Number(sum)
|
||||
}
|
||||
|
||||
// SUMX2PY2(array_x, array_y) - Returns the sum of the sum of squares
|
||||
pub(crate) fn fn_sumx2py2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let result = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let (_rows, _cols, values_left, values_right) = result;
|
||||
|
||||
let mut sum = 0.0;
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
let x = x_opt.unwrap_or(0.0);
|
||||
let y = y_opt.unwrap_or(0.0);
|
||||
sum += x * x + y * y;
|
||||
}
|
||||
|
||||
CalcResult::Number(sum)
|
||||
}
|
||||
|
||||
// SUMXMY2(array_x, array_y) - Returns the sum of squares of differences
|
||||
pub(crate) fn fn_sumxmy2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let result = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let (_, _, values_left, values_right) = result;
|
||||
|
||||
let mut sum = 0.0;
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
let x = x_opt.unwrap_or(0.0);
|
||||
let y = y_opt.unwrap_or(0.0);
|
||||
let diff = x - y;
|
||||
sum += diff * diff;
|
||||
}
|
||||
|
||||
CalcResult::Number(sum)
|
||||
}
|
||||
|
||||
// Helper function to extract and validate two matrices (ranges or arrays) with compatible shapes.
|
||||
// Returns (rows, cols, values_left, values_right) or an error.
|
||||
pub(crate) fn fn_get_two_matrices(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReferenceIndex,
|
||||
) -> Result<TwoMatricesResult, CalcResult> {
|
||||
if args.len() != 2 {
|
||||
return Err(CalcResult::new_args_number_error(cell));
|
||||
}
|
||||
let x_range = self.evaluate_node_in_context(&args[0], cell);
|
||||
let y_range = self.evaluate_node_in_context(&args[1], cell);
|
||||
|
||||
let result = match (x_range, y_range) {
|
||||
(
|
||||
CalcResult::Range {
|
||||
left: l1,
|
||||
right: r1,
|
||||
},
|
||||
CalcResult::Range {
|
||||
left: l2,
|
||||
right: r2,
|
||||
},
|
||||
) => {
|
||||
if l1.sheet != l2.sheet {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
));
|
||||
}
|
||||
let rows1 = r1.row - l1.row + 1;
|
||||
let cols1 = r1.column - l1.column + 1;
|
||||
let rows2 = r2.row - l2.row + 1;
|
||||
let cols2 = r2.column - l2.column + 1;
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = self.values_from_range(l1, r1)?;
|
||||
let values_right = self.values_from_range(l2, r2)?;
|
||||
(rows1, cols1, values_left, values_right)
|
||||
}
|
||||
(
|
||||
CalcResult::Array(left),
|
||||
CalcResult::Range {
|
||||
left: l2,
|
||||
right: r2,
|
||||
},
|
||||
) => {
|
||||
let rows2 = r2.row - l2.row + 1;
|
||||
let cols2 = r2.column - l2.column + 1;
|
||||
|
||||
let rows1 = left.len() as i32;
|
||||
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Array and range must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = match self.values_from_array(left) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in first array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
let values_right = self.values_from_range(l2, r2)?;
|
||||
(rows2, cols2, values_left, values_right)
|
||||
}
|
||||
(
|
||||
CalcResult::Range {
|
||||
left: l1,
|
||||
right: r1,
|
||||
},
|
||||
CalcResult::Array(right),
|
||||
) => {
|
||||
let rows1 = r1.row - l1.row + 1;
|
||||
let cols1 = r1.column - l1.column + 1;
|
||||
|
||||
let rows2 = right.len() as i32;
|
||||
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Range and array must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = self.values_from_range(l1, r1)?;
|
||||
let values_right = match self.values_from_array(right) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
(rows1, cols1, values_left, values_right)
|
||||
}
|
||||
(CalcResult::Array(left), CalcResult::Array(right)) => {
|
||||
let rows1 = left.len() as i32;
|
||||
let rows2 = right.len() as i32;
|
||||
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
|
||||
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
|
||||
|
||||
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Arrays must be of the same shape".to_string(),
|
||||
));
|
||||
}
|
||||
let values_left = match self.values_from_array(left) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in first array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
let values_right = match self.values_from_array(right) {
|
||||
Err(error) => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error in second array: {:?}", error),
|
||||
));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
(rows1, cols1, values_left, values_right)
|
||||
}
|
||||
_ => {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Both arguments must be ranges or arrays".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ mod lookup_and_reference;
|
||||
mod macros;
|
||||
mod math_util;
|
||||
mod mathematical;
|
||||
mod mathematical_sum;
|
||||
mod statistical;
|
||||
mod subtotal;
|
||||
mod text;
|
||||
@@ -77,9 +76,6 @@ pub enum Function {
|
||||
Sum,
|
||||
Sumif,
|
||||
Sumifs,
|
||||
Sumx2my2,
|
||||
Sumx2py2,
|
||||
Sumxmy2,
|
||||
Tan,
|
||||
Tanh,
|
||||
Acot,
|
||||
@@ -207,6 +203,7 @@ pub enum Function {
|
||||
ChisqTest,
|
||||
ConfidenceNorm,
|
||||
ConfidenceT,
|
||||
// Correl,
|
||||
CovarianceP,
|
||||
CovarianceS,
|
||||
Devsq,
|
||||
@@ -215,7 +212,7 @@ pub enum Function {
|
||||
FDistRT,
|
||||
FInv,
|
||||
FInvRT,
|
||||
FTest,
|
||||
// FTest,
|
||||
Fisher,
|
||||
FisherInv,
|
||||
// Forecast,
|
||||
@@ -224,18 +221,20 @@ pub enum Function {
|
||||
GammaInv,
|
||||
GammaLn,
|
||||
GammaLnPrecise,
|
||||
Gauss,
|
||||
Harmean,
|
||||
// Gauss,
|
||||
// Growth,
|
||||
// Harmean,
|
||||
HypGeomDist,
|
||||
Kurt,
|
||||
Large,
|
||||
// Intercept,
|
||||
// Kurt,
|
||||
// Large,
|
||||
// Linest,
|
||||
// Logest,
|
||||
LogNormDist,
|
||||
LogNormInv,
|
||||
MaxA,
|
||||
Median,
|
||||
MinA,
|
||||
// MaxA,
|
||||
// Median,
|
||||
// MinA,
|
||||
// ModeMult,
|
||||
// ModeSingl,
|
||||
NegbinomDist,
|
||||
@@ -255,16 +254,19 @@ pub enum Function {
|
||||
// Prob,
|
||||
// QuartileExc,
|
||||
// QuartileInc,
|
||||
RankAvg,
|
||||
RankEq,
|
||||
Skew,
|
||||
SkewP,
|
||||
Small,
|
||||
// RankAvg,
|
||||
// RankEq,
|
||||
// Rsq
|
||||
// Skew,
|
||||
// SkewP,
|
||||
// Slope,
|
||||
// Small,
|
||||
Standardize,
|
||||
StDevP,
|
||||
StDevS,
|
||||
Stdeva,
|
||||
Stdevpa,
|
||||
// Steyx,
|
||||
TDist,
|
||||
TDist2T,
|
||||
TDistRT,
|
||||
@@ -415,16 +417,10 @@ pub enum Function {
|
||||
Dvar,
|
||||
Dvarp,
|
||||
Dstdevp,
|
||||
|
||||
Correl,
|
||||
Rsq,
|
||||
Intercept,
|
||||
Slope,
|
||||
Steyx,
|
||||
}
|
||||
|
||||
impl Function {
|
||||
pub fn into_iter() -> IntoIter<Function, 345> {
|
||||
pub fn into_iter() -> IntoIter<Function, 324> {
|
||||
[
|
||||
Function::And,
|
||||
Function::False,
|
||||
@@ -501,9 +497,6 @@ impl Function {
|
||||
Function::Sum,
|
||||
Function::Sumif,
|
||||
Function::Sumifs,
|
||||
Function::Sumx2my2,
|
||||
Function::Sumx2py2,
|
||||
Function::Sumxmy2,
|
||||
Function::Choose,
|
||||
Function::Column,
|
||||
Function::Columns,
|
||||
@@ -718,7 +711,6 @@ impl Function {
|
||||
Function::FDistRT,
|
||||
Function::FInv,
|
||||
Function::FInvRT,
|
||||
Function::FTest,
|
||||
Function::Fisher,
|
||||
Function::FisherInv,
|
||||
Function::Gamma,
|
||||
@@ -754,23 +746,6 @@ 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()
|
||||
}
|
||||
@@ -862,7 +837,6 @@ 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(),
|
||||
|
||||
@@ -893,9 +867,6 @@ 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(),
|
||||
}
|
||||
@@ -1215,7 +1186,6 @@ 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),
|
||||
@@ -1251,28 +1221,6 @@ 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,
|
||||
}
|
||||
@@ -1535,6 +1483,7 @@ 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"),
|
||||
@@ -1551,6 +1500,7 @@ 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"),
|
||||
@@ -1573,7 +1523,6 @@ 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"),
|
||||
@@ -1607,27 +1556,6 @@ 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1947,7 +1875,6 @@ 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),
|
||||
@@ -1981,26 +1908,6 @@ 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,6 +6,13 @@ 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 {
|
||||
@@ -303,10 +310,171 @@ impl Model {
|
||||
|
||||
// CHISQ.TEST(actual_range, expected_range)
|
||||
pub(crate) fn fn_chisq_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (width, height, values_left, values_right) = match self.fn_get_two_matrices(args, cell)
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(r) => return r,
|
||||
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 mut values = Vec::with_capacity(values_left.len());
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// CORREL(array1, array2) - Returns the correlation coefficient of two data sets
|
||||
pub(crate) fn fn_correl(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_, _, values_left, values_right) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let mut n = 0.0;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_x2 = 0.0;
|
||||
let mut sum_y2 = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
|
||||
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||
if let (Some(x), Some(y)) = (x_opt, y_opt) {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_y2 += y * y;
|
||||
sum_xy += x * y;
|
||||
}
|
||||
}
|
||||
|
||||
// Need at least 2 valid pairs
|
||||
if n < 2.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"CORREL requires at least two numeric data points in each range".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let denom_x = n * sum_x2 - sum_x * sum_x;
|
||||
let denom_y = n * sum_y2 - sum_y * sum_y;
|
||||
let denom = (denom_x * denom_y).sqrt();
|
||||
|
||||
if denom == 0.0 || !denom.is_finite() {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Division by zero in CORREL".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let r = num / denom;
|
||||
CalcResult::Number(r)
|
||||
}
|
||||
|
||||
// SLOPE(known_y's, known_x's) - Returns the slope of the linear regression line
|
||||
pub(crate) fn fn_slope(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let mut n = 0.0;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_x2 = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
|
||||
let len = values_y.len().min(values_x.len());
|
||||
for i in 0..len {
|
||||
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_xy += x * y;
|
||||
}
|
||||
}
|
||||
|
||||
if n < 2.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"SLOPE requires at least two numeric data points".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let denom = n * sum_x2 - sum_x * sum_x;
|
||||
if denom == 0.0 || !denom.is_finite() {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Division by zero in SLOPE".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let slope = num / denom;
|
||||
|
||||
CalcResult::Number(slope)
|
||||
}
|
||||
|
||||
// INTERCEPT(known_y's, known_x's) - Returns the y-intercept of the linear regression line
|
||||
pub(crate) fn fn_intercept(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let mut n = 0.0;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_x2 = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
|
||||
let len = values_y.len().min(values_x.len());
|
||||
for i in 0..len {
|
||||
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_xy += x * y;
|
||||
}
|
||||
}
|
||||
|
||||
if n < 2.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"INTERCEPT requires at least two numeric data points".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let denom = n * sum_x2 - sum_x * sum_x;
|
||||
if denom == 0.0 || !denom.is_finite() {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Division by zero in INTERCEPT".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let slope = num / denom;
|
||||
let intercept = (sum_y - slope * sum_x) / n;
|
||||
|
||||
CalcResult::Number(intercept)
|
||||
}
|
||||
|
||||
// STEYX(known_y's, known_x's) - Returns the standard error of the predicted y-values
|
||||
pub(crate) fn fn_steyx(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let mut n = 0.0;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_x2 = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
|
||||
// We need the actual pairs again later for residuals
|
||||
let mut pairs: Vec<(f64, f64)> = Vec::new();
|
||||
|
||||
let len = values_y.len().min(values_x.len());
|
||||
for i in 0..len {
|
||||
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_xy += x * y;
|
||||
pairs.push((x, y));
|
||||
}
|
||||
}
|
||||
|
||||
// Need at least 3 points for STEYX (n - 2 in denominator)
|
||||
if n < 3.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"STEYX requires at least three numeric data points".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let denom = n * sum_x2 - sum_x * sum_x;
|
||||
if denom == 0.0 || !denom.is_finite() {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"Division by zero in STEYX".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let slope = num / denom;
|
||||
let intercept = (sum_y - slope * sum_x) / n;
|
||||
|
||||
// Sum of squared residuals: Σ (y - ŷ)^2, ŷ = intercept + slope * x
|
||||
let mut sse = 0.0;
|
||||
for (x, y) in pairs {
|
||||
let y_hat = intercept + slope * x;
|
||||
let diff = y - y_hat;
|
||||
sse += diff * diff;
|
||||
}
|
||||
|
||||
let dof = n - 2.0;
|
||||
if dof <= 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"STEYX has non-positive degrees of freedom".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let sey = (sse / dof).sqrt();
|
||||
if !sey.is_finite() {
|
||||
return CalcResult::new_error(Error::DIV, cell, "Numerical error in STEYX".to_string());
|
||||
}
|
||||
|
||||
CalcResult::Number(sey)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::parser::ArrayNode;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
@@ -8,219 +6,77 @@ use crate::{
|
||||
};
|
||||
|
||||
impl Model {
|
||||
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) => {
|
||||
f(value);
|
||||
}
|
||||
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 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);
|
||||
}
|
||||
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(())
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
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;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
sum += value;
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
} else {
|
||||
sum += if b { 1.0 } else { 0.0 };
|
||||
count += 1.0;
|
||||
}
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
sum += value;
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::Range { .. } => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
"Unexpected Range".to_string(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::String(s) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
// Do nothing
|
||||
} else if let Ok(t) = s.parse::<f64>() {
|
||||
sum += t;
|
||||
count += 1.0;
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Argument cannot be cast into number".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Ignore everything else
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if count == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
@@ -230,7 +86,6 @@ 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);
|
||||
@@ -588,484 +443,4 @@ 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,7 +1,6 @@
|
||||
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,
|
||||
};
|
||||
@@ -297,122 +296,4 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
use statrs::distribution::{ContinuousCDF, Normal};
|
||||
|
||||
use crate::expressions::token::Error;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{calc_result::CalcResult, expressions::parser::Node, model::Model};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_gauss(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let z = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let dist = match Normal::new(0.0, 1.0) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Failed to construct standard normal distribution".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let result = dist.cdf(z) - 0.5;
|
||||
|
||||
if !result.is_finite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid result for GAUSS".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
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;
|
||||
@@ -17,7 +15,6 @@ mod normal;
|
||||
mod pearson;
|
||||
mod phi;
|
||||
mod poisson;
|
||||
mod rank_eq_avg;
|
||||
mod standard_dev;
|
||||
mod standardize;
|
||||
mod t_dist;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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,
|
||||
};
|
||||
@@ -6,9 +7,176 @@ use crate::{
|
||||
impl Model {
|
||||
// PEARSON(array1, array2)
|
||||
pub(crate) fn fn_pearson(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_, _, values_left, values_right) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(result) => result,
|
||||
Err(e) => return e,
|
||||
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(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Flatten into (x, y) pairs, skipping non-numeric entries (None)
|
||||
@@ -60,54 +228,8 @@ impl Model {
|
||||
}
|
||||
|
||||
let denom = (denom_x * denom_y).sqrt();
|
||||
|
||||
CalcResult::Number(num / denom)
|
||||
}
|
||||
|
||||
// RSQ(array1, array2) = CORREL(array1, array2)^2
|
||||
pub(crate) fn fn_rsq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
let (_rows, _cols, values1, values2) = match self.fn_get_two_matrices(args, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
let mut n = 0.0_f64;
|
||||
let mut sum_x = 0.0_f64;
|
||||
let mut sum_y = 0.0_f64;
|
||||
let mut sum_x2 = 0.0_f64;
|
||||
let mut sum_y2 = 0.0_f64;
|
||||
let mut sum_xy = 0.0_f64;
|
||||
|
||||
let len = values1.len().min(values2.len());
|
||||
for i in 0..len {
|
||||
if let (Some(x), Some(y)) = (values1[i], values2[i]) {
|
||||
n += 1.0;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_x2 += x * x;
|
||||
sum_y2 += y * y;
|
||||
sum_xy += x * y;
|
||||
}
|
||||
}
|
||||
|
||||
if n < 2.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::DIV,
|
||||
cell,
|
||||
"RSQ requires at least two numeric data points in each range".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let num = n * sum_xy - sum_x * sum_y;
|
||||
let denom_x = n * sum_x2 - sum_x * sum_x;
|
||||
let denom_y = n * sum_y2 - sum_y * sum_y;
|
||||
let denom = (denom_x * denom_y).sqrt();
|
||||
|
||||
if denom == 0.0 || !denom.is_finite() {
|
||||
return CalcResult::new_error(Error::DIV, cell, "Division by zero in RSQ".to_string());
|
||||
}
|
||||
|
||||
let r = num / denom;
|
||||
CalcResult::Number(r * r)
|
||||
|
||||
CalcResult::Number(r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::{
|
||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// Helper to collect numeric values from the 2nd argument of RANK.*
|
||||
fn collect_rank_values(
|
||||
&mut self,
|
||||
arg: &Node,
|
||||
cell: CellReferenceIndex,
|
||||
) -> Result<Vec<f64>, CalcResult> {
|
||||
let values = match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Array(array) => match self.values_from_array(array) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: format!("Unsupported array argument: {}", e),
|
||||
})
|
||||
}
|
||||
},
|
||||
CalcResult::Range { left, right } => self.values_from_range(left, right)?,
|
||||
CalcResult::Boolean(value) => {
|
||||
if !matches!(arg, Node::ReferenceKind { .. }) {
|
||||
vec![Some(if value { 1.0 } else { 0.0 })]
|
||||
} else {
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Unsupported argument type".to_string(),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let numeric_values: Vec<f64> = values.into_iter().flatten().collect();
|
||||
Ok(numeric_values)
|
||||
}
|
||||
|
||||
// RANK.EQ(number, ref, [order])
|
||||
pub(crate) fn fn_rank_eq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if !(2..=3).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
// number
|
||||
let number = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// ref
|
||||
let mut values = match self.collect_rank_values(&args[1], cell) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if values.is_empty() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "No numeric values for RANK.EQ".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// order: default 0 (descending)
|
||||
let order = if args.len() == 2 {
|
||||
0.0
|
||||
} else {
|
||||
match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
}
|
||||
};
|
||||
|
||||
values.retain(|v| !v.is_nan());
|
||||
|
||||
// "better" = greater (descending) or smaller (ascending)
|
||||
let mut better = 0;
|
||||
let mut equal = 0;
|
||||
|
||||
if order == 0.0 {
|
||||
// descending
|
||||
for v in &values {
|
||||
if *v > number {
|
||||
better += 1;
|
||||
} else if *v == number {
|
||||
equal += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ascending
|
||||
for v in &values {
|
||||
if *v < number {
|
||||
better += 1;
|
||||
} else if *v == number {
|
||||
equal += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if equal == 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Number not found in reference for RANK.EQ".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let rank = (better as f64) + 1.0;
|
||||
CalcResult::Number(rank)
|
||||
}
|
||||
|
||||
// RANK.AVG(number, ref, [order])
|
||||
pub(crate) fn fn_rank_avg(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||
if !(2..=3).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
// number
|
||||
let number = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
// ref
|
||||
let mut values = match self.collect_rank_values(&args[1], cell) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if values.is_empty() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "No numeric values for RANK.AVG".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// order: default 0 (descending)
|
||||
let order = if args.len() == 2 {
|
||||
0.0
|
||||
} else {
|
||||
match self.get_number_no_bools(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e,
|
||||
}
|
||||
};
|
||||
|
||||
values.retain(|v| !v.is_nan());
|
||||
|
||||
// > or < depending on order
|
||||
let mut better = 0;
|
||||
let mut equal = 0;
|
||||
|
||||
if order == 0.0 {
|
||||
// descending
|
||||
for v in &values {
|
||||
if *v > number {
|
||||
better += 1;
|
||||
} else if *v == number {
|
||||
equal += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ascending
|
||||
for v in &values {
|
||||
if *v < number {
|
||||
better += 1;
|
||||
} else if *v == number {
|
||||
equal += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if equal == 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Number not found in reference for RANK.AVG".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// For ties, average of the ranks. If the equal values occupy positions
|
||||
// (better+1) ..= (better+equal), the average is:
|
||||
// better + (equal + 1) / 2
|
||||
let better_f = better as f64;
|
||||
let equal_f = equal as f64;
|
||||
let rank = better_f + (equal_f + 1.0) / 2.0;
|
||||
|
||||
CalcResult::Number(rank)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ fn mean(xs: &[f64]) -> f64 {
|
||||
s / (n as f64)
|
||||
}
|
||||
|
||||
pub(crate) fn sample_var(xs: &[f64]) -> f64 {
|
||||
fn sample_var(xs: &[f64]) -> f64 {
|
||||
let n = xs.len();
|
||||
if n < 2 {
|
||||
return 0.0;
|
||||
|
||||
@@ -18,6 +18,7 @@ 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;
|
||||
@@ -45,7 +46,6 @@ 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,7 +66,6 @@ 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,9 +7,7 @@ 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;
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_f_test_sanity() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// Valid call
|
||||
model._set("A1", "=F.TEST(A2:A7, B2:B7)");
|
||||
model._set("A2", "9");
|
||||
model._set("A3", "12");
|
||||
model._set("A4", "14");
|
||||
model._set("A5", "16");
|
||||
model._set("A6", "18");
|
||||
model._set("A7", "20");
|
||||
model._set("B2", "11");
|
||||
model._set("B3", "10");
|
||||
model._set("B4", "15");
|
||||
model._set("B5", "17");
|
||||
model._set("B6", "19");
|
||||
model._set("B7", "21");
|
||||
|
||||
// Too few args
|
||||
model._set("A8", "=F.TEST(A2:A7)");
|
||||
|
||||
// Too many args
|
||||
model._set("A9", "=F.TEST(A2:A7, B2:B7, C2:C7)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0.859284302");
|
||||
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A9"), *"#ERROR!");
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_fn_gauss_smoke() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=GAUSS(-3)");
|
||||
model._set("A2", "=GAUSS(-2.3)");
|
||||
model._set("A3", "=GAUSS(-1.7)");
|
||||
model._set("A4", "=GAUSS(0)");
|
||||
model._set("A5", "=GAUSS(0.5)");
|
||||
model._set("A6", "=GAUSS(1)");
|
||||
model._set("A7", "=GAUSS(1.3)");
|
||||
model._set("A8", "=GAUSS(3)");
|
||||
model._set("A9", "=GAUSS(4)");
|
||||
|
||||
model._set("G6", "=GAUSS()");
|
||||
model._set("G7", "=GAUSS(1, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"-0.498650102");
|
||||
assert_eq!(model._get_text("A2"), *"-0.48927589");
|
||||
assert_eq!(model._get_text("A3"), *"-0.455434537");
|
||||
assert_eq!(model._get_text("A4"), *"0");
|
||||
assert_eq!(model._get_text("A5"), *"0.191462461");
|
||||
assert_eq!(model._get_text("A6"), *"0.341344746");
|
||||
assert_eq!(model._get_text("A7"), *"0.403199515");
|
||||
assert_eq!(model._get_text("A8"), *"0.498650102");
|
||||
assert_eq!(model._get_text("A9"), *"0.499968329");
|
||||
|
||||
assert_eq!(model._get_text("G6"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("G7"), *"#ERROR!");
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=CELL("address",A1)");
|
||||
model._set("A2", "=CELL()");
|
||||
|
||||
model._set("A3", "=INFO("system")");
|
||||
model._set("A4", "=INFO()");
|
||||
|
||||
model._set("A5", "=N(TRUE)");
|
||||
model._set("A6", "=N()");
|
||||
model._set("A7", "=N(1, 2)");
|
||||
|
||||
model._set("A8", "=SHEETS()");
|
||||
model._set("A9", "=SHEETS(1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"$A$1");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#N/IMPL!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A5"), *"1");
|
||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("A8"), *"1");
|
||||
assert_eq!(model._get_text("A9"), *"#N/IMPL!");
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_floor_floating_point_precision() {
|
||||
// This test specifically checks the floating-point precision bug fix
|
||||
// Bug: FLOOR(7.1, 0.1) was returning 7.0 instead of 7.1
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// FLOOR tests
|
||||
model._set("C5", "=FLOOR(7.1, 0.1)");
|
||||
model._set("H7", "=FLOOR(-7.1, -0.1)");
|
||||
|
||||
// FLOOR.PRECISE tests
|
||||
model._set("C53", "=FLOOR.PRECISE(7.1, 0.1)");
|
||||
model._set("H53", "=FLOOR.PRECISE(7.1, -0.1)");
|
||||
|
||||
// FLOOR.MATH tests
|
||||
model._set("C101", "=FLOOR.MATH(7.1, 0.1)");
|
||||
model._set("H101", "=FLOOR.MATH(7.1, -0.1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// All should return 7.1
|
||||
assert_eq!(model._get_text("C5"), *"7.1");
|
||||
assert_eq!(model._get_text("H7"), *"-7.1");
|
||||
assert_eq!(model._get_text("C53"), *"7.1");
|
||||
assert_eq!(model._get_text("H53"), *"7.1");
|
||||
assert_eq!(model._get_text("C101"), *"7.1");
|
||||
assert_eq!(model._get_text("H101"), *"7.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_additional_precision_cases() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=FLOOR(7.9, 0.1)");
|
||||
model._set("A2", "=FLOOR(2.6, 0.5)");
|
||||
model._set("A3", "=FLOOR(0.3, 0.1)"); // 0.1 + 0.2 type scenario
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"7.9");
|
||||
assert_eq!(model._get_text("A2"), *"2.5");
|
||||
assert_eq!(model._get_text("A3"), *"0.3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_basic_cases() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=FLOOR(3.7, 2)");
|
||||
model._set("A2", "=FLOOR(3.2, 1)");
|
||||
model._set("A3", "=FLOOR(10, 3)");
|
||||
model._set("A4", "=FLOOR(7, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"2");
|
||||
assert_eq!(model._get_text("A2"), *"3");
|
||||
assert_eq!(model._get_text("A3"), *"9");
|
||||
assert_eq!(model._get_text("A4"), *"6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_negative_numbers() {
|
||||
let mut model = new_empty_model();
|
||||
// Both negative: rounds toward zero
|
||||
model._set("A1", "=FLOOR(-2.5, -2)");
|
||||
model._set("A2", "=FLOOR(-11, -3)");
|
||||
|
||||
// Negative number, positive significance: rounds away from zero
|
||||
model._set("A3", "=FLOOR(-11, 3)");
|
||||
model._set("A4", "=FLOOR(-2.5, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"-2");
|
||||
assert_eq!(model._get_text("A2"), *"-9");
|
||||
assert_eq!(model._get_text("A3"), *"-12");
|
||||
assert_eq!(model._get_text("A4"), *"-4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_error_cases() {
|
||||
let mut model = new_empty_model();
|
||||
// Positive number with negative significance should error
|
||||
model._set("A1", "=FLOOR(2.5, -2)");
|
||||
model._set("A2", "=FLOOR(10, -3)");
|
||||
|
||||
// Division by zero
|
||||
model._set("A3", "=FLOOR(5, 0)");
|
||||
|
||||
// Wrong number of arguments
|
||||
model._set("A4", "=FLOOR(5)");
|
||||
model._set("A5", "=FLOOR(5, 1, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A3"), *"#DIV/0!");
|
||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_edge_cases() {
|
||||
let mut model = new_empty_model();
|
||||
// Zero value
|
||||
model._set("A1", "=FLOOR(0, 5)");
|
||||
model._set("A2", "=FLOOR(0, 0)");
|
||||
|
||||
// Exact multiples
|
||||
model._set("A3", "=FLOOR(10, 5)");
|
||||
model._set("A4", "=FLOOR(9, 3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"0");
|
||||
assert_eq!(model._get_text("A2"), *"0");
|
||||
assert_eq!(model._get_text("A3"), *"10");
|
||||
assert_eq!(model._get_text("A4"), *"9");
|
||||
}
|
||||
29
base/src/test/test_fn_fact.rs
Normal file
29
base/src/test/test_fn_fact.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
#![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,24 +7,15 @@ const TIMESTAMP_2023: i64 = 1679319865208;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
mock_time::set_mock_time(TIMESTAMP_2023);
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("A1", "=NOW(1, 1)");
|
||||
model._set("A2", "=NOW(\"Europe/Berlin\")");
|
||||
model._set("A3", "=NOW(\"faketimezone\")");
|
||||
model._set("A1", "=NOW(1)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"#ERROR!",
|
||||
"Wrong number of arguments"
|
||||
);
|
||||
assert_eq!(model._get_text("A2"), *"20/03/2023 14:44:25");
|
||||
assert_eq!(
|
||||
model._get_text("A3"),
|
||||
"#VALUE!",
|
||||
"Invalid timezone: faketimezone"
|
||||
"NOW should not accept arguments"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=SUMSQ()");
|
||||
model._set("A2", "=SUMSQ(2)");
|
||||
model._set("A3", "=SUMSQ(1, 2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), *"4");
|
||||
assert_eq!(model._get_text("A3"), *"5");
|
||||
}
|
||||
@@ -11,7 +11,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
||||
|
||||
| Function | Status | Documentation |
|
||||
| ---------- | ---------------------------------------------- | ------------- |
|
||||
| CELL | <Badge type="tip" text="Available" /> | – |
|
||||
| CELL | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| 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="tip" text="Available" /> | – |
|
||||
| ISNA | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| 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="tip" text="Available" /> | – |
|
||||
| SHEETS | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| TYPE | <Badge type="tip" text="Available" /> | – |
|
||||
|
||||
@@ -7,5 +7,6 @@ lang: en-US
|
||||
# CELL
|
||||
|
||||
::: warning
|
||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||
🚧 This function is not yet available in IronCalc.
|
||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||
:::
|
||||
@@ -7,5 +7,6 @@ lang: en-US
|
||||
# N
|
||||
|
||||
::: warning
|
||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||
🚧 This function is not yet available in IronCalc.
|
||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||
:::
|
||||
@@ -7,5 +7,6 @@ lang: en-US
|
||||
# SHEETS
|
||||
|
||||
::: warning
|
||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||
🚧 This function is not yet available in IronCalc.
|
||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||
:::
|
||||
@@ -6,33 +6,26 @@ lang: en-US
|
||||
|
||||
# COLUMN function
|
||||
## 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 cell, column, range, or [Named Range](/web-application/name-manager.html) for which you wish to find the column number.
|
||||
* *reference* ([cell](/features/value-types#references), [optional](/features/optional-arguments.md)). The number of the cell you wish to reference the column number of.
|
||||
### Additional guidance
|
||||
* When referencing a range of cells, only the column number of the leftmost cell will be returned.
|
||||
* When referencing a range of cells, only the column number of the left most cell will be returned.
|
||||
* You are also able to reference complete columns instead of individual cells.
|
||||
* 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. If no reference is included, the column number of the cell where the formula is entered will be returned.
|
||||
COLUMN returns the [number](/features/value-types#numbers) of the specific cell or column which is being referenced.
|
||||
### Error conditions
|
||||
* 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.
|
||||
* IronCalc currently does not support the referencing of cells with names.
|
||||
## Details
|
||||
The COLUMN Function can only be used to display the correlating number of a single column within a Sheet. If you wish to show the number of columns used within a specific range, you can use the [COLUMNS](/functions/lookup_and_reference/columns) Function.
|
||||
The COLUMN Function can only be used to display the correlating number of a single column within a Sheet. If you wish to show the number of columns used within a specific range, you can use the COLUMNS Function.
|
||||
## Examples
|
||||
### No Cell Reference
|
||||
When no cell reference is made, the formula uses **=COLUMN()**. This will output the column number of the cell where the formula is entered.<br><br>For example, if the formula is placed in cell A1, then "1" will be displayed.
|
||||
When no cell reference is made, the formula uses **=COLUMN()**. This will then output the column number of the cell where the formula is placed.<br><br>For example, if the formula is placed in cell A1, then "1" will be displayed.
|
||||
### With Cell Reference
|
||||
When a cell reference is made, the formula uses **=COLUMN(<span title="Reference" style="color:#1E88E5">Referenced Cell</span>)**. This will then output the column number of the referenced cell, regardless of where the formula is placed in the sheet.<br><br>For example, if B1 is the referenced cell, then "2" will be the output of the formula, regardless of where the formula is placed in the sheet.<br><br>**Note:** references do not have to be specific cells, you can also reference complete columns. For example, **=COLUMN(B:B)** would also result in an output of "2".
|
||||
When a cell reference is made, the formula uses **=COLUMN([Referenced Cell])**. This will then output the column number of the referenced cell, regardless of where the formula is placed in the sheet.<br><br>For example, if the cell B1 is the referenced cell, "2" will be the output of the formula no matter where it is placed in the sheet.<br><br>**Note:** references do not always have to be specific cells, you can also reference complete columns. For example, **=COLUMN(B:B)** would also result in an output of "2".
|
||||
### Range References
|
||||
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.
|
||||
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
|
||||
@@ -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="tip" text="Available" /> | – |
|
||||
| SUMSQ | <Badge type="info" text="Not implemented yet" /> | – |
|
||||
| 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,5 +7,6 @@ lang: en-US
|
||||
# SUMSQ
|
||||
|
||||
::: warning
|
||||
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
|
||||
🚧 This function is not yet available in IronCalc.
|
||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
||||
:::
|
||||
@@ -135,76 +135,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
};
|
||||
|
||||
const onIncreaseFontSize = (delta: number) => {
|
||||
/** Automatically adjusts row heights when font size changes. */
|
||||
const DEFAULT_ROW_HEIGHT = 28;
|
||||
|
||||
const {
|
||||
sheet,
|
||||
range: [rowStart, columnStart, rowEnd, columnEnd],
|
||||
} = model.getSelectedView();
|
||||
|
||||
/** Normalize the range (selection can be from bottom-right to top-left) */
|
||||
const actualRowStart = Math.min(rowStart, rowEnd);
|
||||
const actualRowEnd = Math.max(rowStart, rowEnd);
|
||||
const actualColumnStart = Math.min(columnStart, columnEnd);
|
||||
const actualColumnEnd = Math.max(columnStart, columnEnd);
|
||||
|
||||
/** Capture current state BEFORE updating font size */
|
||||
const rowData: Array<{
|
||||
row: number;
|
||||
oldFontSize: number;
|
||||
oldRowHeight: number;
|
||||
maxLineCount: number;
|
||||
}> = [];
|
||||
|
||||
for (let row = actualRowStart; row <= actualRowEnd; row++) {
|
||||
let maxFontSize = 0;
|
||||
let maxLineCount = 1;
|
||||
for (let col = actualColumnStart; col <= actualColumnEnd; col++) {
|
||||
const style = model.getCellStyle(sheet, row, col);
|
||||
maxFontSize = Math.max(maxFontSize, style.font.sz);
|
||||
|
||||
// Count lines in cell content (we add new lines with Alt+Enter / Option+Enter)
|
||||
const cellContent = model.getCellContent(sheet, row, col);
|
||||
const lineCount = cellContent.split("\n").length;
|
||||
maxLineCount = Math.max(maxLineCount, lineCount);
|
||||
}
|
||||
const oldRowHeight = model.getRowHeight(sheet, row);
|
||||
rowData.push({
|
||||
row,
|
||||
oldFontSize: maxFontSize,
|
||||
oldRowHeight,
|
||||
maxLineCount,
|
||||
});
|
||||
}
|
||||
|
||||
/** Update the font size in the model */
|
||||
updateRangeStyle("font.size_delta", `${delta}`);
|
||||
|
||||
/** Adjust row heights based on the new font sizes and line counts */
|
||||
for (const { row, oldFontSize, oldRowHeight, maxLineCount } of rowData) {
|
||||
const newFontSize = oldFontSize + delta;
|
||||
|
||||
if (oldRowHeight < DEFAULT_ROW_HEIGHT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lineHeight = newFontSize * 1.5;
|
||||
const requiredHeight = (maxLineCount - 1) * lineHeight + 8 + newFontSize;
|
||||
|
||||
let newRowHeight: number;
|
||||
if (requiredHeight > DEFAULT_ROW_HEIGHT) {
|
||||
newRowHeight = requiredHeight;
|
||||
} else {
|
||||
newRowHeight = DEFAULT_ROW_HEIGHT;
|
||||
}
|
||||
|
||||
if (Math.abs(newRowHeight - oldRowHeight) > 0.1) {
|
||||
model.setRowsHeight(sheet, row, row, newRowHeight);
|
||||
}
|
||||
}
|
||||
|
||||
setRedrawId((id) => id + 1);
|
||||
};
|
||||
|
||||
const onCopyStyles = () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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";
|
||||
@@ -722,7 +721,7 @@ export default class WorksheetCanvas {
|
||||
const style = this.model.getCellStyle(selectedSheet, row, column);
|
||||
|
||||
// first the background
|
||||
let backgroundColor = theme.palette.common.white;
|
||||
let backgroundColor = "#FFFFFF";
|
||||
if (style.fill.fg_color) {
|
||||
backgroundColor = style.fill.fg_color;
|
||||
}
|
||||
@@ -1037,21 +1036,14 @@ 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
|
||||
? isFullColumnSelected
|
||||
? theme.palette.primary.main
|
||||
: headerSelectedBackground
|
||||
? headerSelectedBackground
|
||||
: headerBackground;
|
||||
div.style.color = selected
|
||||
? isFullColumnSelected
|
||||
? theme.palette.common.white
|
||||
: headerSelectedColor
|
||||
: headerTextColor;
|
||||
div.style.color = selected ? headerSelectedColor : headerTextColor;
|
||||
div.style.fontWeight = "bold";
|
||||
div.style.borderLeft = `1px solid ${headerBorderColor}`;
|
||||
div.style.borderTop = `1px solid ${headerBorderColor}`;
|
||||
@@ -1079,15 +1071,9 @@ 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;
|
||||
@@ -1099,9 +1085,7 @@ export default class WorksheetCanvas {
|
||||
context.fillStyle = headerBorderColor;
|
||||
context.fillRect(0.5, topLeftCornerY, headerColumnWidth, rowHeight);
|
||||
context.fillStyle = selected
|
||||
? isFullRowSelected
|
||||
? theme.palette.primary.main
|
||||
: headerSelectedBackground
|
||||
? headerSelectedBackground
|
||||
: headerBackground;
|
||||
context.fillRect(
|
||||
0.5,
|
||||
@@ -1113,11 +1097,7 @@ export default class WorksheetCanvas {
|
||||
context.fillStyle = outlineColor;
|
||||
context.fillRect(headerColumnWidth - 1, topLeftCornerY, 1, rowHeight);
|
||||
}
|
||||
context.fillStyle = selected
|
||||
? isFullRowSelected
|
||||
? theme.palette.common.white
|
||||
: headerSelectedColor
|
||||
: headerTextColor;
|
||||
context.fillStyle = selected ? headerSelectedColor : headerTextColor;
|
||||
context.font = `bold 12px ${defaultCellFontFamily}`;
|
||||
context.fillText(
|
||||
`${row}`,
|
||||
@@ -1142,17 +1122,11 @@ 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(
|
||||
@@ -1172,12 +1146,7 @@ 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,
|
||||
isFullColumnSelected,
|
||||
);
|
||||
deltaX += this.addColumnHeader(deltaX, column, selected);
|
||||
}
|
||||
|
||||
if (frozenColumns !== 0) {
|
||||
@@ -1193,12 +1162,7 @@ export default class WorksheetCanvas {
|
||||
|
||||
for (let column = firstColumn; column <= lastColumn; column += 1) {
|
||||
const selected = column >= columnStart && column <= columnEnd;
|
||||
deltaX += this.addColumnHeader(
|
||||
deltaX,
|
||||
column,
|
||||
selected,
|
||||
isFullColumnSelected,
|
||||
);
|
||||
deltaX += this.addColumnHeader(deltaX, column, selected);
|
||||
}
|
||||
|
||||
columnHeaders.style.width = `${deltaX}px`;
|
||||
@@ -1208,7 +1172,6 @@ export default class WorksheetCanvas {
|
||||
deltaX: number,
|
||||
column: number,
|
||||
selected: boolean,
|
||||
isFullColumnSelected: boolean,
|
||||
): number {
|
||||
const columnWidth = this.getColumnWidth(
|
||||
this.model.getSelectedSheet(),
|
||||
@@ -1219,7 +1182,7 @@ export default class WorksheetCanvas {
|
||||
div.textContent = columnNameFromNumber(column);
|
||||
this.columnHeaders.insertBefore(div, null);
|
||||
|
||||
this.styleColumnHeader(columnWidth, div, selected, isFullColumnSelected);
|
||||
this.styleColumnHeader(columnWidth, div, selected);
|
||||
this.addColumnResizeHandle(deltaX + columnWidth, column, columnWidth);
|
||||
return columnWidth;
|
||||
}
|
||||
|
||||
25
webapp/app.ironcalc.com/frontend/package-lock.json
generated
25
webapp/app.ironcalc.com/frontend/package-lock.json
generated
@@ -91,7 +91,6 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -571,7 +570,6 @@
|
||||
"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",
|
||||
@@ -615,7 +613,6 @@
|
||||
"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",
|
||||
@@ -2029,7 +2026,6 @@
|
||||
"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"
|
||||
}
|
||||
@@ -2126,7 +2122,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.25",
|
||||
"caniuse-lite": "^1.0.30001754",
|
||||
@@ -2696,7 +2691,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -2764,7 +2758,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -2774,7 +2767,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -2986,7 +2978,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -3032,7 +3023,6 @@
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -3123,6 +3113,21 @@
|
||||
"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,19 +227,16 @@ const Wrapper = styled("div")`
|
||||
`;
|
||||
|
||||
const DRAWER_WIDTH = 264;
|
||||
export const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 768;
|
||||
const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 440;
|
||||
|
||||
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
|
||||
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;`}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -250,7 +247,7 @@ const MobileOverlay = styled("div")`
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
z-index: 100;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
|
||||
@media (min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE + 1}px) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { Model } from "@ironcalc/workbook";
|
||||
import { IconButton, Tooltip } from "@mui/material";
|
||||
import { 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";
|
||||
@@ -78,7 +77,7 @@ export function FileBar(properties: {
|
||||
{properties.isDrawerOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
|
||||
</DrawerButton>
|
||||
</Tooltip>
|
||||
{width > MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE && (
|
||||
{width > 440 && (
|
||||
<FileMenu
|
||||
newModel={properties.newModel}
|
||||
newModelFromTemplate={properties.newModelFromTemplate}
|
||||
@@ -93,7 +92,7 @@ export function FileBar(properties: {
|
||||
onDelete={properties.onDelete}
|
||||
/>
|
||||
)}
|
||||
{width > MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE && <HelpMenu />}
|
||||
{width > 440 && <HelpMenu />}
|
||||
<WorkbookTitleWrapper>
|
||||
<WorkbookTitle
|
||||
name={properties.model.getName()}
|
||||
@@ -113,8 +112,7 @@ export function FileBar(properties: {
|
||||
<div style={{ fontWeight: "bold" }}>{cloudWarningText2}</div>
|
||||
</div>
|
||||
}
|
||||
placement="bottom"
|
||||
enterTouchDelay={0}
|
||||
placement="bottom-start"
|
||||
enterDelay={500}
|
||||
slotProps={{
|
||||
popper: {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/calc_tests/N_CELL_INFO_SHEETS.xlsx
Normal file
BIN
xlsx/tests/calc_tests/N_CELL_INFO_SHEETS.xlsx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user