Compare commits
50 Commits
fact_gamma
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
203d640dbd | ||
|
|
ba75ffcf4f | ||
|
|
b5c977d3aa | ||
|
|
4029441cea | ||
|
|
cd47c609a0 | ||
|
|
ae6acdcdd5 | ||
|
|
c196db2115 | ||
|
|
a3c201e4e4 | ||
|
|
126e62957a | ||
|
|
294a651ae5 | ||
|
|
6f8a1e0da6 | ||
|
|
205ba6ee2d | ||
|
|
547b331773 | ||
|
|
f96612cf23 | ||
|
|
745435b950 | ||
|
|
4ca996cd3f | ||
|
|
3fbb91c414 | ||
|
|
93c9c42607 | ||
|
|
11edc2378e | ||
|
|
962e70c834 | ||
|
|
f803dad0a3 | ||
|
|
19580fc1ad | ||
|
|
e760b2d08e | ||
|
|
0e6ded7154 | ||
|
|
db26403432 | ||
|
|
9193479cce | ||
|
|
f814a75ae5 | ||
|
|
c8da5efb5f | ||
|
|
522e734395 | ||
|
|
2a7d59e512 | ||
|
|
c4142d4bf8 | ||
|
|
885d344b5b | ||
|
|
bed6f007cd | ||
|
|
dbd1b2df60 | ||
|
|
db552047c8 | ||
|
|
bcbacdb0a3 | ||
|
|
d0f37854d9 | ||
|
|
99b03f70c3 | ||
|
|
3e1605a494 | ||
|
|
d6aad08e73 | ||
|
|
8597d14a4e | ||
|
|
01b19b9c35 | ||
|
|
4649a0c78c | ||
|
|
cd0baf5ba7 | ||
|
|
167d169f1a | ||
|
|
080574b112 | ||
|
|
6056b8f122 | ||
|
|
25f7891343 | ||
|
|
bdd0af0a39 | ||
|
|
261924396d |
@@ -12,6 +12,9 @@ pub(crate) const DEFAULT_WINDOW_WIDTH: i64 = 800;
|
|||||||
pub(crate) const LAST_COLUMN: i32 = 16_384;
|
pub(crate) const LAST_COLUMN: i32 = 16_384;
|
||||||
pub(crate) const LAST_ROW: i32 = 1_048_576;
|
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:
|
// 693_594 is computed as:
|
||||||
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
||||||
// The 2 days offset is because of Excel 1900 bug
|
// The 2 days offset is because of Excel 1900 bug
|
||||||
|
|||||||
@@ -471,6 +471,20 @@ impl Parser {
|
|||||||
Node::NumberKind(s) => ArrayNode::Number(s),
|
Node::NumberKind(s) => ArrayNode::Number(s),
|
||||||
Node::StringKind(s) => ArrayNode::String(s),
|
Node::StringKind(s) => ArrayNode::String(s),
|
||||||
Node::ErrorKind(kind) => ArrayNode::Error(kind),
|
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),
|
error @ Node::ParseErrorKind { .. } => return Err(error),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Node::ParseErrorKind {
|
return Err(Node::ParseErrorKind {
|
||||||
@@ -490,6 +504,20 @@ impl Parser {
|
|||||||
Node::NumberKind(s) => ArrayNode::Number(s),
|
Node::NumberKind(s) => ArrayNode::Number(s),
|
||||||
Node::StringKind(s) => ArrayNode::String(s),
|
Node::StringKind(s) => ArrayNode::String(s),
|
||||||
Node::ErrorKind(kind) => ArrayNode::Error(kind),
|
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),
|
error @ Node::ParseErrorKind { .. } => return Err(error),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Node::ParseErrorKind {
|
return Err(Node::ParseErrorKind {
|
||||||
|
|||||||
@@ -872,12 +872,10 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
Function::Combin => args_signature_scalars(arg_count, 2, 0),
|
Function::Combin => args_signature_scalars(arg_count, 2, 0),
|
||||||
Function::Combina => args_signature_scalars(arg_count, 2, 0),
|
Function::Combina => args_signature_scalars(arg_count, 2, 0),
|
||||||
Function::Sumsq => vec![Signature::Vector; arg_count],
|
Function::Sumsq => vec![Signature::Vector; arg_count],
|
||||||
|
|
||||||
Function::N => args_signature_scalars(arg_count, 1, 0),
|
Function::N => args_signature_scalars(arg_count, 1, 0),
|
||||||
Function::Sheets => args_signature_scalars(arg_count, 0, 1),
|
Function::Sheets => args_signature_scalars(arg_count, 0, 1),
|
||||||
Function::Cell => args_signature_scalars(arg_count, 1, 1),
|
Function::Cell => args_signature_scalars(arg_count, 1, 1),
|
||||||
Function::Info => 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::Daverage => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
Function::Dcount => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
Function::Dcount => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
Function::Dget => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
Function::Dget => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
@@ -890,7 +888,6 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
Function::Dvar => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
Function::Dvar => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
Function::Dvarp => 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::Dstdevp => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
|
|
||||||
Function::BetaDist => args_signature_scalars(arg_count, 4, 2),
|
Function::BetaDist => args_signature_scalars(arg_count, 4, 2),
|
||||||
Function::BetaInv => args_signature_scalars(arg_count, 3, 2),
|
Function::BetaInv => args_signature_scalars(arg_count, 3, 2),
|
||||||
Function::BinomDist => args_signature_scalars(arg_count, 4, 0),
|
Function::BinomDist => args_signature_scalars(arg_count, 4, 0),
|
||||||
@@ -929,6 +926,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
Function::FDistRT => args_signature_scalars(arg_count, 3, 0),
|
Function::FDistRT => args_signature_scalars(arg_count, 3, 0),
|
||||||
Function::FInv => 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::FInvRT => args_signature_scalars(arg_count, 3, 0),
|
||||||
|
Function::FTest => vec![Signature::Vector; 2],
|
||||||
Function::Fisher => args_signature_scalars(arg_count, 1, 0),
|
Function::Fisher => args_signature_scalars(arg_count, 1, 0),
|
||||||
Function::FisherInv => args_signature_scalars(arg_count, 1, 0),
|
Function::FisherInv => args_signature_scalars(arg_count, 1, 0),
|
||||||
Function::Gamma => args_signature_scalars(arg_count, 1, 0),
|
Function::Gamma => args_signature_scalars(arg_count, 1, 0),
|
||||||
@@ -989,6 +987,26 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
vec![Signature::Error; arg_count]
|
vec![Signature::Error; arg_count]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Function::Sumx2my2 => vec![Signature::Vector; 2],
|
||||||
|
Function::Sumx2py2 => vec![Signature::Vector; 2],
|
||||||
|
Function::Sumxmy2 => vec![Signature::Vector; 2],
|
||||||
|
Function::Correl => vec![Signature::Vector; 2],
|
||||||
|
Function::Rsq => vec![Signature::Vector; 2],
|
||||||
|
Function::Intercept => vec![Signature::Vector; 2],
|
||||||
|
Function::Slope => vec![Signature::Vector; 2],
|
||||||
|
Function::Steyx => vec![Signature::Vector; 2],
|
||||||
|
Function::Gauss => args_signature_scalars(arg_count, 1, 0),
|
||||||
|
Function::Harmean => vec![Signature::Vector; arg_count],
|
||||||
|
Function::Kurt => vec![Signature::Vector; arg_count],
|
||||||
|
Function::Large => vec![Signature::Vector, Signature::Scalar],
|
||||||
|
Function::MaxA => vec![Signature::Vector; arg_count],
|
||||||
|
Function::Median => vec![Signature::Vector; arg_count],
|
||||||
|
Function::MinA => vec![Signature::Vector; arg_count],
|
||||||
|
Function::RankAvg => vec![Signature::Scalar, Signature::Vector, Signature::Scalar],
|
||||||
|
Function::RankEq => vec![Signature::Scalar, Signature::Vector, Signature::Scalar],
|
||||||
|
Function::Skew => vec![Signature::Vector; arg_count],
|
||||||
|
Function::SkewP => vec![Signature::Vector; arg_count],
|
||||||
|
Function::Small => vec![Signature::Vector, Signature::Scalar],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1014,7 +1032,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Atan => scalar_arguments(args),
|
Function::Atan => scalar_arguments(args),
|
||||||
Function::Atan2 => scalar_arguments(args),
|
Function::Atan2 => scalar_arguments(args),
|
||||||
Function::Atanh => scalar_arguments(args),
|
Function::Atanh => scalar_arguments(args),
|
||||||
Function::Choose => scalar_arguments(args), // static_analysis_choose(args, cell),
|
Function::Choose => scalar_arguments(args),
|
||||||
Function::Column => not_implemented(args),
|
Function::Column => not_implemented(args),
|
||||||
Function::Columns => not_implemented(args),
|
Function::Columns => not_implemented(args),
|
||||||
Function::Cos => scalar_arguments(args),
|
Function::Cos => scalar_arguments(args),
|
||||||
@@ -1061,7 +1079,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Lookup => not_implemented(args),
|
Function::Lookup => not_implemented(args),
|
||||||
Function::Match => not_implemented(args),
|
Function::Match => not_implemented(args),
|
||||||
Function::Offset => static_analysis_offset(args),
|
Function::Offset => static_analysis_offset(args),
|
||||||
// FIXME: Row could return an array
|
|
||||||
Function::Row => StaticResult::Scalar,
|
Function::Row => StaticResult::Scalar,
|
||||||
Function::Rows => not_implemented(args),
|
Function::Rows => not_implemented(args),
|
||||||
Function::Vlookup => not_implemented(args),
|
Function::Vlookup => not_implemented(args),
|
||||||
@@ -1253,7 +1270,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Sheets => scalar_arguments(args),
|
Function::Sheets => scalar_arguments(args),
|
||||||
Function::Cell => scalar_arguments(args),
|
Function::Cell => scalar_arguments(args),
|
||||||
Function::Info => scalar_arguments(args),
|
Function::Info => scalar_arguments(args),
|
||||||
|
|
||||||
Function::Dget => not_implemented(args),
|
Function::Dget => not_implemented(args),
|
||||||
Function::Dmax => not_implemented(args),
|
Function::Dmax => not_implemented(args),
|
||||||
Function::Dmin => not_implemented(args),
|
Function::Dmin => not_implemented(args),
|
||||||
@@ -1266,7 +1282,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::Dvar => not_implemented(args),
|
Function::Dvar => not_implemented(args),
|
||||||
Function::Dvarp => not_implemented(args),
|
Function::Dvarp => not_implemented(args),
|
||||||
Function::Dstdevp => not_implemented(args),
|
Function::Dstdevp => not_implemented(args),
|
||||||
|
|
||||||
Function::BetaDist => StaticResult::Scalar,
|
Function::BetaDist => StaticResult::Scalar,
|
||||||
Function::BetaInv => StaticResult::Scalar,
|
Function::BetaInv => StaticResult::Scalar,
|
||||||
Function::BinomDist => StaticResult::Scalar,
|
Function::BinomDist => StaticResult::Scalar,
|
||||||
@@ -1287,6 +1302,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::FDistRT => StaticResult::Scalar,
|
Function::FDistRT => StaticResult::Scalar,
|
||||||
Function::FInv => StaticResult::Scalar,
|
Function::FInv => StaticResult::Scalar,
|
||||||
Function::FInvRT => StaticResult::Scalar,
|
Function::FInvRT => StaticResult::Scalar,
|
||||||
|
Function::FTest => StaticResult::Scalar,
|
||||||
Function::Fisher => StaticResult::Scalar,
|
Function::Fisher => StaticResult::Scalar,
|
||||||
Function::FisherInv => StaticResult::Scalar,
|
Function::FisherInv => StaticResult::Scalar,
|
||||||
Function::Gamma => StaticResult::Scalar,
|
Function::Gamma => StaticResult::Scalar,
|
||||||
@@ -1322,5 +1338,25 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
|||||||
Function::VarA => StaticResult::Scalar,
|
Function::VarA => StaticResult::Scalar,
|
||||||
Function::WeibullDist => StaticResult::Scalar,
|
Function::WeibullDist => StaticResult::Scalar,
|
||||||
Function::ZTest => StaticResult::Scalar,
|
Function::ZTest => StaticResult::Scalar,
|
||||||
|
Function::Sumx2my2 => StaticResult::Scalar,
|
||||||
|
Function::Sumx2py2 => StaticResult::Scalar,
|
||||||
|
Function::Sumxmy2 => StaticResult::Scalar,
|
||||||
|
Function::Correl => StaticResult::Scalar,
|
||||||
|
Function::Rsq => StaticResult::Scalar,
|
||||||
|
Function::Intercept => StaticResult::Scalar,
|
||||||
|
Function::Slope => StaticResult::Scalar,
|
||||||
|
Function::Steyx => StaticResult::Scalar,
|
||||||
|
Function::Gauss => StaticResult::Scalar,
|
||||||
|
Function::Harmean => StaticResult::Scalar,
|
||||||
|
Function::Kurt => StaticResult::Scalar,
|
||||||
|
Function::Large => StaticResult::Scalar,
|
||||||
|
Function::MaxA => StaticResult::Scalar,
|
||||||
|
Function::Median => StaticResult::Scalar,
|
||||||
|
Function::MinA => StaticResult::Scalar,
|
||||||
|
Function::RankAvg => StaticResult::Scalar,
|
||||||
|
Function::RankEq => StaticResult::Scalar,
|
||||||
|
Function::Skew => StaticResult::Scalar,
|
||||||
|
Function::SkewP => StaticResult::Scalar,
|
||||||
|
Function::Small => StaticResult::Scalar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use chrono::Months;
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use chrono::NaiveTime;
|
use chrono::NaiveTime;
|
||||||
use chrono::Timelike;
|
use chrono::Timelike;
|
||||||
|
use chrono_tz::Tz;
|
||||||
|
|
||||||
const SECONDS_PER_DAY: i32 = 86_400;
|
const SECONDS_PER_DAY: i32 = 86_400;
|
||||||
const SECONDS_PER_DAY_F64: f64 = SECONDS_PER_DAY as f64;
|
const SECONDS_PER_DAY_F64: f64 = SECONDS_PER_DAY as f64;
|
||||||
@@ -770,12 +771,12 @@ impl Model {
|
|||||||
Ok(values)
|
Ok(values)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the current date/time as an Excel serial number in the model's configured timezone.
|
// Returns the current date/time as an Excel serial number in the given timezone.
|
||||||
// Used by TODAY() and NOW().
|
// Used by TODAY() and NOW().
|
||||||
fn current_excel_serial(&self) -> Option<f64> {
|
pub(crate) fn current_excel_serial_with_timezone(&self, tz: Tz) -> Option<f64> {
|
||||||
let seconds = get_milliseconds_since_epoch() / 1000;
|
let seconds = get_milliseconds_since_epoch() / 1000;
|
||||||
DateTime::from_timestamp(seconds, 0).map(|dt| {
|
DateTime::from_timestamp(seconds, 0).map(|dt| {
|
||||||
let local_time = dt.with_timezone(&self.tz);
|
let local_time = dt.with_timezone(&tz);
|
||||||
let days_from_1900 = local_time.num_days_from_ce() - EXCEL_DATE_BASE;
|
let 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);
|
let fraction = (local_time.num_seconds_from_midnight() as f64) / (60.0 * 60.0 * 24.0);
|
||||||
days_from_1900 as f64 + fraction
|
days_from_1900 as f64 + fraction
|
||||||
@@ -978,7 +979,7 @@ impl Model {
|
|||||||
message: "Wrong number of arguments".to_string(),
|
message: "Wrong number of arguments".to_string(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
match self.current_excel_serial() {
|
match self.current_excel_serial_with_timezone(self.tz) {
|
||||||
Some(serial) => CalcResult::Number(serial.floor()),
|
Some(serial) => CalcResult::Number(serial.floor()),
|
||||||
None => CalcResult::Error {
|
None => CalcResult::Error {
|
||||||
error: Error::ERROR,
|
error: Error::ERROR,
|
||||||
@@ -989,14 +990,35 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fn_now(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_now(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if !args.is_empty() {
|
if args.len() > 1 {
|
||||||
return CalcResult::Error {
|
return CalcResult::Error {
|
||||||
error: Error::ERROR,
|
error: Error::ERROR,
|
||||||
origin: cell,
|
origin: cell,
|
||||||
message: "Wrong number of arguments".to_string(),
|
message: "Wrong number of arguments".to_string(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
match self.current_excel_serial() {
|
let tz = match args.first() {
|
||||||
|
Some(arg0) => {
|
||||||
|
// Parse timezone argument
|
||||||
|
let tz_str = match self.get_string(arg0, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
let tz: Tz = match tz_str.parse() {
|
||||||
|
Ok(tz) => tz,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: format!("Invalid timezone: {}", &tz_str),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tz
|
||||||
|
}
|
||||||
|
None => self.tz,
|
||||||
|
};
|
||||||
|
match self.current_excel_serial_with_timezone(tz) {
|
||||||
Some(serial) => CalcResult::Number(serial),
|
Some(serial) => CalcResult::Number(serial),
|
||||||
None => CalcResult::Error {
|
None => CalcResult::Error {
|
||||||
error: Error::ERROR,
|
error: Error::ERROR,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use crate::cast::NumberOrArray;
|
use crate::cast::NumberOrArray;
|
||||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
use crate::constants::{EXCEL_PRECISION, LAST_COLUMN, LAST_ROW};
|
||||||
use crate::expressions::parser::ArrayNode;
|
use crate::expressions::parser::ArrayNode;
|
||||||
use crate::expressions::types::CellReferenceIndex;
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
use crate::functions::math_util::{from_roman, to_roman_with_form};
|
use crate::functions::math_util::{from_roman, to_roman_with_form};
|
||||||
use crate::number_format::to_precision;
|
use crate::number_format::{to_excel_precision, to_precision};
|
||||||
use crate::single_number_fn;
|
use crate::single_number_fn;
|
||||||
use crate::{
|
use crate::{
|
||||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
@@ -984,7 +984,9 @@ impl Model {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = f64::floor(value / significance) * significance;
|
// Apply Excel precision to the ratio to handle floating-point rounding errors
|
||||||
|
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||||
|
let result = f64::floor(ratio) * significance;
|
||||||
CalcResult::Number(result)
|
CalcResult::Number(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1121,10 +1123,14 @@ impl Model {
|
|||||||
}
|
}
|
||||||
let significance = significance.abs();
|
let significance = significance.abs();
|
||||||
if value < 0.0 && mode != 0.0 {
|
if value < 0.0 && mode != 0.0 {
|
||||||
let result = f64::ceil(value / significance) * significance;
|
// Apply Excel precision to handle floating-point rounding errors
|
||||||
|
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||||
|
let result = f64::ceil(ratio) * significance;
|
||||||
CalcResult::Number(result)
|
CalcResult::Number(result)
|
||||||
} else {
|
} else {
|
||||||
let result = f64::floor(value / significance) * significance;
|
// Apply Excel precision to handle floating-point rounding errors
|
||||||
|
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||||
|
let result = f64::floor(ratio) * significance;
|
||||||
CalcResult::Number(result)
|
CalcResult::Number(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1154,7 +1160,9 @@ impl Model {
|
|||||||
return CalcResult::Number(0.0);
|
return CalcResult::Number(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = f64::floor(value / significance) * significance;
|
// Apply Excel precision to handle floating-point rounding errors
|
||||||
|
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
|
||||||
|
let result = f64::floor(ratio) * significance;
|
||||||
CalcResult::Number(result)
|
CalcResult::Number(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
230
base/src/functions/mathematical_sum.rs
Normal file
230
base/src/functions/mathematical_sum.rs
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
type TwoMatricesResult = (i32, i32, Vec<Option<f64>>, Vec<Option<f64>>);
|
||||||
|
|
||||||
|
// Helper to check if two shapes are the same or compatible 1D shapes
|
||||||
|
fn is_same_shape_or_1d(rows1: i32, cols1: i32, rows2: i32, cols2: i32) -> bool {
|
||||||
|
(rows1 == rows2 && cols1 == cols2)
|
||||||
|
|| (rows1 == 1 && cols2 == 1 && cols1 == rows2)
|
||||||
|
|| (rows2 == 1 && cols1 == 1 && cols2 == rows1)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// SUMX2MY2(array_x, array_y) - Returns the sum of the difference of squares
|
||||||
|
pub(crate) fn fn_sumx2my2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let result = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(s) => return s,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_, _, values_left, values_right) = result;
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||||
|
let x = x_opt.unwrap_or(0.0);
|
||||||
|
let y = y_opt.unwrap_or(0.0);
|
||||||
|
sum += x * x - y * y;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SUMX2PY2(array_x, array_y) - Returns the sum of the sum of squares
|
||||||
|
pub(crate) fn fn_sumx2py2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let result = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(s) => return s,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_rows, _cols, values_left, values_right) = result;
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||||
|
let x = x_opt.unwrap_or(0.0);
|
||||||
|
let y = y_opt.unwrap_or(0.0);
|
||||||
|
sum += x * x + y * y;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SUMXMY2(array_x, array_y) - Returns the sum of squares of differences
|
||||||
|
pub(crate) fn fn_sumxmy2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let result = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(s) => return s,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_, _, values_left, values_right) = result;
|
||||||
|
|
||||||
|
let mut sum = 0.0;
|
||||||
|
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||||
|
let x = x_opt.unwrap_or(0.0);
|
||||||
|
let y = y_opt.unwrap_or(0.0);
|
||||||
|
let diff = x - y;
|
||||||
|
sum += diff * diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to extract and validate two matrices (ranges or arrays) with compatible shapes.
|
||||||
|
// Returns (rows, cols, values_left, values_right) or an error.
|
||||||
|
pub(crate) fn fn_get_two_matrices(
|
||||||
|
&mut self,
|
||||||
|
args: &[Node],
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> Result<TwoMatricesResult, CalcResult> {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return Err(CalcResult::new_args_number_error(cell));
|
||||||
|
}
|
||||||
|
let x_range = self.evaluate_node_in_context(&args[0], cell);
|
||||||
|
let y_range = self.evaluate_node_in_context(&args[1], cell);
|
||||||
|
|
||||||
|
let result = match (x_range, y_range) {
|
||||||
|
(
|
||||||
|
CalcResult::Range {
|
||||||
|
left: l1,
|
||||||
|
right: r1,
|
||||||
|
},
|
||||||
|
CalcResult::Range {
|
||||||
|
left: l2,
|
||||||
|
right: r2,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
if l1.sheet != l2.sheet {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let rows1 = r1.row - l1.row + 1;
|
||||||
|
let cols1 = r1.column - l1.column + 1;
|
||||||
|
let rows2 = r2.row - l2.row + 1;
|
||||||
|
let cols2 = r2.column - l2.column + 1;
|
||||||
|
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges must be of the same shape".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let values_left = self.values_from_range(l1, r1)?;
|
||||||
|
let values_right = self.values_from_range(l2, r2)?;
|
||||||
|
(rows1, cols1, values_left, values_right)
|
||||||
|
}
|
||||||
|
(
|
||||||
|
CalcResult::Array(left),
|
||||||
|
CalcResult::Range {
|
||||||
|
left: l2,
|
||||||
|
right: r2,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
let rows2 = r2.row - l2.row + 1;
|
||||||
|
let cols2 = r2.column - l2.column + 1;
|
||||||
|
|
||||||
|
let rows1 = left.len() as i32;
|
||||||
|
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
|
||||||
|
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Array and range must be of the same shape".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let values_left = match self.values_from_array(left) {
|
||||||
|
Err(error) => {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in first array: {:?}", error),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(v) => v,
|
||||||
|
};
|
||||||
|
let values_right = self.values_from_range(l2, r2)?;
|
||||||
|
(rows2, cols2, values_left, values_right)
|
||||||
|
}
|
||||||
|
(
|
||||||
|
CalcResult::Range {
|
||||||
|
left: l1,
|
||||||
|
right: r1,
|
||||||
|
},
|
||||||
|
CalcResult::Array(right),
|
||||||
|
) => {
|
||||||
|
let rows1 = r1.row - l1.row + 1;
|
||||||
|
let cols1 = r1.column - l1.column + 1;
|
||||||
|
|
||||||
|
let rows2 = right.len() as i32;
|
||||||
|
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
|
||||||
|
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Range and array must be of the same shape".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let values_left = self.values_from_range(l1, r1)?;
|
||||||
|
let values_right = match self.values_from_array(right) {
|
||||||
|
Err(error) => {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in second array: {:?}", error),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(v) => v,
|
||||||
|
};
|
||||||
|
(rows1, cols1, values_left, values_right)
|
||||||
|
}
|
||||||
|
(CalcResult::Array(left), CalcResult::Array(right)) => {
|
||||||
|
let rows1 = left.len() as i32;
|
||||||
|
let rows2 = right.len() as i32;
|
||||||
|
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
|
||||||
|
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
|
||||||
|
|
||||||
|
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Arrays must be of the same shape".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let values_left = match self.values_from_array(left) {
|
||||||
|
Err(error) => {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in first array: {:?}", error),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(v) => v,
|
||||||
|
};
|
||||||
|
let values_right = match self.values_from_array(right) {
|
||||||
|
Err(error) => {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in second array: {:?}", error),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(v) => v,
|
||||||
|
};
|
||||||
|
(rows1, cols1, values_left, values_right)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Both arguments must be ranges or arrays".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ mod lookup_and_reference;
|
|||||||
mod macros;
|
mod macros;
|
||||||
mod math_util;
|
mod math_util;
|
||||||
mod mathematical;
|
mod mathematical;
|
||||||
|
mod mathematical_sum;
|
||||||
mod statistical;
|
mod statistical;
|
||||||
mod subtotal;
|
mod subtotal;
|
||||||
mod text;
|
mod text;
|
||||||
@@ -76,6 +77,9 @@ pub enum Function {
|
|||||||
Sum,
|
Sum,
|
||||||
Sumif,
|
Sumif,
|
||||||
Sumifs,
|
Sumifs,
|
||||||
|
Sumx2my2,
|
||||||
|
Sumx2py2,
|
||||||
|
Sumxmy2,
|
||||||
Tan,
|
Tan,
|
||||||
Tanh,
|
Tanh,
|
||||||
Acot,
|
Acot,
|
||||||
@@ -203,7 +207,6 @@ pub enum Function {
|
|||||||
ChisqTest,
|
ChisqTest,
|
||||||
ConfidenceNorm,
|
ConfidenceNorm,
|
||||||
ConfidenceT,
|
ConfidenceT,
|
||||||
// Correl,
|
|
||||||
CovarianceP,
|
CovarianceP,
|
||||||
CovarianceS,
|
CovarianceS,
|
||||||
Devsq,
|
Devsq,
|
||||||
@@ -212,7 +215,7 @@ pub enum Function {
|
|||||||
FDistRT,
|
FDistRT,
|
||||||
FInv,
|
FInv,
|
||||||
FInvRT,
|
FInvRT,
|
||||||
// FTest,
|
FTest,
|
||||||
Fisher,
|
Fisher,
|
||||||
FisherInv,
|
FisherInv,
|
||||||
// Forecast,
|
// Forecast,
|
||||||
@@ -221,20 +224,18 @@ pub enum Function {
|
|||||||
GammaInv,
|
GammaInv,
|
||||||
GammaLn,
|
GammaLn,
|
||||||
GammaLnPrecise,
|
GammaLnPrecise,
|
||||||
// Gauss,
|
Gauss,
|
||||||
// Growth,
|
Harmean,
|
||||||
// Harmean,
|
|
||||||
HypGeomDist,
|
HypGeomDist,
|
||||||
// Intercept,
|
Kurt,
|
||||||
// Kurt,
|
Large,
|
||||||
// Large,
|
|
||||||
// Linest,
|
// Linest,
|
||||||
// Logest,
|
// Logest,
|
||||||
LogNormDist,
|
LogNormDist,
|
||||||
LogNormInv,
|
LogNormInv,
|
||||||
// MaxA,
|
MaxA,
|
||||||
// Median,
|
Median,
|
||||||
// MinA,
|
MinA,
|
||||||
// ModeMult,
|
// ModeMult,
|
||||||
// ModeSingl,
|
// ModeSingl,
|
||||||
NegbinomDist,
|
NegbinomDist,
|
||||||
@@ -254,19 +255,16 @@ pub enum Function {
|
|||||||
// Prob,
|
// Prob,
|
||||||
// QuartileExc,
|
// QuartileExc,
|
||||||
// QuartileInc,
|
// QuartileInc,
|
||||||
// RankAvg,
|
RankAvg,
|
||||||
// RankEq,
|
RankEq,
|
||||||
// Rsq
|
Skew,
|
||||||
// Skew,
|
SkewP,
|
||||||
// SkewP,
|
Small,
|
||||||
// Slope,
|
|
||||||
// Small,
|
|
||||||
Standardize,
|
Standardize,
|
||||||
StDevP,
|
StDevP,
|
||||||
StDevS,
|
StDevS,
|
||||||
Stdeva,
|
Stdeva,
|
||||||
Stdevpa,
|
Stdevpa,
|
||||||
// Steyx,
|
|
||||||
TDist,
|
TDist,
|
||||||
TDist2T,
|
TDist2T,
|
||||||
TDistRT,
|
TDistRT,
|
||||||
@@ -417,10 +415,16 @@ pub enum Function {
|
|||||||
Dvar,
|
Dvar,
|
||||||
Dvarp,
|
Dvarp,
|
||||||
Dstdevp,
|
Dstdevp,
|
||||||
|
|
||||||
|
Correl,
|
||||||
|
Rsq,
|
||||||
|
Intercept,
|
||||||
|
Slope,
|
||||||
|
Steyx,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Function {
|
impl Function {
|
||||||
pub fn into_iter() -> IntoIter<Function, 324> {
|
pub fn into_iter() -> IntoIter<Function, 345> {
|
||||||
[
|
[
|
||||||
Function::And,
|
Function::And,
|
||||||
Function::False,
|
Function::False,
|
||||||
@@ -497,6 +501,9 @@ impl Function {
|
|||||||
Function::Sum,
|
Function::Sum,
|
||||||
Function::Sumif,
|
Function::Sumif,
|
||||||
Function::Sumifs,
|
Function::Sumifs,
|
||||||
|
Function::Sumx2my2,
|
||||||
|
Function::Sumx2py2,
|
||||||
|
Function::Sumxmy2,
|
||||||
Function::Choose,
|
Function::Choose,
|
||||||
Function::Column,
|
Function::Column,
|
||||||
Function::Columns,
|
Function::Columns,
|
||||||
@@ -711,6 +718,7 @@ impl Function {
|
|||||||
Function::FDistRT,
|
Function::FDistRT,
|
||||||
Function::FInv,
|
Function::FInv,
|
||||||
Function::FInvRT,
|
Function::FInvRT,
|
||||||
|
Function::FTest,
|
||||||
Function::Fisher,
|
Function::Fisher,
|
||||||
Function::FisherInv,
|
Function::FisherInv,
|
||||||
Function::Gamma,
|
Function::Gamma,
|
||||||
@@ -746,6 +754,23 @@ impl Function {
|
|||||||
Function::VarA,
|
Function::VarA,
|
||||||
Function::WeibullDist,
|
Function::WeibullDist,
|
||||||
Function::ZTest,
|
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()
|
.into_iter()
|
||||||
}
|
}
|
||||||
@@ -837,6 +862,7 @@ impl Function {
|
|||||||
Function::FDistRT => "_xlfn.F.DIST.RT".to_string(),
|
Function::FDistRT => "_xlfn.F.DIST.RT".to_string(),
|
||||||
Function::FInv => "_xlfn.F.INV".to_string(),
|
Function::FInv => "_xlfn.F.INV".to_string(),
|
||||||
Function::FInvRT => "_xlfn.F.INV.RT".to_string(),
|
Function::FInvRT => "_xlfn.F.INV.RT".to_string(),
|
||||||
|
Function::FTest => "_xlfn.F.TEST".to_string(),
|
||||||
|
|
||||||
Function::HypGeomDist => "_xlfn.HYPGEOM.DIST".to_string(),
|
Function::HypGeomDist => "_xlfn.HYPGEOM.DIST".to_string(),
|
||||||
|
|
||||||
@@ -867,6 +893,9 @@ impl Function {
|
|||||||
|
|
||||||
Function::WeibullDist => "_xlfn.WEIBULL.DIST".to_string(),
|
Function::WeibullDist => "_xlfn.WEIBULL.DIST".to_string(),
|
||||||
Function::ZTest => "_xlfn.Z.TEST".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(),
|
_ => self.to_string(),
|
||||||
}
|
}
|
||||||
@@ -1186,6 +1215,7 @@ impl Function {
|
|||||||
"F.DIST.RT" | "_XLFN.F.DIST.RT" => Some(Function::FDistRT),
|
"F.DIST.RT" | "_XLFN.F.DIST.RT" => Some(Function::FDistRT),
|
||||||
"F.INV" | "_XLFN.F.INV" => Some(Function::FInv),
|
"F.INV" | "_XLFN.F.INV" => Some(Function::FInv),
|
||||||
"F.INV.RT" | "_XLFN.F.INV.RT" => Some(Function::FInvRT),
|
"F.INV.RT" | "_XLFN.F.INV.RT" => Some(Function::FInvRT),
|
||||||
|
"F.TEST" | "_XLFN.F.TEST" => Some(Function::FTest),
|
||||||
"FISHER" => Some(Function::Fisher),
|
"FISHER" => Some(Function::Fisher),
|
||||||
"FISHERINV" => Some(Function::FisherInv),
|
"FISHERINV" => Some(Function::FisherInv),
|
||||||
"GAMMA" | "_XLFN.GAMMA" => Some(Function::Gamma),
|
"GAMMA" | "_XLFN.GAMMA" => Some(Function::Gamma),
|
||||||
@@ -1221,6 +1251,28 @@ impl Function {
|
|||||||
"VARA" => Some(Function::VarA),
|
"VARA" => Some(Function::VarA),
|
||||||
"WEIBULL.DIST" | "_XLFN.WEIBULL.DIST" => Some(Function::WeibullDist),
|
"WEIBULL.DIST" | "_XLFN.WEIBULL.DIST" => Some(Function::WeibullDist),
|
||||||
"Z.TEST" | "_XLFN.Z.TEST" => Some(Function::ZTest),
|
"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,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -1483,7 +1535,6 @@ impl fmt::Display for Function {
|
|||||||
Function::Combin => write!(f, "COMBIN"),
|
Function::Combin => write!(f, "COMBIN"),
|
||||||
Function::Combina => write!(f, "COMBINA"),
|
Function::Combina => write!(f, "COMBINA"),
|
||||||
Function::Sumsq => write!(f, "SUMSQ"),
|
Function::Sumsq => write!(f, "SUMSQ"),
|
||||||
|
|
||||||
Function::N => write!(f, "N"),
|
Function::N => write!(f, "N"),
|
||||||
Function::Cell => write!(f, "CELL"),
|
Function::Cell => write!(f, "CELL"),
|
||||||
Function::Info => write!(f, "INFO"),
|
Function::Info => write!(f, "INFO"),
|
||||||
@@ -1500,7 +1551,6 @@ impl fmt::Display for Function {
|
|||||||
Function::Dvar => write!(f, "DVAR"),
|
Function::Dvar => write!(f, "DVAR"),
|
||||||
Function::Dvarp => write!(f, "DVARP"),
|
Function::Dvarp => write!(f, "DVARP"),
|
||||||
Function::Dstdevp => write!(f, "DSTDEVP"),
|
Function::Dstdevp => write!(f, "DSTDEVP"),
|
||||||
|
|
||||||
Function::BetaDist => write!(f, "BETA.DIST"),
|
Function::BetaDist => write!(f, "BETA.DIST"),
|
||||||
Function::BetaInv => write!(f, "BETA.INV"),
|
Function::BetaInv => write!(f, "BETA.INV"),
|
||||||
Function::BinomDist => write!(f, "BINOM.DIST"),
|
Function::BinomDist => write!(f, "BINOM.DIST"),
|
||||||
@@ -1523,6 +1573,7 @@ impl fmt::Display for Function {
|
|||||||
Function::FInvRT => write!(f, "F.INV.RT"),
|
Function::FInvRT => write!(f, "F.INV.RT"),
|
||||||
Function::Fisher => write!(f, "FISHER"),
|
Function::Fisher => write!(f, "FISHER"),
|
||||||
Function::FisherInv => write!(f, "FISHERINV"),
|
Function::FisherInv => write!(f, "FISHERINV"),
|
||||||
|
Function::FTest => write!(f, "F.TEST"),
|
||||||
Function::Gamma => write!(f, "GAMMA"),
|
Function::Gamma => write!(f, "GAMMA"),
|
||||||
Function::GammaDist => write!(f, "GAMMA.DIST"),
|
Function::GammaDist => write!(f, "GAMMA.DIST"),
|
||||||
Function::GammaInv => write!(f, "GAMMA.INV"),
|
Function::GammaInv => write!(f, "GAMMA.INV"),
|
||||||
@@ -1556,6 +1607,27 @@ impl fmt::Display for Function {
|
|||||||
Function::VarA => write!(f, "VARA"),
|
Function::VarA => write!(f, "VARA"),
|
||||||
Function::WeibullDist => write!(f, "WEIBULL.DIST"),
|
Function::WeibullDist => write!(f, "WEIBULL.DIST"),
|
||||||
Function::ZTest => write!(f, "Z.TEST"),
|
Function::ZTest => write!(f, "Z.TEST"),
|
||||||
|
Function::Sumx2my2 => write!(f, "SUMX2MY2"),
|
||||||
|
Function::Sumx2py2 => write!(f, "SUMX2PY2"),
|
||||||
|
Function::Sumxmy2 => write!(f, "SUMXMY2"),
|
||||||
|
Function::Correl => write!(f, "CORREL"),
|
||||||
|
Function::Rsq => write!(f, "RSQ"),
|
||||||
|
Function::Intercept => write!(f, "INTERCEPT"),
|
||||||
|
Function::Slope => write!(f, "SLOPE"),
|
||||||
|
Function::Steyx => write!(f, "STEYX"),
|
||||||
|
// new ones
|
||||||
|
Function::Gauss => write!(f, "GAUSS"),
|
||||||
|
Function::Harmean => write!(f, "HARMEAN"),
|
||||||
|
Function::Kurt => write!(f, "KURT"),
|
||||||
|
Function::Large => write!(f, "LARGE"),
|
||||||
|
Function::MaxA => write!(f, "MAXA"),
|
||||||
|
Function::Median => write!(f, "MEDIAN"),
|
||||||
|
Function::MinA => write!(f, "MINA"),
|
||||||
|
Function::RankAvg => write!(f, "RANK.AVG"),
|
||||||
|
Function::RankEq => write!(f, "RANK.EQ"),
|
||||||
|
Function::Skew => write!(f, "SKEW"),
|
||||||
|
Function::SkewP => write!(f, "SKEW.P"),
|
||||||
|
Function::Small => write!(f, "SMALL"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1875,6 +1947,7 @@ impl Model {
|
|||||||
Function::FInvRT => self.fn_f_inv_rt(args, cell),
|
Function::FInvRT => self.fn_f_inv_rt(args, cell),
|
||||||
Function::Fisher => self.fn_fisher(args, cell),
|
Function::Fisher => self.fn_fisher(args, cell),
|
||||||
Function::FisherInv => self.fn_fisher_inv(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::Gamma => self.fn_gamma(args, cell),
|
||||||
Function::GammaDist => self.fn_gamma_dist(args, cell),
|
Function::GammaDist => self.fn_gamma_dist(args, cell),
|
||||||
Function::GammaInv => self.fn_gamma_inv(args, cell),
|
Function::GammaInv => self.fn_gamma_inv(args, cell),
|
||||||
@@ -1908,6 +1981,26 @@ impl Model {
|
|||||||
Function::VarA => self.fn_vara(args, cell),
|
Function::VarA => self.fn_vara(args, cell),
|
||||||
Function::WeibullDist => self.fn_weibull_dist(args, cell),
|
Function::WeibullDist => self.fn_weibull_dist(args, cell),
|
||||||
Function::ZTest => self.fn_z_test(args, cell),
|
Function::ZTest => self.fn_z_test(args, cell),
|
||||||
|
Function::Sumx2my2 => self.fn_sumx2my2(args, cell),
|
||||||
|
Function::Sumx2py2 => self.fn_sumx2py2(args, cell),
|
||||||
|
Function::Sumxmy2 => self.fn_sumxmy2(args, cell),
|
||||||
|
Function::Correl => self.fn_correl(args, cell),
|
||||||
|
Function::Rsq => self.fn_rsq(args, cell),
|
||||||
|
Function::Intercept => self.fn_intercept(args, cell),
|
||||||
|
Function::Slope => self.fn_slope(args, cell),
|
||||||
|
Function::Steyx => self.fn_steyx(args, cell),
|
||||||
|
Function::Gauss => self.fn_gauss(args, cell),
|
||||||
|
Function::Harmean => self.fn_harmean(args, cell),
|
||||||
|
Function::Kurt => self.fn_kurt(args, cell),
|
||||||
|
Function::Large => self.fn_large(args, cell),
|
||||||
|
Function::MaxA => self.fn_maxa(args, cell),
|
||||||
|
Function::Median => self.fn_median(args, cell),
|
||||||
|
Function::MinA => self.fn_mina(args, cell),
|
||||||
|
Function::RankAvg => self.fn_rank_avg(args, cell),
|
||||||
|
Function::RankEq => self.fn_rank_eq(args, cell),
|
||||||
|
Function::Skew => self.fn_skew(args, cell),
|
||||||
|
Function::SkewP => self.fn_skew_p(args, cell),
|
||||||
|
Function::Small => self.fn_small(args, cell),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,6 @@ use crate::{
|
|||||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
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 {
|
impl Model {
|
||||||
// CHISQ.DIST(x, deg_freedom, cumulative)
|
// CHISQ.DIST(x, deg_freedom, cumulative)
|
||||||
pub(crate) fn fn_chisq_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_chisq_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
@@ -310,171 +303,10 @@ impl Model {
|
|||||||
|
|
||||||
// CHISQ.TEST(actual_range, expected_range)
|
// CHISQ.TEST(actual_range, expected_range)
|
||||||
pub(crate) fn fn_chisq_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_chisq_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() != 2 {
|
let (width, height, values_left, values_right) = match self.fn_get_two_matrices(args, cell)
|
||||||
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,
|
Ok(v) => v,
|
||||||
};
|
Err(r) => return r,
|
||||||
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());
|
let mut values = Vec::with_capacity(values_left.len());
|
||||||
|
|||||||
227
base/src/functions/statistical/correl.rs
Normal file
227
base/src/functions/statistical/correl.rs
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// CORREL(array1, array2) - Returns the correlation coefficient of two data sets
|
||||||
|
pub(crate) fn fn_correl(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (_, _, values_left, values_right) = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut n = 0.0;
|
||||||
|
let mut sum_x = 0.0;
|
||||||
|
let mut sum_y = 0.0;
|
||||||
|
let mut sum_x2 = 0.0;
|
||||||
|
let mut sum_y2 = 0.0;
|
||||||
|
let mut sum_xy = 0.0;
|
||||||
|
|
||||||
|
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
|
||||||
|
if let (Some(x), Some(y)) = (x_opt, y_opt) {
|
||||||
|
n += 1.0;
|
||||||
|
sum_x += x;
|
||||||
|
sum_y += y;
|
||||||
|
sum_x2 += x * x;
|
||||||
|
sum_y2 += y * y;
|
||||||
|
sum_xy += x * y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need at least 2 valid pairs
|
||||||
|
if n < 2.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"CORREL requires at least two numeric data points in each range".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let num = n * sum_xy - sum_x * sum_y;
|
||||||
|
let denom_x = n * sum_x2 - sum_x * sum_x;
|
||||||
|
let denom_y = n * sum_y2 - sum_y * sum_y;
|
||||||
|
let denom = (denom_x * denom_y).sqrt();
|
||||||
|
|
||||||
|
if denom == 0.0 || !denom.is_finite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Division by zero in CORREL".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = num / denom;
|
||||||
|
CalcResult::Number(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SLOPE(known_y's, known_x's) - Returns the slope of the linear regression line
|
||||||
|
pub(crate) fn fn_slope(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut n = 0.0;
|
||||||
|
let mut sum_x = 0.0;
|
||||||
|
let mut sum_y = 0.0;
|
||||||
|
let mut sum_x2 = 0.0;
|
||||||
|
let mut sum_xy = 0.0;
|
||||||
|
|
||||||
|
let len = values_y.len().min(values_x.len());
|
||||||
|
for i in 0..len {
|
||||||
|
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||||
|
n += 1.0;
|
||||||
|
sum_x += x;
|
||||||
|
sum_y += y;
|
||||||
|
sum_x2 += x * x;
|
||||||
|
sum_xy += x * y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 2.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"SLOPE requires at least two numeric data points".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let denom = n * sum_x2 - sum_x * sum_x;
|
||||||
|
if denom == 0.0 || !denom.is_finite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Division by zero in SLOPE".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let num = n * sum_xy - sum_x * sum_y;
|
||||||
|
let slope = num / denom;
|
||||||
|
|
||||||
|
CalcResult::Number(slope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// INTERCEPT(known_y's, known_x's) - Returns the y-intercept of the linear regression line
|
||||||
|
pub(crate) fn fn_intercept(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut n = 0.0;
|
||||||
|
let mut sum_x = 0.0;
|
||||||
|
let mut sum_y = 0.0;
|
||||||
|
let mut sum_x2 = 0.0;
|
||||||
|
let mut sum_xy = 0.0;
|
||||||
|
|
||||||
|
let len = values_y.len().min(values_x.len());
|
||||||
|
for i in 0..len {
|
||||||
|
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||||
|
n += 1.0;
|
||||||
|
sum_x += x;
|
||||||
|
sum_y += y;
|
||||||
|
sum_x2 += x * x;
|
||||||
|
sum_xy += x * y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 2.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"INTERCEPT requires at least two numeric data points".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let denom = n * sum_x2 - sum_x * sum_x;
|
||||||
|
if denom == 0.0 || !denom.is_finite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Division by zero in INTERCEPT".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let num = n * sum_xy - sum_x * sum_y;
|
||||||
|
let slope = num / denom;
|
||||||
|
let intercept = (sum_y - slope * sum_x) / n;
|
||||||
|
|
||||||
|
CalcResult::Number(intercept)
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEYX(known_y's, known_x's) - Returns the standard error of the predicted y-values
|
||||||
|
pub(crate) fn fn_steyx(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut n = 0.0;
|
||||||
|
let mut sum_x = 0.0;
|
||||||
|
let mut sum_y = 0.0;
|
||||||
|
let mut sum_x2 = 0.0;
|
||||||
|
let mut sum_xy = 0.0;
|
||||||
|
|
||||||
|
// We need the actual pairs again later for residuals
|
||||||
|
let mut pairs: Vec<(f64, f64)> = Vec::new();
|
||||||
|
|
||||||
|
let len = values_y.len().min(values_x.len());
|
||||||
|
for i in 0..len {
|
||||||
|
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
|
||||||
|
n += 1.0;
|
||||||
|
sum_x += x;
|
||||||
|
sum_y += y;
|
||||||
|
sum_x2 += x * x;
|
||||||
|
sum_xy += x * y;
|
||||||
|
pairs.push((x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need at least 3 points for STEYX (n - 2 in denominator)
|
||||||
|
if n < 3.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"STEYX requires at least three numeric data points".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let denom = n * sum_x2 - sum_x * sum_x;
|
||||||
|
if denom == 0.0 || !denom.is_finite() {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Division by zero in STEYX".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let num = n * sum_xy - sum_x * sum_y;
|
||||||
|
let slope = num / denom;
|
||||||
|
let intercept = (sum_y - slope * sum_x) / n;
|
||||||
|
|
||||||
|
// Sum of squared residuals: Σ (y - ŷ)^2, ŷ = intercept + slope * x
|
||||||
|
let mut sse = 0.0;
|
||||||
|
for (x, y) in pairs {
|
||||||
|
let y_hat = intercept + slope * x;
|
||||||
|
let diff = y - y_hat;
|
||||||
|
sse += diff * diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dof = n - 2.0;
|
||||||
|
if dof <= 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"STEYX has non-positive degrees of freedom".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sey = (sse / dof).sqrt();
|
||||||
|
if !sey.is_finite() {
|
||||||
|
return CalcResult::new_error(Error::DIV, cell, "Numerical error in STEYX".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(sey)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||||
use crate::expressions::parser::ArrayNode;
|
use crate::expressions::parser::ArrayNode;
|
||||||
use crate::expressions::types::CellReferenceIndex;
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
@@ -6,77 +8,219 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
fn for_each_value<F>(
|
||||||
if args.is_empty() {
|
&mut self,
|
||||||
return CalcResult::new_args_number_error(cell);
|
args: &[Node],
|
||||||
}
|
cell: CellReferenceIndex,
|
||||||
let mut count = 0.0;
|
mut f: F,
|
||||||
let mut sum = 0.0;
|
) -> Result<(), CalcResult>
|
||||||
|
where
|
||||||
|
F: FnMut(f64),
|
||||||
|
{
|
||||||
for arg in args {
|
for arg in args {
|
||||||
match self.evaluate_node_in_context(arg, cell) {
|
match self.evaluate_node_in_context(arg, cell) {
|
||||||
CalcResult::Number(value) => {
|
CalcResult::Number(value) => {
|
||||||
count += 1.0;
|
f(value);
|
||||||
sum += value;
|
|
||||||
}
|
}
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(value) => {
|
||||||
if let Node::ReferenceKind { .. } = arg {
|
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 {
|
} else {
|
||||||
sum += if b { 1.0 } else { 0.0 };
|
return Err(CalcResult::new_error(
|
||||||
count += 1.0;
|
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 } => {
|
CalcResult::Range { left, right } => {
|
||||||
if left.sheet != right.sheet {
|
if left.sheet != right.sheet {
|
||||||
return CalcResult::new_error(
|
return Err(CalcResult::new_error(
|
||||||
Error::VALUE,
|
Error::VALUE,
|
||||||
cell,
|
cell,
|
||||||
"Ranges are in different sheets".to_string(),
|
"Ranges are in different sheets".to_string(),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
for row in left.row..(right.row + 1) {
|
|
||||||
for column in left.column..(right.column + 1) {
|
for row in left.row..=right.row {
|
||||||
|
for column in left.column..=right.column {
|
||||||
match self.evaluate_cell(CellReferenceIndex {
|
match self.evaluate_cell(CellReferenceIndex {
|
||||||
sheet: left.sheet,
|
sheet: left.sheet,
|
||||||
row,
|
row,
|
||||||
column,
|
column,
|
||||||
}) {
|
}) {
|
||||||
CalcResult::Number(value) => {
|
CalcResult::Number(value) => {
|
||||||
count += 1.0;
|
f(value);
|
||||||
sum += value;
|
|
||||||
}
|
}
|
||||||
error @ CalcResult::Error { .. } => return error,
|
error @ CalcResult::Error { .. } => return Err(error),
|
||||||
CalcResult::Range { .. } => {
|
CalcResult::Range { .. } => {
|
||||||
return CalcResult::new_error(
|
return Err(CalcResult::new_error(
|
||||||
Error::ERROR,
|
Error::ERROR,
|
||||||
cell,
|
cell,
|
||||||
"Unexpected Range".to_string(),
|
"Unexpected Range".to_string(),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
error @ CalcResult::Error { .. } => return error,
|
error @ CalcResult::Error { .. } => return Err(error),
|
||||||
CalcResult::String(s) => {
|
// Everything else is ignored
|
||||||
if let Node::ReferenceKind { .. } = arg {
|
_ => {}
|
||||||
// Do nothing
|
}
|
||||||
} else if let Ok(t) = s.parse::<f64>() {
|
}
|
||||||
sum += t;
|
|
||||||
count += 1.0;
|
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 {
|
} else {
|
||||||
return CalcResult::Error {
|
return Err(CalcResult::new_error(
|
||||||
error: Error::VALUE,
|
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,
|
origin: cell,
|
||||||
message: "Argument cannot be cast into number".to_string(),
|
message: "Error in array".to_string(),
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
|
||||||
// Ignore everything else
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Range { left, right } => {
|
||||||
|
if left.sheet != right.sheet {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Ranges are in different sheets".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in left.row..=right.row {
|
||||||
|
for column in left.column..=right.column {
|
||||||
|
match self.evaluate_cell(CellReferenceIndex {
|
||||||
|
sheet: left.sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
}) {
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
f(value);
|
||||||
|
}
|
||||||
|
CalcResult::Boolean(b) => {
|
||||||
|
f(if b { 1.0 } else { 0.0 });
|
||||||
|
}
|
||||||
|
CalcResult::String(_) => {
|
||||||
|
f(0.0);
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return Err(error),
|
||||||
|
CalcResult::Range { .. } => {
|
||||||
|
return Err(CalcResult::new_error(
|
||||||
|
Error::ERROR,
|
||||||
|
cell,
|
||||||
|
"Unexpected Range".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error @ CalcResult::Error { .. } => return Err(error),
|
||||||
|
// Everything else is ignored
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
let mut count = 0.0;
|
||||||
|
let mut sum = 0.0;
|
||||||
|
if let Err(e) = self.for_each_value(args, cell, |f| {
|
||||||
|
count += 1.0;
|
||||||
|
sum += f;
|
||||||
|
}) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
if count == 0.0 {
|
if count == 0.0 {
|
||||||
return CalcResult::Error {
|
return CalcResult::Error {
|
||||||
error: Error::DIV,
|
error: Error::DIV,
|
||||||
@@ -86,6 +230,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
CalcResult::Number(sum / count)
|
CalcResult::Number(sum / count)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fn_averagea(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_averagea(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.is_empty() {
|
if args.is_empty() {
|
||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
@@ -443,4 +588,484 @@ impl Model {
|
|||||||
|
|
||||||
CalcResult::Number(sum_abs_dev / n)
|
CalcResult::Number(sum_abs_dev / n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_median(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut values: Vec<f64> = Vec::new();
|
||||||
|
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if values.is_empty() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "No numeric values for MEDIAN".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
|
||||||
|
|
||||||
|
let n = values.len();
|
||||||
|
let median = if n % 2 == 1 {
|
||||||
|
// odd
|
||||||
|
values[n / 2]
|
||||||
|
} else {
|
||||||
|
// even: average of the two middle values
|
||||||
|
let a = values[(n / 2) - 1];
|
||||||
|
let b = values[n / 2];
|
||||||
|
(a + b) / 2.0
|
||||||
|
};
|
||||||
|
|
||||||
|
CalcResult::Number(median)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_harmean(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut values: Vec<f64> = Vec::new();
|
||||||
|
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if values.is_empty() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "Division by Zero".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Excel HARMEAN: all values must be > 0
|
||||||
|
if values.iter().any(|&v| v <= 0.0) {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "HARMEAN requires strictly positive values".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = values.len() as f64;
|
||||||
|
let sum_recip: f64 = values.iter().map(|v| 1.0 / v).sum();
|
||||||
|
|
||||||
|
if sum_recip == 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "Division by Zero".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(n / sum_recip)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_mina(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
let mut mina: Option<f64> = None;
|
||||||
|
if let Err(e) = self.for_each_value_a(args, cell, |f| {
|
||||||
|
if let Some(m) = mina {
|
||||||
|
mina = Some(m.min(f));
|
||||||
|
} else {
|
||||||
|
mina = Some(f);
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
if let Some(mina) = mina {
|
||||||
|
CalcResult::Number(mina)
|
||||||
|
} else {
|
||||||
|
CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "No numeric values for MINA".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_maxa(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
let mut maxa: Option<f64> = None;
|
||||||
|
if let Err(e) = self.for_each_value_a(args, cell, |f| {
|
||||||
|
if let Some(m) = maxa {
|
||||||
|
maxa = Some(m.max(f));
|
||||||
|
} else {
|
||||||
|
maxa = Some(f);
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
if let Some(maxa) = maxa {
|
||||||
|
CalcResult::Number(maxa)
|
||||||
|
} else {
|
||||||
|
CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "No numeric values for MAXA".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_skew(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
// Sample skewness (Excel SKEW)
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut values: Vec<f64> = Vec::new();
|
||||||
|
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = values.len();
|
||||||
|
if n < 3 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "SKEW requires at least 3 data points".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let n_f = n as f64;
|
||||||
|
let mean = values.iter().sum::<f64>() / n_f;
|
||||||
|
|
||||||
|
let mut m2 = 0.0;
|
||||||
|
for &x in &values {
|
||||||
|
let d = x - mean;
|
||||||
|
m2 += d * d;
|
||||||
|
}
|
||||||
|
|
||||||
|
if m2 == 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "Zero variance in SKEW".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = (m2 / (n_f - 1.0)).sqrt();
|
||||||
|
|
||||||
|
let mut sum_cubed = 0.0;
|
||||||
|
for &x in &values {
|
||||||
|
let z = (x - mean) / s;
|
||||||
|
sum_cubed += z * z * z;
|
||||||
|
}
|
||||||
|
|
||||||
|
let skew = (n_f / ((n_f - 1.0) * (n_f - 2.0))) * sum_cubed;
|
||||||
|
CalcResult::Number(skew)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_skew_p(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
// Population skewness (Excel SKEW.P)
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut values: Vec<f64> = Vec::new();
|
||||||
|
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = values.len();
|
||||||
|
if n < 2 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "SKEW.P requires at least 2 data points".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let n_f = n as f64;
|
||||||
|
let mean = values.iter().sum::<f64>() / n_f;
|
||||||
|
|
||||||
|
let mut m2 = 0.0;
|
||||||
|
for &x in &values {
|
||||||
|
let d = x - mean;
|
||||||
|
m2 += d * d;
|
||||||
|
}
|
||||||
|
|
||||||
|
if m2 == 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "Zero variance in SKEW.P".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let sigma = (m2 / n_f).sqrt();
|
||||||
|
|
||||||
|
let mut sum_cubed = 0.0;
|
||||||
|
for &x in &values {
|
||||||
|
let z = (x - mean) / sigma;
|
||||||
|
sum_cubed += z * z * z;
|
||||||
|
}
|
||||||
|
|
||||||
|
let skew_p = sum_cubed / n_f;
|
||||||
|
CalcResult::Number(skew_p)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_kurt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.is_empty() {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut values: Vec<f64> = Vec::new();
|
||||||
|
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = values.len();
|
||||||
|
if n < 4 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "KURT requires at least 4 data points".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let n_f = n as f64;
|
||||||
|
let mean = values.iter().sum::<f64>() / n_f;
|
||||||
|
|
||||||
|
let mut m2 = 0.0;
|
||||||
|
for &x in &values {
|
||||||
|
let d = x - mean;
|
||||||
|
m2 += d * d;
|
||||||
|
}
|
||||||
|
|
||||||
|
if m2 == 0.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::DIV,
|
||||||
|
origin: cell,
|
||||||
|
message: "Zero variance in KURT".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = (m2 / (n_f - 1.0)).sqrt();
|
||||||
|
|
||||||
|
let mut sum_fourth = 0.0;
|
||||||
|
for &x in &values {
|
||||||
|
let z = (x - mean) / s;
|
||||||
|
sum_fourth += z * z * z * z;
|
||||||
|
}
|
||||||
|
|
||||||
|
let term1 = (n_f * (n_f + 1.0)) / ((n_f - 1.0) * (n_f - 2.0) * (n_f - 3.0)) * sum_fourth;
|
||||||
|
let term2 = 3.0 * (n_f - 1.0) * (n_f - 1.0) / ((n_f - 2.0) * (n_f - 3.0));
|
||||||
|
|
||||||
|
let kurt = term1 - term2;
|
||||||
|
CalcResult::Number(kurt)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_large(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
let values = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
|
CalcResult::Array(array) => match self.values_from_array(array) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: format!("Unsupported array argument: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => return e,
|
||||||
|
},
|
||||||
|
CalcResult::Boolean(value) => {
|
||||||
|
if !matches!(args[0], Node::ReferenceKind { .. }) {
|
||||||
|
vec![Some(if value { 1.0 } else { 0.0 })]
|
||||||
|
} else {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Unsupported argument type".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
if !matches!(args[0], Node::ReferenceKind { .. }) {
|
||||||
|
vec![Some(value)]
|
||||||
|
} else {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Unsupported argument type".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::String(value) => {
|
||||||
|
if !matches!(args[0], Node::ReferenceKind { .. }) {
|
||||||
|
if let Some(parsed) = self.cast_number(&value) {
|
||||||
|
vec![Some(parsed)]
|
||||||
|
} else {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "Unsupported argument type".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Unsupported argument type".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NIMPL,
|
||||||
|
origin: cell,
|
||||||
|
message: "Unsupported argument type".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let k = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(s) => return s,
|
||||||
|
};
|
||||||
|
if k < 1.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "K must be >= 1".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let mut numeric_values: Vec<f64> = values.into_iter().flatten().collect();
|
||||||
|
if numeric_values.is_empty() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "No numeric values for LARGE".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
numeric_values.sort_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||||
|
let k_usize = k as usize;
|
||||||
|
if k_usize > numeric_values.len() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "K is larger than the number of data points".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
CalcResult::Number(numeric_values[k_usize - 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fn_small(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let values = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
|
CalcResult::Array(array) => match self.values_from_array(array) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: format!("Unsupported array argument: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => return e,
|
||||||
|
},
|
||||||
|
CalcResult::Boolean(value) => {
|
||||||
|
if !matches!(args[0], Node::ReferenceKind { .. }) {
|
||||||
|
vec![Some(if value { 1.0 } else { 0.0 })]
|
||||||
|
} else {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Unsupported argument type".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
if !matches!(args[0], Node::ReferenceKind { .. }) {
|
||||||
|
vec![Some(value)]
|
||||||
|
} else {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Unsupported argument type".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcResult::String(value) => {
|
||||||
|
if !matches!(args[0], Node::ReferenceKind { .. }) {
|
||||||
|
if let Some(parsed) = self.cast_number(&value) {
|
||||||
|
vec![Some(parsed)]
|
||||||
|
} else {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: "Unsupported argument type".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Unsupported argument type".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NIMPL,
|
||||||
|
origin: cell,
|
||||||
|
message: "Unsupported argument type".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let k = match self.get_number_no_bools(&args[1], cell) {
|
||||||
|
Ok(f) => f.trunc(),
|
||||||
|
Err(s) => return s,
|
||||||
|
};
|
||||||
|
|
||||||
|
if k < 1.0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "K must be >= 1".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut numeric_values: Vec<f64> = values.into_iter().flatten().collect();
|
||||||
|
if numeric_values.is_empty() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "No numeric values for SMALL".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// For SMALL, sort ascending
|
||||||
|
numeric_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
|
||||||
|
|
||||||
|
let k_usize = k as usize;
|
||||||
|
if k_usize > numeric_values.len() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "K is larger than the number of data points".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(numeric_values[k_usize - 1])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use statrs::distribution::{Continuous, ContinuousCDF, FisherSnedecor};
|
use statrs::distribution::{Continuous, ContinuousCDF, FisherSnedecor};
|
||||||
|
|
||||||
use crate::expressions::types::CellReferenceIndex;
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::functions::statistical::t_dist::sample_var;
|
||||||
use crate::{
|
use crate::{
|
||||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
};
|
};
|
||||||
@@ -296,4 +297,122 @@ impl Model {
|
|||||||
|
|
||||||
CalcResult::Number(x)
|
CalcResult::Number(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// F.TEST(array1, array2)
|
||||||
|
pub(crate) fn fn_f_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
let values1_opts = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
|
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => return error,
|
||||||
|
},
|
||||||
|
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in first array: {:?}", error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"First argument must be a range or array".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get second sample as Vec<Option<f64>>
|
||||||
|
let values2_opts = match self.evaluate_node_in_context(&args[1], cell) {
|
||||||
|
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => return error,
|
||||||
|
},
|
||||||
|
CalcResult::Array(a) => match self.values_from_array(a) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
format!("Error in second array: {:?}", error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::VALUE,
|
||||||
|
cell,
|
||||||
|
"Second argument must be a range or array".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let values1: Vec<f64> = values1_opts.into_iter().flatten().collect();
|
||||||
|
let values2: Vec<f64> = values2_opts.into_iter().flatten().collect();
|
||||||
|
|
||||||
|
let n1 = values1.len();
|
||||||
|
let n2 = values2.len();
|
||||||
|
|
||||||
|
// If fewer than 2 numeric values in either sample -> #DIV/0!
|
||||||
|
if n1 < 2 || n2 < 2 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"F.TEST requires at least two numeric values in each sample".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let v1 = sample_var(&values1);
|
||||||
|
let v2 = sample_var(&values2);
|
||||||
|
|
||||||
|
if v1 <= 0.0 || v2 <= 0.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"Variance of one sample is zero in F.TEST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// F ratio: larger variance / smaller variance
|
||||||
|
let mut f = v1 / v2;
|
||||||
|
let mut df1 = (n1 - 1) as f64;
|
||||||
|
let mut df2 = (n2 - 1) as f64;
|
||||||
|
|
||||||
|
if f < 1.0 {
|
||||||
|
f = 1.0 / f;
|
||||||
|
std::mem::swap(&mut df1, &mut df2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = match FisherSnedecor::new(df1, df2) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::NUM,
|
||||||
|
cell,
|
||||||
|
"Invalid parameters for F distribution in F.TEST".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// One-tailed right-tail probability
|
||||||
|
let tail = 1.0 - dist.cdf(f);
|
||||||
|
// F.TEST is two-tailed: p = 2 * tail (with F >= 1)
|
||||||
|
let mut p = 2.0 * tail;
|
||||||
|
|
||||||
|
// Clamp tiny FP noise
|
||||||
|
if p < 0.0 && p > -1e-15 {
|
||||||
|
p = 0.0;
|
||||||
|
}
|
||||||
|
if p > 1.0 && p < 1.0 + 1e-15 {
|
||||||
|
p = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(p)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
base/src/functions/statistical/gauss.rs
Normal file
39
base/src/functions/statistical/gauss.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use statrs::distribution::{ContinuousCDF, Normal};
|
||||||
|
|
||||||
|
use crate::expressions::token::Error;
|
||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{calc_result::CalcResult, expressions::parser::Node, model::Model};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub(crate) fn fn_gauss(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
let z = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(s) => return s,
|
||||||
|
};
|
||||||
|
let dist = match Normal::new(0.0, 1.0) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::ERROR,
|
||||||
|
origin: cell,
|
||||||
|
message: "Failed to construct standard normal distribution".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = dist.cdf(z) - 0.5;
|
||||||
|
|
||||||
|
if !result.is_finite() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Invalid result for GAUSS".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalcResult::Number(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
mod beta;
|
mod beta;
|
||||||
mod binom;
|
mod binom;
|
||||||
mod chisq;
|
mod chisq;
|
||||||
|
mod correl;
|
||||||
mod count_and_average;
|
mod count_and_average;
|
||||||
mod covariance;
|
mod covariance;
|
||||||
mod devsq;
|
mod devsq;
|
||||||
mod exponential;
|
mod exponential;
|
||||||
mod fisher;
|
mod fisher;
|
||||||
mod gamma;
|
mod gamma;
|
||||||
|
mod gauss;
|
||||||
mod geomean;
|
mod geomean;
|
||||||
mod hypegeom;
|
mod hypegeom;
|
||||||
mod if_ifs;
|
mod if_ifs;
|
||||||
@@ -15,6 +17,7 @@ mod normal;
|
|||||||
mod pearson;
|
mod pearson;
|
||||||
mod phi;
|
mod phi;
|
||||||
mod poisson;
|
mod poisson;
|
||||||
|
mod rank_eq_avg;
|
||||||
mod standard_dev;
|
mod standard_dev;
|
||||||
mod standardize;
|
mod standardize;
|
||||||
mod t_dist;
|
mod t_dist;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::expressions::types::CellReferenceIndex;
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
use crate::functions::statistical::chisq::is_same_shape_or_1d;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
};
|
};
|
||||||
@@ -7,176 +6,9 @@ use crate::{
|
|||||||
impl Model {
|
impl Model {
|
||||||
// PEARSON(array1, array2)
|
// PEARSON(array1, array2)
|
||||||
pub(crate) fn fn_pearson(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_pearson(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() != 2 {
|
let (_, _, values_left, values_right) = match self.fn_get_two_matrices(args, cell) {
|
||||||
return CalcResult::new_args_number_error(cell);
|
Ok(result) => result,
|
||||||
}
|
Err(e) => return e,
|
||||||
|
|
||||||
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)
|
// Flatten into (x, y) pairs, skipping non-numeric entries (None)
|
||||||
@@ -228,8 +60,54 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let denom = (denom_x * denom_y).sqrt();
|
let denom = (denom_x * denom_y).sqrt();
|
||||||
let r = num / denom;
|
|
||||||
|
|
||||||
CalcResult::Number(r)
|
CalcResult::Number(num / denom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RSQ(array1, array2) = CORREL(array1, array2)^2
|
||||||
|
pub(crate) fn fn_rsq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
let (_rows, _cols, values1, values2) = match self.fn_get_two_matrices(args, cell) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut n = 0.0_f64;
|
||||||
|
let mut sum_x = 0.0_f64;
|
||||||
|
let mut sum_y = 0.0_f64;
|
||||||
|
let mut sum_x2 = 0.0_f64;
|
||||||
|
let mut sum_y2 = 0.0_f64;
|
||||||
|
let mut sum_xy = 0.0_f64;
|
||||||
|
|
||||||
|
let len = values1.len().min(values2.len());
|
||||||
|
for i in 0..len {
|
||||||
|
if let (Some(x), Some(y)) = (values1[i], values2[i]) {
|
||||||
|
n += 1.0;
|
||||||
|
sum_x += x;
|
||||||
|
sum_y += y;
|
||||||
|
sum_x2 += x * x;
|
||||||
|
sum_y2 += y * y;
|
||||||
|
sum_xy += x * y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 2.0 {
|
||||||
|
return CalcResult::new_error(
|
||||||
|
Error::DIV,
|
||||||
|
cell,
|
||||||
|
"RSQ requires at least two numeric data points in each range".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let num = n * sum_xy - sum_x * sum_y;
|
||||||
|
let denom_x = n * sum_x2 - sum_x * sum_x;
|
||||||
|
let denom_y = n * sum_y2 - sum_y * sum_y;
|
||||||
|
let denom = (denom_x * denom_y).sqrt();
|
||||||
|
|
||||||
|
if denom == 0.0 || !denom.is_finite() {
|
||||||
|
return CalcResult::new_error(Error::DIV, cell, "Division by zero in RSQ".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = num / denom;
|
||||||
|
CalcResult::Number(r * r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
202
base/src/functions/statistical/rank_eq_avg.rs
Normal file
202
base/src/functions/statistical/rank_eq_avg.rs
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
|
use crate::{
|
||||||
|
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
// Helper to collect numeric values from the 2nd argument of RANK.*
|
||||||
|
fn collect_rank_values(
|
||||||
|
&mut self,
|
||||||
|
arg: &Node,
|
||||||
|
cell: CellReferenceIndex,
|
||||||
|
) -> Result<Vec<f64>, CalcResult> {
|
||||||
|
let values = match self.evaluate_node_in_context(arg, cell) {
|
||||||
|
CalcResult::Array(array) => match self.values_from_array(array) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(CalcResult::Error {
|
||||||
|
error: Error::VALUE,
|
||||||
|
origin: cell,
|
||||||
|
message: format!("Unsupported array argument: {}", e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CalcResult::Range { left, right } => self.values_from_range(left, right)?,
|
||||||
|
CalcResult::Boolean(value) => {
|
||||||
|
if !matches!(arg, Node::ReferenceKind { .. }) {
|
||||||
|
vec![Some(if value { 1.0 } else { 0.0 })]
|
||||||
|
} else {
|
||||||
|
return Err(CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "Unsupported argument type".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(CalcResult::Error {
|
||||||
|
error: Error::NIMPL,
|
||||||
|
origin: cell,
|
||||||
|
message: "Unsupported argument type".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let numeric_values: Vec<f64> = values.into_iter().flatten().collect();
|
||||||
|
Ok(numeric_values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RANK.EQ(number, ref, [order])
|
||||||
|
pub(crate) fn fn_rank_eq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if !(2..=3).contains(&args.len()) {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// number
|
||||||
|
let number = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ref
|
||||||
|
let mut values = match self.collect_rank_values(&args[1], cell) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if values.is_empty() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "No numeric values for RANK.EQ".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// order: default 0 (descending)
|
||||||
|
let order = if args.len() == 2 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
values.retain(|v| !v.is_nan());
|
||||||
|
|
||||||
|
// "better" = greater (descending) or smaller (ascending)
|
||||||
|
let mut better = 0;
|
||||||
|
let mut equal = 0;
|
||||||
|
|
||||||
|
if order == 0.0 {
|
||||||
|
// descending
|
||||||
|
for v in &values {
|
||||||
|
if *v > number {
|
||||||
|
better += 1;
|
||||||
|
} else if *v == number {
|
||||||
|
equal += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ascending
|
||||||
|
for v in &values {
|
||||||
|
if *v < number {
|
||||||
|
better += 1;
|
||||||
|
} else if *v == number {
|
||||||
|
equal += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if equal == 0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NA,
|
||||||
|
origin: cell,
|
||||||
|
message: "Number not found in reference for RANK.EQ".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let rank = (better as f64) + 1.0;
|
||||||
|
CalcResult::Number(rank)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RANK.AVG(number, ref, [order])
|
||||||
|
pub(crate) fn fn_rank_avg(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
|
if !(2..=3).contains(&args.len()) {
|
||||||
|
return CalcResult::new_args_number_error(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// number
|
||||||
|
let number = match self.get_number_no_bools(&args[0], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ref
|
||||||
|
let mut values = match self.collect_rank_values(&args[1], cell) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if values.is_empty() {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NUM,
|
||||||
|
origin: cell,
|
||||||
|
message: "No numeric values for RANK.AVG".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// order: default 0 (descending)
|
||||||
|
let order = if args.len() == 2 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
match self.get_number_no_bools(&args[2], cell) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => return e,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
values.retain(|v| !v.is_nan());
|
||||||
|
|
||||||
|
// > or < depending on order
|
||||||
|
let mut better = 0;
|
||||||
|
let mut equal = 0;
|
||||||
|
|
||||||
|
if order == 0.0 {
|
||||||
|
// descending
|
||||||
|
for v in &values {
|
||||||
|
if *v > number {
|
||||||
|
better += 1;
|
||||||
|
} else if *v == number {
|
||||||
|
equal += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ascending
|
||||||
|
for v in &values {
|
||||||
|
if *v < number {
|
||||||
|
better += 1;
|
||||||
|
} else if *v == number {
|
||||||
|
equal += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if equal == 0 {
|
||||||
|
return CalcResult::Error {
|
||||||
|
error: Error::NA,
|
||||||
|
origin: cell,
|
||||||
|
message: "Number not found in reference for RANK.AVG".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// For ties, average of the ranks. If the equal values occupy positions
|
||||||
|
// (better+1) ..= (better+equal), the average is:
|
||||||
|
// better + (equal + 1) / 2
|
||||||
|
let better_f = better as f64;
|
||||||
|
let equal_f = equal as f64;
|
||||||
|
let rank = better_f + (equal_f + 1.0) / 2.0;
|
||||||
|
|
||||||
|
CalcResult::Number(rank)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ fn mean(xs: &[f64]) -> f64 {
|
|||||||
s / (n as f64)
|
s / (n as f64)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sample_var(xs: &[f64]) -> f64 {
|
pub(crate) fn sample_var(xs: &[f64]) -> f64 {
|
||||||
let n = xs.len();
|
let n = xs.len();
|
||||||
if n < 2 {
|
if n < 2 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
mod test_actions;
|
mod test_actions;
|
||||||
|
mod test_arabic_roman;
|
||||||
mod test_binary_search;
|
mod test_binary_search;
|
||||||
mod test_cell;
|
mod test_cell;
|
||||||
mod test_cell_clear_contents;
|
mod test_cell_clear_contents;
|
||||||
@@ -39,12 +40,14 @@ mod test_metadata;
|
|||||||
mod test_model_cell_clear_all;
|
mod test_model_cell_clear_all;
|
||||||
mod test_model_is_empty_cell;
|
mod test_model_is_empty_cell;
|
||||||
mod test_move_formula;
|
mod test_move_formula;
|
||||||
|
mod test_mround_trunc_int;
|
||||||
mod test_quote_prefix;
|
mod test_quote_prefix;
|
||||||
mod test_row_column_styles;
|
mod test_row_column_styles;
|
||||||
mod test_set_user_input;
|
mod test_set_user_input;
|
||||||
mod test_sheet_markup;
|
mod test_sheet_markup;
|
||||||
mod test_sheets;
|
mod test_sheets;
|
||||||
mod test_styles;
|
mod test_styles;
|
||||||
|
mod test_sumsq;
|
||||||
mod test_trigonometric;
|
mod test_trigonometric;
|
||||||
mod test_true_false;
|
mod test_true_false;
|
||||||
mod test_weekday_return_types;
|
mod test_weekday_return_types;
|
||||||
@@ -65,6 +68,7 @@ mod test_escape_quotes;
|
|||||||
mod test_even_odd;
|
mod test_even_odd;
|
||||||
mod test_exp_sign;
|
mod test_exp_sign;
|
||||||
mod test_extend;
|
mod test_extend;
|
||||||
|
mod test_floor;
|
||||||
mod test_fn_datevalue_timevalue;
|
mod test_fn_datevalue_timevalue;
|
||||||
mod test_fn_fv;
|
mod test_fn_fv;
|
||||||
mod test_fn_round;
|
mod test_fn_round;
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ mod test_fn_covariance;
|
|||||||
mod test_fn_devsq;
|
mod test_fn_devsq;
|
||||||
mod test_fn_expon_dist;
|
mod test_fn_expon_dist;
|
||||||
mod test_fn_f;
|
mod test_fn_f;
|
||||||
|
mod test_fn_f_test;
|
||||||
mod test_fn_fisher;
|
mod test_fn_fisher;
|
||||||
|
mod test_fn_gauss;
|
||||||
mod test_fn_hyp_geom_dist;
|
mod test_fn_hyp_geom_dist;
|
||||||
mod test_fn_log_norm;
|
mod test_fn_log_norm;
|
||||||
mod test_fn_norm_dist;
|
mod test_fn_norm_dist;
|
||||||
|
|||||||
35
base/src/test/statistical/test_fn_f_test.rs
Normal file
35
base/src/test/statistical/test_fn_f_test.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_f_test_sanity() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// Valid call
|
||||||
|
model._set("A1", "=F.TEST(A2:A7, B2:B7)");
|
||||||
|
model._set("A2", "9");
|
||||||
|
model._set("A3", "12");
|
||||||
|
model._set("A4", "14");
|
||||||
|
model._set("A5", "16");
|
||||||
|
model._set("A6", "18");
|
||||||
|
model._set("A7", "20");
|
||||||
|
model._set("B2", "11");
|
||||||
|
model._set("B3", "10");
|
||||||
|
model._set("B4", "15");
|
||||||
|
model._set("B5", "17");
|
||||||
|
model._set("B6", "19");
|
||||||
|
model._set("B7", "21");
|
||||||
|
|
||||||
|
// Too few args
|
||||||
|
model._set("A8", "=F.TEST(A2:A7)");
|
||||||
|
|
||||||
|
// Too many args
|
||||||
|
model._set("A9", "=F.TEST(A2:A7, B2:B7, C2:C7)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0.859284302");
|
||||||
|
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A9"), *"#ERROR!");
|
||||||
|
}
|
||||||
35
base/src/test/statistical/test_fn_gauss.rs
Normal file
35
base/src/test/statistical/test_fn_gauss.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fn_gauss_smoke() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=GAUSS(-3)");
|
||||||
|
model._set("A2", "=GAUSS(-2.3)");
|
||||||
|
model._set("A3", "=GAUSS(-1.7)");
|
||||||
|
model._set("A4", "=GAUSS(0)");
|
||||||
|
model._set("A5", "=GAUSS(0.5)");
|
||||||
|
model._set("A6", "=GAUSS(1)");
|
||||||
|
model._set("A7", "=GAUSS(1.3)");
|
||||||
|
model._set("A8", "=GAUSS(3)");
|
||||||
|
model._set("A9", "=GAUSS(4)");
|
||||||
|
|
||||||
|
model._set("G6", "=GAUSS()");
|
||||||
|
model._set("G7", "=GAUSS(1, 1)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"-0.498650102");
|
||||||
|
assert_eq!(model._get_text("A2"), *"-0.48927589");
|
||||||
|
assert_eq!(model._get_text("A3"), *"-0.455434537");
|
||||||
|
assert_eq!(model._get_text("A4"), *"0");
|
||||||
|
assert_eq!(model._get_text("A5"), *"0.191462461");
|
||||||
|
assert_eq!(model._get_text("A6"), *"0.341344746");
|
||||||
|
assert_eq!(model._get_text("A7"), *"0.403199515");
|
||||||
|
assert_eq!(model._get_text("A8"), *"0.498650102");
|
||||||
|
assert_eq!(model._get_text("A9"), *"0.499968329");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("G6"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("G7"), *"#ERROR!");
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ use crate::test::util::new_empty_model;
|
|||||||
fn arguments() {
|
fn arguments() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
model._set("A1", "=ARABIC()");
|
model._set("A1", "=ARABIC()");
|
||||||
model._set("A2", "=ARABIC(V)");
|
model._set("A2", "=ARABIC(\"V\")");
|
||||||
model._set("A3", "=ARABIC(V, 2)");
|
model._set("A3", "=ARABIC(\"V\", 2)");
|
||||||
|
|
||||||
model._set("A4", "=ROMAN()");
|
model._set("A4", "=ROMAN()");
|
||||||
model._set("A5", "=ROMAN(5)");
|
model._set("A5", "=ROMAN(5)");
|
||||||
35
base/src/test/test_cell_info_n_sheets
Normal file
35
base/src/test/test_cell_info_n_sheets
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=CELL("address",A1)");
|
||||||
|
model._set("A2", "=CELL()");
|
||||||
|
|
||||||
|
model._set("A3", "=INFO("system")");
|
||||||
|
model._set("A4", "=INFO()");
|
||||||
|
|
||||||
|
model._set("A5", "=N(TRUE)");
|
||||||
|
model._set("A6", "=N()");
|
||||||
|
model._set("A7", "=N(1, 2)");
|
||||||
|
|
||||||
|
model._set("A8", "=SHEETS()");
|
||||||
|
model._set("A9", "=SHEETS(1)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"$A$1");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A3"), *"#N/IMPL!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A5"), *"1");
|
||||||
|
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A7"), *"#ERROR!");
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A8"), *"1");
|
||||||
|
assert_eq!(model._get_text("A9"), *"#N/IMPL!");
|
||||||
|
}
|
||||||
123
base/src/test/test_floor.rs
Normal file
123
base/src/test/test_floor.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_floor_floating_point_precision() {
|
||||||
|
// This test specifically checks the floating-point precision bug fix
|
||||||
|
// Bug: FLOOR(7.1, 0.1) was returning 7.0 instead of 7.1
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
// FLOOR tests
|
||||||
|
model._set("C5", "=FLOOR(7.1, 0.1)");
|
||||||
|
model._set("H7", "=FLOOR(-7.1, -0.1)");
|
||||||
|
|
||||||
|
// FLOOR.PRECISE tests
|
||||||
|
model._set("C53", "=FLOOR.PRECISE(7.1, 0.1)");
|
||||||
|
model._set("H53", "=FLOOR.PRECISE(7.1, -0.1)");
|
||||||
|
|
||||||
|
// FLOOR.MATH tests
|
||||||
|
model._set("C101", "=FLOOR.MATH(7.1, 0.1)");
|
||||||
|
model._set("H101", "=FLOOR.MATH(7.1, -0.1)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
// All should return 7.1
|
||||||
|
assert_eq!(model._get_text("C5"), *"7.1");
|
||||||
|
assert_eq!(model._get_text("H7"), *"-7.1");
|
||||||
|
assert_eq!(model._get_text("C53"), *"7.1");
|
||||||
|
assert_eq!(model._get_text("H53"), *"7.1");
|
||||||
|
assert_eq!(model._get_text("C101"), *"7.1");
|
||||||
|
assert_eq!(model._get_text("H101"), *"7.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_floor_additional_precision_cases() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=FLOOR(7.9, 0.1)");
|
||||||
|
model._set("A2", "=FLOOR(2.6, 0.5)");
|
||||||
|
model._set("A3", "=FLOOR(0.3, 0.1)"); // 0.1 + 0.2 type scenario
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"7.9");
|
||||||
|
assert_eq!(model._get_text("A2"), *"2.5");
|
||||||
|
assert_eq!(model._get_text("A3"), *"0.3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_floor_basic_cases() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=FLOOR(3.7, 2)");
|
||||||
|
model._set("A2", "=FLOOR(3.2, 1)");
|
||||||
|
model._set("A3", "=FLOOR(10, 3)");
|
||||||
|
model._set("A4", "=FLOOR(7, 2)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"2");
|
||||||
|
assert_eq!(model._get_text("A2"), *"3");
|
||||||
|
assert_eq!(model._get_text("A3"), *"9");
|
||||||
|
assert_eq!(model._get_text("A4"), *"6");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_floor_negative_numbers() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
// Both negative: rounds toward zero
|
||||||
|
model._set("A1", "=FLOOR(-2.5, -2)");
|
||||||
|
model._set("A2", "=FLOOR(-11, -3)");
|
||||||
|
|
||||||
|
// Negative number, positive significance: rounds away from zero
|
||||||
|
model._set("A3", "=FLOOR(-11, 3)");
|
||||||
|
model._set("A4", "=FLOOR(-2.5, 2)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"-2");
|
||||||
|
assert_eq!(model._get_text("A2"), *"-9");
|
||||||
|
assert_eq!(model._get_text("A3"), *"-12");
|
||||||
|
assert_eq!(model._get_text("A4"), *"-4");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_floor_error_cases() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
// Positive number with negative significance should error
|
||||||
|
model._set("A1", "=FLOOR(2.5, -2)");
|
||||||
|
model._set("A2", "=FLOOR(10, -3)");
|
||||||
|
|
||||||
|
// Division by zero
|
||||||
|
model._set("A3", "=FLOOR(5, 0)");
|
||||||
|
|
||||||
|
// Wrong number of arguments
|
||||||
|
model._set("A4", "=FLOOR(5)");
|
||||||
|
model._set("A5", "=FLOOR(5, 1, 1)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||||
|
assert_eq!(model._get_text("A3"), *"#DIV/0!");
|
||||||
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_floor_edge_cases() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
// Zero value
|
||||||
|
model._set("A1", "=FLOOR(0, 5)");
|
||||||
|
model._set("A2", "=FLOOR(0, 0)");
|
||||||
|
|
||||||
|
// Exact multiples
|
||||||
|
model._set("A3", "=FLOOR(10, 5)");
|
||||||
|
model._set("A4", "=FLOOR(9, 3)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"0");
|
||||||
|
assert_eq!(model._get_text("A2"), *"0");
|
||||||
|
assert_eq!(model._get_text("A3"), *"10");
|
||||||
|
assert_eq!(model._get_text("A4"), *"9");
|
||||||
|
}
|
||||||
@@ -20,7 +20,6 @@ fn arguments() {
|
|||||||
model._set("A11", "=INT(10.22, 1)");
|
model._set("A11", "=INT(10.22, 1)");
|
||||||
model._set("A12", "=INT(10.22, 1, 2)");
|
model._set("A12", "=INT(10.22, 1, 2)");
|
||||||
|
|
||||||
|
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||||
@@ -29,7 +28,7 @@ fn arguments() {
|
|||||||
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
assert_eq!(model._get_text("A4"), *"#ERROR!");
|
||||||
|
|
||||||
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
assert_eq!(model._get_text("A5"), *"#ERROR!");
|
||||||
assert_eq!(model._get_text("A6"), *"#ERROR!");
|
assert_eq!(model._get_text("A6"), *"10");
|
||||||
assert_eq!(model._get_text("A7"), *"10.2");
|
assert_eq!(model._get_text("A7"), *"10.2");
|
||||||
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
assert_eq!(model._get_text("A8"), *"#ERROR!");
|
||||||
|
|
||||||
@@ -7,15 +7,24 @@ const TIMESTAMP_2023: i64 = 1679319865208;
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arguments() {
|
fn arguments() {
|
||||||
|
mock_time::set_mock_time(TIMESTAMP_2023);
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
model._set("A1", "=NOW(1)");
|
model._set("A1", "=NOW(1, 1)");
|
||||||
|
model._set("A2", "=NOW(\"Europe/Berlin\")");
|
||||||
|
model._set("A3", "=NOW(\"faketimezone\")");
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model._get_text("A1"),
|
model._get_text("A1"),
|
||||||
"#ERROR!",
|
"#ERROR!",
|
||||||
"NOW should not accept arguments"
|
"Wrong number of arguments"
|
||||||
|
);
|
||||||
|
assert_eq!(model._get_text("A2"), *"20/03/2023 14:44:25");
|
||||||
|
assert_eq!(
|
||||||
|
model._get_text("A3"),
|
||||||
|
"#VALUE!",
|
||||||
|
"Invalid timezone: faketimezone"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
base/src/test/test_sumsq.rs
Normal file
17
base/src/test/test_sumsq.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arguments() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
model._set("A1", "=SUMSQ()");
|
||||||
|
model._set("A2", "=SUMSQ(2)");
|
||||||
|
model._set("A3", "=SUMSQ(1, 2)");
|
||||||
|
|
||||||
|
model.evaluate();
|
||||||
|
|
||||||
|
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||||
|
assert_eq!(model._get_text("A2"), *"4");
|
||||||
|
assert_eq!(model._get_text("A3"), *"5");
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
|
|
||||||
| Function | Status | Documentation |
|
| Function | Status | Documentation |
|
||||||
| ---------- | ---------------------------------------------- | ------------- |
|
| ---------- | ---------------------------------------------- | ------------- |
|
||||||
| CELL | <Badge type="info" text="Not implemented yet" /> | – |
|
| CELL | <Badge type="tip" text="Available" /> | – |
|
||||||
| ERROR.TYPE | <Badge type="tip" text="Available" /> | – |
|
| ERROR.TYPE | <Badge type="tip" text="Available" /> | – |
|
||||||
| INFO | <Badge type="info" text="Not implemented yet" /> | – |
|
| INFO | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| ISBLANK | <Badge type="tip" text="Available" /> | – |
|
| 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" /> | – |
|
| ISEVEN | <Badge type="tip" text="Available" /> | – |
|
||||||
| ISFORMULA | <Badge type="tip" text="Available" /> | – |
|
| ISFORMULA | <Badge type="tip" text="Available" /> | – |
|
||||||
| ISLOGICAL | <Badge type="tip" text="Available" /> | – |
|
| ISLOGICAL | <Badge type="tip" text="Available" /> | – |
|
||||||
| ISNA | <Badge type="info" text="Not implemented yet" /> | – |
|
| ISNA | <Badge type="tip" text="Available" /> | – |
|
||||||
| ISNONTEXT | <Badge type="tip" text="Available" /> | – |
|
| ISNONTEXT | <Badge type="tip" text="Available" /> | – |
|
||||||
| ISNUMBER | <Badge type="tip" text="Available" /> | – |
|
| ISNUMBER | <Badge type="tip" text="Available" /> | – |
|
||||||
| ISODD | <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" /> | – |
|
| N | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| NA | <Badge type="tip" text="Available" /> | – |
|
| NA | <Badge type="tip" text="Available" /> | – |
|
||||||
| SHEET | <Badge type="tip" text="Available" /> | – |
|
| SHEET | <Badge type="tip" text="Available" /> | – |
|
||||||
| SHEETS | <Badge type="info" text="Not implemented yet" /> | – |
|
| SHEETS | <Badge type="tip" text="Available" /> | – |
|
||||||
| TYPE | <Badge type="tip" text="Available" /> | – |
|
| TYPE | <Badge type="tip" text="Available" /> | – |
|
||||||
|
|||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# CELL
|
# CELL
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 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).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# N
|
# N
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 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).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# SHEETS
|
# SHEETS
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 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).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -6,26 +6,33 @@ lang: en-US
|
|||||||
|
|
||||||
# COLUMN function
|
# COLUMN function
|
||||||
## Overview
|
## 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
|
## Usage
|
||||||
### Syntax
|
### Syntax
|
||||||
**COLUMN(<span title="Reference" style="color:#1E88E5">reference</span>) => <span title="Number" style="color:#1E88E5">column</span>**
|
**COLUMN(<span title="Reference" style="color:#1E88E5">reference</span>) => <span title="Number" style="color:#1E88E5">column</span>**
|
||||||
### Argument descriptions
|
### Argument descriptions
|
||||||
* *reference* ([cell](/features/value-types#references), [optional](/features/optional-arguments.md)). The number of the cell you wish to reference the column number of.
|
* *reference* ([cell](/features/value-types#references), [optional](/features/optional-arguments.md)). The cell, column, range, or [Named Range](/web-application/name-manager.html) for which you wish to find the column number.
|
||||||
### Additional guidance
|
### 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 leftmost cell will be returned.
|
||||||
* You are also able to reference complete columns instead of individual cells.
|
* 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
|
### Returned value
|
||||||
COLUMN returns the [number](/features/value-types#numbers) of the specific cell or column which is being referenced.
|
COLUMN returns the [number](/features/value-types#numbers) of the specific cell or column which is being referenced. If no reference is included, the column number of the cell where the formula is entered will be returned.
|
||||||
### Error conditions
|
### Error conditions
|
||||||
* IronCalc currently does not support the referencing of cells with names.
|
* A [#NAME?](/features/error-types.html#name) error is returned if a Named Range being referenced is deleted.
|
||||||
|
* A [#REF!](/features/error-types.html#ref) error is returned if a cell being referenced is deleted.
|
||||||
|
* A [#VALUE!](/features/error-types.html#value) error is returned if a column being referenced is deleted.
|
||||||
## Details
|
## Details
|
||||||
The COLUMN Function can only be used to display the correlating number of a single column within a Sheet. If you wish to show the number of columns used within a specific range, you can use the COLUMNS Function.
|
The COLUMN Function can only be used to display the correlating number of a single column within a Sheet. If you wish to show the number of columns used within a specific range, you can use the [COLUMNS](/functions/lookup_and_reference/columns) Function.
|
||||||
## Examples
|
## Examples
|
||||||
### No Cell Reference
|
### No Cell Reference
|
||||||
When no cell reference is made, the formula uses **=COLUMN()**. This will then output the column number of the cell where the formula is placed.<br><br>For example, if the formula is placed in cell A1, then "1" will be displayed.
|
When no cell reference is made, the formula uses **=COLUMN()**. This will output the column number of the cell where the formula is entered.<br><br>For example, if the formula is placed in cell A1, then "1" will be displayed.
|
||||||
### With Cell Reference
|
### With Cell Reference
|
||||||
When a cell reference is made, the formula uses **=COLUMN([Referenced Cell])**. This will then output the column number of the referenced cell, regardless of where the formula is placed in the sheet.<br><br>For example, if the cell B1 is the referenced cell, "2" will be the output of the formula no matter where it is placed in the sheet.<br><br>**Note:** references do not always have to be specific cells, you can also reference complete columns. For example, **=COLUMN(B:B)** would also result in an output of "2".
|
When a cell reference is made, the formula uses **=COLUMN(<span title="Reference" style="color:#1E88E5">Referenced Cell</span>)**. This will then output the column number of the referenced cell, regardless of where the formula is placed in the sheet.<br><br>For example, if B1 is the referenced cell, then "2" will be the output of the formula, regardless of where the formula is placed in the sheet.<br><br>**Note:** references do not have to be specific cells, you can also reference complete columns. For example, **=COLUMN(B:B)** would also result in an output of "2".
|
||||||
### Range References
|
### Range References
|
||||||
The COLUMN function can also be used to reference a range of Cells or Columns. In this case only the most left-hand column will be the resulting output.<br><br>For example, **=COLUMN(A1:J1)** will result in the ouput of "1".
|
The COLUMN function can also be used to reference a range of cells or columns. In this case only the leftmost column will be the resulting output.<br><br>For example, **=COLUMN(A1:J1)** will result in the output of "1".
|
||||||
## Links
|
## Links
|
||||||
|
* Visit Microsoft Excel's [Column function](https://support.microsoft.com/en-us/office/column-function-44e8c754-711c-4df3-9da4-47a55042554b) page.
|
||||||
|
* Both [Google Sheets](https://support.google.com/docs/answer/3093373) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/COLUMN) provide versions of the COLUMN function.
|
||||||
@@ -86,7 +86,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
|
|||||||
| SUMIF | <Badge type="tip" text="Available" /> | – |
|
| SUMIF | <Badge type="tip" text="Available" /> | – |
|
||||||
| SUMIFS | <Badge type="tip" text="Available" /> | – |
|
| SUMIFS | <Badge type="tip" text="Available" /> | – |
|
||||||
| SUMPRODUCT | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMPRODUCT | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SUMSQ | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMSQ | <Badge type="tip" text="Available" /> | – |
|
||||||
| SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SUMX2PY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMX2PY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
| SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
| SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | – |
|
||||||
|
|||||||
@@ -7,6 +7,5 @@ lang: en-US
|
|||||||
# SUMSQ
|
# SUMSQ
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
🚧 This function is not yet available in IronCalc.
|
🚧 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).
|
||||||
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
|
|
||||||
:::
|
:::
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { Model } from "@ironcalc/wasm";
|
import type { Model } from "@ironcalc/wasm";
|
||||||
import { styled } from "@mui/material";
|
import { styled } from "@mui/material";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { Fx } from "../../icons";
|
import { Fx } from "../../icons";
|
||||||
import { theme } from "../../theme";
|
import { theme } from "../../theme";
|
||||||
import { FORMULA_BAR_HEIGHT } from "../constants";
|
import { FORMULA_BAR_HEIGHT } from "../constants";
|
||||||
@@ -9,6 +11,7 @@ import {
|
|||||||
ROW_HEIGH_SCALE,
|
ROW_HEIGH_SCALE,
|
||||||
} from "../WorksheetCanvas/constants";
|
} from "../WorksheetCanvas/constants";
|
||||||
import type { WorkbookState } from "../workbookState";
|
import type { WorkbookState } from "../workbookState";
|
||||||
|
import FormulaBarMenu from "./FormulaBarMenu";
|
||||||
|
|
||||||
type FormulaBarProps = {
|
type FormulaBarProps = {
|
||||||
cellAddress: string;
|
cellAddress: string;
|
||||||
@@ -17,6 +20,8 @@ type FormulaBarProps = {
|
|||||||
workbookState: WorkbookState;
|
workbookState: WorkbookState;
|
||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
onTextUpdated: () => void;
|
onTextUpdated: () => void;
|
||||||
|
openDrawer: () => void;
|
||||||
|
canEdit: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function FormulaBar(properties: FormulaBarProps) {
|
function FormulaBar(properties: FormulaBarProps) {
|
||||||
@@ -28,10 +33,27 @@ function FormulaBar(properties: FormulaBarProps) {
|
|||||||
onTextUpdated,
|
onTextUpdated,
|
||||||
workbookState,
|
workbookState,
|
||||||
} = properties;
|
} = properties;
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleMenuOpenChange = (isOpen: boolean): void => {
|
||||||
|
setIsMenuOpen(isOpen);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<AddressContainer>
|
<AddressContainer $active={isMenuOpen}>
|
||||||
|
<FormulaBarMenu
|
||||||
|
onMenuOpenChange={handleMenuOpenChange}
|
||||||
|
openDrawer={properties.openDrawer}
|
||||||
|
canEdit={properties.canEdit}
|
||||||
|
model={model}
|
||||||
|
onUpdate={onChange}
|
||||||
|
>
|
||||||
<CellBarAddress>{cellAddress}</CellBarAddress>
|
<CellBarAddress>{cellAddress}</CellBarAddress>
|
||||||
|
<StyledIcon>
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</StyledIcon>
|
||||||
|
</FormulaBarMenu>
|
||||||
</AddressContainer>
|
</AddressContainer>
|
||||||
<Divider />
|
<Divider />
|
||||||
<FormulaContainer>
|
<FormulaContainer>
|
||||||
@@ -101,7 +123,7 @@ const Divider = styled("div")`
|
|||||||
background-color: ${theme.palette.grey["300"]};
|
background-color: ${theme.palette.grey["300"]};
|
||||||
min-width: 1px;
|
min-width: 1px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
margin: 0px 16px;
|
margin: 0px 16px 0px 8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FormulaContainer = styled("div")`
|
const FormulaContainer = styled("div")`
|
||||||
@@ -121,22 +143,46 @@ const Container = styled("div")`
|
|||||||
background: ${(properties): string =>
|
background: ${(properties): string =>
|
||||||
properties.theme.palette.background.default};
|
properties.theme.palette.background.default};
|
||||||
height: ${FORMULA_BAR_HEIGHT}px;
|
height: ${FORMULA_BAR_HEIGHT}px;
|
||||||
|
border-top: 1px solid ${theme.palette.grey["300"]};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const AddressContainer = styled("div")`
|
const AddressContainer = styled("div")<{ $active?: boolean }>`
|
||||||
padding-left: 16px;
|
|
||||||
color: ${theme.palette.common.black};
|
color: ${theme.palette.common.black};
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: normal;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
flex-grow: row;
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: ${(props) =>
|
||||||
|
props.$active ? theme.palette.action.selected : "transparent"};
|
||||||
|
&:hover {
|
||||||
|
background-color: ${(props) =>
|
||||||
|
props.$active ? theme.palette.action.selected : theme.palette.grey["100"]};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CellBarAddress = styled("div")`
|
const CellBarAddress = styled("div")`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: "center";
|
box-sizing: border-box;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding-left: 8px;
|
||||||
|
background-color: transparent;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIcon = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px 2px;
|
||||||
|
background-color: transparent;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EditorWrapper = styled("div")`
|
const EditorWrapper = styled("div")`
|
||||||
|
|||||||
170
webapp/IronCalc/src/components/FormulaBar/FormulaBarMenu.tsx
Normal file
170
webapp/IronCalc/src/components/FormulaBar/FormulaBarMenu.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import type { Model } from "@ironcalc/wasm";
|
||||||
|
import { Menu, MenuItem, styled } from "@mui/material";
|
||||||
|
import { Tag } from "lucide-react";
|
||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { theme } from "../../theme";
|
||||||
|
import { parseRangeInSheet } from "../Editor/util";
|
||||||
|
|
||||||
|
type FormulaBarMenuProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onMenuOpenChange: (isOpen: boolean) => void;
|
||||||
|
openDrawer: () => void;
|
||||||
|
canEdit: boolean;
|
||||||
|
model: Model;
|
||||||
|
onUpdate: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormulaBarMenu = (properties: FormulaBarMenuProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isMenuOpen, setMenuOpen] = useState(false);
|
||||||
|
const anchorElement = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleMenuOpen = useCallback((): void => {
|
||||||
|
setMenuOpen(true);
|
||||||
|
properties.onMenuOpenChange(true);
|
||||||
|
}, [properties.onMenuOpenChange]);
|
||||||
|
|
||||||
|
const handleMenuClose = useCallback((): void => {
|
||||||
|
setMenuOpen(false);
|
||||||
|
properties.onMenuOpenChange(false);
|
||||||
|
}, [properties.onMenuOpenChange]);
|
||||||
|
|
||||||
|
const definedNameList = properties.model.getDefinedNameList();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ChildrenWrapper onClick={handleMenuOpen} ref={anchorElement}>
|
||||||
|
{properties.children}
|
||||||
|
</ChildrenWrapper>
|
||||||
|
<StyledMenu
|
||||||
|
open={isMenuOpen}
|
||||||
|
onClose={handleMenuClose}
|
||||||
|
anchorEl={anchorElement.current}
|
||||||
|
marginThreshold={0}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "left",
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "left",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{definedNameList.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{definedNameList.map((definedName) => {
|
||||||
|
return (
|
||||||
|
<MenuItemWrapper
|
||||||
|
key={`${definedName.name}-${definedName.scope}`}
|
||||||
|
disableRipple
|
||||||
|
onClick={() => {
|
||||||
|
// select the area corresponding to the defined name
|
||||||
|
const formula = definedName.formula;
|
||||||
|
const range = parseRangeInSheet(properties.model, formula);
|
||||||
|
if (range) {
|
||||||
|
const [
|
||||||
|
sheetIndex,
|
||||||
|
rowStart,
|
||||||
|
columnStart,
|
||||||
|
rowEnd,
|
||||||
|
columnEnd,
|
||||||
|
] = range;
|
||||||
|
properties.model.setSelectedSheet(sheetIndex);
|
||||||
|
properties.model.setSelectedCell(rowStart, columnStart);
|
||||||
|
properties.model.setSelectedRange(
|
||||||
|
rowStart,
|
||||||
|
columnStart,
|
||||||
|
rowEnd,
|
||||||
|
columnEnd,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
properties.onUpdate();
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tag />
|
||||||
|
<MenuItemText>{definedName.name}</MenuItemText>
|
||||||
|
<MenuItemExample>{definedName.formula}</MenuItemExample>
|
||||||
|
</MenuItemWrapper>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<MenuDivider />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<MenuItemWrapper
|
||||||
|
onClick={() => {
|
||||||
|
properties.openDrawer();
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
disabled={!properties.canEdit}
|
||||||
|
disableRipple
|
||||||
|
>
|
||||||
|
<MenuItemText>{t("formula_bar.manage_named_ranges")}</MenuItemText>
|
||||||
|
</MenuItemWrapper>
|
||||||
|
</StyledMenu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledMenu = styled(Menu)`
|
||||||
|
top: 4px;
|
||||||
|
min-width: 260px;
|
||||||
|
max-width: 460px;
|
||||||
|
& .MuiPaper-root {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 0px;
|
||||||
|
margin-left: -4px;
|
||||||
|
}
|
||||||
|
& .MuiList-root {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuItemWrapper = styled(MenuItem)`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
gap: 8px;
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
min-width: 172px;
|
||||||
|
margin: 0px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
height: 32px;
|
||||||
|
& svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: ${theme.palette.grey[600]};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ChildrenWrapper = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuDivider = styled("div")`
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-top: 1px solid ${theme.palette.grey[200]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuItemText = styled("div")`
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: ${theme.palette.common.black};
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuItemExample = styled("div")`
|
||||||
|
color: ${theme.palette.grey[400]};
|
||||||
|
margin-left: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default FormulaBarMenu;
|
||||||
@@ -398,7 +398,9 @@ const ListItem = styled("div")<{ $isSelected: boolean }>(({ $isSelected }) => ({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
|
gap: "8px",
|
||||||
padding: "8px 12px",
|
padding: "8px 12px",
|
||||||
|
cursor: "pointer",
|
||||||
minHeight: "40px",
|
minHeight: "40px",
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
borderBottom: `1px solid ${theme.palette.grey[200]}`,
|
borderBottom: `1px solid ${theme.palette.grey[200]}`,
|
||||||
@@ -438,6 +440,8 @@ const NameText = styled("span")({
|
|||||||
fontSize: "12px",
|
fontSize: "12px",
|
||||||
color: theme.palette.common.black,
|
color: theme.palette.common.black,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
|
wordBreak: "break-all",
|
||||||
|
overflowWrap: "break-word",
|
||||||
});
|
});
|
||||||
|
|
||||||
const IconsWrapper = styled("div")({
|
const IconsWrapper = styled("div")({
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import {
|
|||||||
Redo2,
|
Redo2,
|
||||||
RemoveFormatting,
|
RemoveFormatting,
|
||||||
Strikethrough,
|
Strikethrough,
|
||||||
Tags,
|
|
||||||
Type,
|
Type,
|
||||||
Underline,
|
Underline,
|
||||||
Undo2,
|
Undo2,
|
||||||
@@ -87,7 +86,6 @@ type ToolbarProperties = {
|
|||||||
numFmt: string;
|
numFmt: string;
|
||||||
showGridLines: boolean;
|
showGridLines: boolean;
|
||||||
onToggleShowGridLines: (show: boolean) => void;
|
onToggleShowGridLines: (show: boolean) => void;
|
||||||
openDrawer: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function Toolbar(properties: ToolbarProperties) {
|
function Toolbar(properties: ToolbarProperties) {
|
||||||
@@ -514,18 +512,6 @@ function Toolbar(properties: ToolbarProperties) {
|
|||||||
{properties.showGridLines ? <Grid2x2Check /> : <Grid2x2X />}
|
{properties.showGridLines ? <Grid2x2Check /> : <Grid2x2X />}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("toolbar.named_ranges")}>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
onClick={() => {
|
|
||||||
properties.openDrawer();
|
|
||||||
}}
|
|
||||||
disabled={!canEdit}
|
|
||||||
>
|
|
||||||
<Tags />
|
|
||||||
</StyledButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={t("toolbar.selected_png")}>
|
<Tooltip title={t("toolbar.selected_png")}>
|
||||||
<StyledButton
|
<StyledButton
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -665,9 +665,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
model.setShowGridLines(sheet, show);
|
model.setShowGridLines(sheet, show);
|
||||||
setRedrawId((id) => id + 1);
|
setRedrawId((id) => id + 1);
|
||||||
}}
|
}}
|
||||||
openDrawer={() => {
|
|
||||||
setDrawerOpen(true);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<WorksheetAreaLeft $drawerWidth={isDrawerOpen ? drawerWidth : 0}>
|
<WorksheetAreaLeft $drawerWidth={isDrawerOpen ? drawerWidth : 0}>
|
||||||
<FormulaBar
|
<FormulaBar
|
||||||
@@ -682,6 +679,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
}}
|
}}
|
||||||
model={model}
|
model={model}
|
||||||
workbookState={workbookState}
|
workbookState={workbookState}
|
||||||
|
openDrawer={() => {
|
||||||
|
setDrawerOpen(true);
|
||||||
|
}}
|
||||||
|
canEdit={true}
|
||||||
/>
|
/>
|
||||||
<Worksheet
|
<Worksheet
|
||||||
model={model}
|
model={model}
|
||||||
@@ -762,9 +763,9 @@ type WorksheetAreaLeftProps = { $drawerWidth: number };
|
|||||||
const WorksheetAreaLeft = styled("div")<WorksheetAreaLeftProps>(
|
const WorksheetAreaLeft = styled("div")<WorksheetAreaLeftProps>(
|
||||||
({ $drawerWidth }) => ({
|
({ $drawerWidth }) => ({
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: `${TOOLBAR_HEIGHT + 1}px`,
|
top: `${TOOLBAR_HEIGHT}px`,
|
||||||
width: `calc(100% - ${$drawerWidth}px)`,
|
width: `calc(100% - ${$drawerWidth}px)`,
|
||||||
height: `calc(100% - ${TOOLBAR_HEIGHT + 1}px)`,
|
height: `calc(100% - ${TOOLBAR_HEIGHT}px)`,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { CellStyle, Model } from "@ironcalc/wasm";
|
import type { CellStyle, Model } from "@ironcalc/wasm";
|
||||||
import { columnNameFromNumber } from "@ironcalc/wasm";
|
import { columnNameFromNumber } from "@ironcalc/wasm";
|
||||||
|
import { theme } from "../../theme";
|
||||||
import { getColor } from "../Editor/util";
|
import { getColor } from "../Editor/util";
|
||||||
import type { Cell } from "../types";
|
import type { Cell } from "../types";
|
||||||
import type { WorkbookState } from "../workbookState";
|
import type { WorkbookState } from "../workbookState";
|
||||||
@@ -721,7 +722,7 @@ export default class WorksheetCanvas {
|
|||||||
const style = this.model.getCellStyle(selectedSheet, row, column);
|
const style = this.model.getCellStyle(selectedSheet, row, column);
|
||||||
|
|
||||||
// first the background
|
// first the background
|
||||||
let backgroundColor = "#FFFFFF";
|
let backgroundColor = theme.palette.common.white;
|
||||||
if (style.fill.fg_color) {
|
if (style.fill.fg_color) {
|
||||||
backgroundColor = style.fill.fg_color;
|
backgroundColor = style.fill.fg_color;
|
||||||
}
|
}
|
||||||
@@ -1036,14 +1037,21 @@ export default class WorksheetCanvas {
|
|||||||
width: number,
|
width: number,
|
||||||
div: HTMLDivElement,
|
div: HTMLDivElement,
|
||||||
selected: boolean,
|
selected: boolean,
|
||||||
|
isFullColumnSelected: boolean,
|
||||||
): void {
|
): void {
|
||||||
div.style.boxSizing = "border-box";
|
div.style.boxSizing = "border-box";
|
||||||
div.style.width = `${width}px`;
|
div.style.width = `${width}px`;
|
||||||
div.style.height = `${headerRowHeight}px`;
|
div.style.height = `${headerRowHeight}px`;
|
||||||
div.style.backgroundColor = selected
|
div.style.backgroundColor = selected
|
||||||
? headerSelectedBackground
|
? isFullColumnSelected
|
||||||
|
? theme.palette.primary.main
|
||||||
|
: headerSelectedBackground
|
||||||
: headerBackground;
|
: headerBackground;
|
||||||
div.style.color = selected ? headerSelectedColor : headerTextColor;
|
div.style.color = selected
|
||||||
|
? isFullColumnSelected
|
||||||
|
? theme.palette.common.white
|
||||||
|
: headerSelectedColor
|
||||||
|
: headerTextColor;
|
||||||
div.style.fontWeight = "bold";
|
div.style.fontWeight = "bold";
|
||||||
div.style.borderLeft = `1px solid ${headerBorderColor}`;
|
div.style.borderLeft = `1px solid ${headerBorderColor}`;
|
||||||
div.style.borderTop = `1px solid ${headerBorderColor}`;
|
div.style.borderTop = `1px solid ${headerBorderColor}`;
|
||||||
@@ -1071,9 +1079,15 @@ export default class WorksheetCanvas {
|
|||||||
const { sheet: selectedSheet, range } = this.model.getSelectedView();
|
const { sheet: selectedSheet, range } = this.model.getSelectedView();
|
||||||
let rowStart = range[0];
|
let rowStart = range[0];
|
||||||
let rowEnd = range[2];
|
let rowEnd = range[2];
|
||||||
|
let columnStart = range[1];
|
||||||
|
let columnEnd = range[3];
|
||||||
if (rowStart > rowEnd) {
|
if (rowStart > rowEnd) {
|
||||||
[rowStart, rowEnd] = [rowEnd, rowStart];
|
[rowStart, rowEnd] = [rowEnd, rowStart];
|
||||||
}
|
}
|
||||||
|
if (columnStart > columnEnd) {
|
||||||
|
[columnStart, columnEnd] = [columnEnd, columnStart];
|
||||||
|
}
|
||||||
|
const isFullRowSelected = columnStart === 1 && columnEnd === LAST_COLUMN;
|
||||||
const context = this.ctx;
|
const context = this.ctx;
|
||||||
|
|
||||||
let topLeftCornerY = headerRowHeight + 0.5;
|
let topLeftCornerY = headerRowHeight + 0.5;
|
||||||
@@ -1085,7 +1099,9 @@ export default class WorksheetCanvas {
|
|||||||
context.fillStyle = headerBorderColor;
|
context.fillStyle = headerBorderColor;
|
||||||
context.fillRect(0.5, topLeftCornerY, headerColumnWidth, rowHeight);
|
context.fillRect(0.5, topLeftCornerY, headerColumnWidth, rowHeight);
|
||||||
context.fillStyle = selected
|
context.fillStyle = selected
|
||||||
? headerSelectedBackground
|
? isFullRowSelected
|
||||||
|
? theme.palette.primary.main
|
||||||
|
: headerSelectedBackground
|
||||||
: headerBackground;
|
: headerBackground;
|
||||||
context.fillRect(
|
context.fillRect(
|
||||||
0.5,
|
0.5,
|
||||||
@@ -1097,7 +1113,11 @@ export default class WorksheetCanvas {
|
|||||||
context.fillStyle = outlineColor;
|
context.fillStyle = outlineColor;
|
||||||
context.fillRect(headerColumnWidth - 1, topLeftCornerY, 1, rowHeight);
|
context.fillRect(headerColumnWidth - 1, topLeftCornerY, 1, rowHeight);
|
||||||
}
|
}
|
||||||
context.fillStyle = selected ? headerSelectedColor : headerTextColor;
|
context.fillStyle = selected
|
||||||
|
? isFullRowSelected
|
||||||
|
? theme.palette.common.white
|
||||||
|
: headerSelectedColor
|
||||||
|
: headerTextColor;
|
||||||
context.font = `bold 12px ${defaultCellFontFamily}`;
|
context.font = `bold 12px ${defaultCellFontFamily}`;
|
||||||
context.fillText(
|
context.fillText(
|
||||||
`${row}`,
|
`${row}`,
|
||||||
@@ -1122,11 +1142,17 @@ export default class WorksheetCanvas {
|
|||||||
const { columnHeaders } = this;
|
const { columnHeaders } = this;
|
||||||
let deltaX = 0;
|
let deltaX = 0;
|
||||||
const { range } = this.model.getSelectedView();
|
const { range } = this.model.getSelectedView();
|
||||||
|
let rowStart = range[0];
|
||||||
|
let rowEnd = range[2];
|
||||||
let columnStart = range[1];
|
let columnStart = range[1];
|
||||||
let columnEnd = range[3];
|
let columnEnd = range[3];
|
||||||
if (columnStart > columnEnd) {
|
if (columnStart > columnEnd) {
|
||||||
[columnStart, columnEnd] = [columnEnd, columnStart];
|
[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"))
|
for (const header of columnHeaders.querySelectorAll(".column-header"))
|
||||||
header.remove();
|
header.remove();
|
||||||
for (const handle of columnHeaders.querySelectorAll(
|
for (const handle of columnHeaders.querySelectorAll(
|
||||||
@@ -1146,7 +1172,12 @@ export default class WorksheetCanvas {
|
|||||||
// Frozen headers
|
// Frozen headers
|
||||||
for (let column = 1; column <= frozenColumns; column += 1) {
|
for (let column = 1; column <= frozenColumns; column += 1) {
|
||||||
const selected = column >= columnStart && column <= columnEnd;
|
const selected = column >= columnStart && column <= columnEnd;
|
||||||
deltaX += this.addColumnHeader(deltaX, column, selected);
|
deltaX += this.addColumnHeader(
|
||||||
|
deltaX,
|
||||||
|
column,
|
||||||
|
selected,
|
||||||
|
isFullColumnSelected,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frozenColumns !== 0) {
|
if (frozenColumns !== 0) {
|
||||||
@@ -1162,7 +1193,12 @@ export default class WorksheetCanvas {
|
|||||||
|
|
||||||
for (let column = firstColumn; column <= lastColumn; column += 1) {
|
for (let column = firstColumn; column <= lastColumn; column += 1) {
|
||||||
const selected = column >= columnStart && column <= columnEnd;
|
const selected = column >= columnStart && column <= columnEnd;
|
||||||
deltaX += this.addColumnHeader(deltaX, column, selected);
|
deltaX += this.addColumnHeader(
|
||||||
|
deltaX,
|
||||||
|
column,
|
||||||
|
selected,
|
||||||
|
isFullColumnSelected,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
columnHeaders.style.width = `${deltaX}px`;
|
columnHeaders.style.width = `${deltaX}px`;
|
||||||
@@ -1172,6 +1208,7 @@ export default class WorksheetCanvas {
|
|||||||
deltaX: number,
|
deltaX: number,
|
||||||
column: number,
|
column: number,
|
||||||
selected: boolean,
|
selected: boolean,
|
||||||
|
isFullColumnSelected: boolean,
|
||||||
): number {
|
): number {
|
||||||
const columnWidth = this.getColumnWidth(
|
const columnWidth = this.getColumnWidth(
|
||||||
this.model.getSelectedSheet(),
|
this.model.getSelectedSheet(),
|
||||||
@@ -1182,7 +1219,7 @@ export default class WorksheetCanvas {
|
|||||||
div.textContent = columnNameFromNumber(column);
|
div.textContent = columnNameFromNumber(column);
|
||||||
this.columnHeaders.insertBefore(div, null);
|
this.columnHeaders.insertBefore(div, null);
|
||||||
|
|
||||||
this.styleColumnHeader(columnWidth, div, selected);
|
this.styleColumnHeader(columnWidth, div, selected, isFullColumnSelected);
|
||||||
this.addColumnResizeHandle(deltaX + columnWidth, column, columnWidth);
|
this.addColumnResizeHandle(deltaX + columnWidth, column, columnWidth);
|
||||||
return columnWidth;
|
return columnWidth;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,9 @@
|
|||||||
"label": "Formula",
|
"label": "Formula",
|
||||||
"title": "Update formula"
|
"title": "Update formula"
|
||||||
},
|
},
|
||||||
|
"formula_bar": {
|
||||||
|
"manage_named_ranges": "Manage Named Ranges"
|
||||||
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"add_sheet": "Add sheet",
|
"add_sheet": "Add sheet",
|
||||||
"sheet_list": "Sheet list",
|
"sheet_list": "Sheet list",
|
||||||
|
|||||||
25
webapp/app.ironcalc.com/frontend/package-lock.json
generated
25
webapp/app.ironcalc.com/frontend/package-lock.json
generated
@@ -91,6 +91,7 @@
|
|||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -570,6 +571,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||||
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
@@ -613,6 +615,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
|
||||||
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
|
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
@@ -2026,6 +2029,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.4.tgz",
|
||||||
"integrity": "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==",
|
"integrity": "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -2122,6 +2126,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.8.25",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001754",
|
||||||
@@ -2691,6 +2696,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -2758,6 +2764,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -2767,6 +2774,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -2978,6 +2986,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -3023,6 +3032,7 @@
|
|||||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -3113,21 +3123,6 @@
|
|||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
|
||||||
"node_modules/yaml": {
|
|
||||||
"version": "2.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
|
|
||||||
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"yaml": "bin.mjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.6"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,16 +227,19 @@ const Wrapper = styled("div")`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const DRAWER_WIDTH = 264;
|
const DRAWER_WIDTH = 264;
|
||||||
const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 440;
|
export const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 768;
|
||||||
|
|
||||||
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
|
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
|
||||||
margin-left: ${({ isDrawerOpen }) => (isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`)};
|
margin-left: ${({ isDrawerOpen }) =>
|
||||||
width: ${({ isDrawerOpen }) => (isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : "100%")};
|
isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`};
|
||||||
|
width: ${({ isDrawerOpen }) =>
|
||||||
|
isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : "100%"};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
@media (max-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px) {
|
@media (max-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px) {
|
||||||
${({ isDrawerOpen }) => isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`}
|
${({ isDrawerOpen }) =>
|
||||||
|
isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -247,7 +250,7 @@ const MobileOverlay = styled("div")`
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
z-index: 1;
|
z-index: 100;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
@media (min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE + 1}px) {
|
@media (min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE + 1}px) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Model } from "@ironcalc/workbook";
|
|||||||
import { IconButton, Tooltip } from "@mui/material";
|
import { IconButton, Tooltip } from "@mui/material";
|
||||||
import { CloudOff, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
import { CloudOff, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
||||||
import { useLayoutEffect, useRef, useState } from "react";
|
import { useLayoutEffect, useRef, useState } from "react";
|
||||||
|
import { MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE } from "../App";
|
||||||
import { FileMenu } from "./FileMenu";
|
import { FileMenu } from "./FileMenu";
|
||||||
import { HelpMenu } from "./HelpMenu";
|
import { HelpMenu } from "./HelpMenu";
|
||||||
import { downloadModel } from "./rpc";
|
import { downloadModel } from "./rpc";
|
||||||
@@ -77,7 +78,7 @@ export function FileBar(properties: {
|
|||||||
{properties.isDrawerOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
|
{properties.isDrawerOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
|
||||||
</DrawerButton>
|
</DrawerButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{width > 440 && (
|
{width > MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE && (
|
||||||
<FileMenu
|
<FileMenu
|
||||||
newModel={properties.newModel}
|
newModel={properties.newModel}
|
||||||
newModelFromTemplate={properties.newModelFromTemplate}
|
newModelFromTemplate={properties.newModelFromTemplate}
|
||||||
@@ -92,7 +93,7 @@ export function FileBar(properties: {
|
|||||||
onDelete={properties.onDelete}
|
onDelete={properties.onDelete}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{width > 440 && <HelpMenu />}
|
{width > MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE && <HelpMenu />}
|
||||||
<WorkbookTitleWrapper>
|
<WorkbookTitleWrapper>
|
||||||
<WorkbookTitle
|
<WorkbookTitle
|
||||||
name={properties.model.getName()}
|
name={properties.model.getName()}
|
||||||
@@ -112,7 +113,8 @@ export function FileBar(properties: {
|
|||||||
<div style={{ fontWeight: "bold" }}>{cloudWarningText2}</div>
|
<div style={{ fontWeight: "bold" }}>{cloudWarningText2}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
placement="bottom-start"
|
placement="bottom"
|
||||||
|
enterTouchDelay={0}
|
||||||
enterDelay={500}
|
enterDelay={500}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
popper: {
|
popper: {
|
||||||
|
|||||||
BIN
xlsx/tests/calc_tests/CELL.xlsx
Normal file
BIN
xlsx/tests/calc_tests/CELL.xlsx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/calc_tests/MINA_MAXA.xlsx
Normal file
BIN
xlsx/tests/calc_tests/MINA_MAXA.xlsx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/calc_tests/N.xlsx
Normal file
BIN
xlsx/tests/calc_tests/N.xlsx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/calc_tests/RANK_EQ_RANK_AVG.xlsx
Normal file
BIN
xlsx/tests/calc_tests/RANK_EQ_RANK_AVG.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/SHEET_SHEETS.xlsx
Normal file
BIN
xlsx/tests/calc_tests/SHEET_SHEETS.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/SMALL_LARGE.xlsx
Normal file
BIN
xlsx/tests/calc_tests/SMALL_LARGE.xlsx
Normal file
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/calc_tests/SUMX2MY2_SUMX2PY2_SUMXMY2.xlsx
Normal file
BIN
xlsx/tests/calc_tests/SUMX2MY2_SUMX2PY2_SUMXMY2.xlsx
Normal file
Binary file not shown.
Binary file not shown.
BIN
xlsx/tests/statistical/CORREL_SLOPE_INTERCEPT_RSQ_STEYX.xlsx
Normal file
BIN
xlsx/tests/statistical/CORREL_SLOPE_INTERCEPT_RSQ_STEYX.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/statistical/F_TEST.xlsx
Normal file
BIN
xlsx/tests/statistical/F_TEST.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/statistical/MEADIAN_KURT_SKEW_HARMEAN.xlsx
Normal file
BIN
xlsx/tests/statistical/MEADIAN_KURT_SKEW_HARMEAN.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user