Compare commits

..

51 Commits

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

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

View File

@@ -12,6 +12,9 @@ pub(crate) const DEFAULT_WINDOW_WIDTH: i64 = 800;
pub(crate) const LAST_COLUMN: i32 = 16_384;
pub(crate) const LAST_ROW: i32 = 1_048_576;
// Excel uses 15 significant digits of precision for all numeric calculations.
pub(crate) const EXCEL_PRECISION: usize = 15;
// 693_594 is computed as:
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
// The 2 days offset is because of Excel 1900 bug

View File

@@ -471,6 +471,20 @@ impl Parser {
Node::NumberKind(s) => ArrayNode::Number(s),
Node::StringKind(s) => ArrayNode::String(s),
Node::ErrorKind(kind) => ArrayNode::Error(kind),
Node::UnaryKind {
kind: OpUnary::Minus,
right,
} => {
if let Node::NumberKind(n) = *right {
ArrayNode::Number(-n)
} else {
return Err(Node::ParseErrorKind {
formula: self.lexer.get_formula(),
message: "Invalid value in array".to_string(),
position: self.lexer.get_position() as usize,
});
}
}
error @ Node::ParseErrorKind { .. } => return Err(error),
_ => {
return Err(Node::ParseErrorKind {
@@ -490,6 +504,20 @@ impl Parser {
Node::NumberKind(s) => ArrayNode::Number(s),
Node::StringKind(s) => ArrayNode::String(s),
Node::ErrorKind(kind) => ArrayNode::Error(kind),
Node::UnaryKind {
kind: OpUnary::Minus,
right,
} => {
if let Node::NumberKind(n) = *right {
ArrayNode::Number(-n)
} else {
return Err(Node::ParseErrorKind {
formula: self.lexer.get_formula(),
message: "Invalid value in array".to_string(),
position: self.lexer.get_position() as usize,
});
}
}
error @ Node::ParseErrorKind { .. } => return Err(error),
_ => {
return Err(Node::ParseErrorKind {

View File

@@ -872,12 +872,10 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Combin => args_signature_scalars(arg_count, 2, 0),
Function::Combina => args_signature_scalars(arg_count, 2, 0),
Function::Sumsq => vec![Signature::Vector; arg_count],
Function::N => args_signature_scalars(arg_count, 1, 0),
Function::Sheets => args_signature_scalars(arg_count, 0, 1),
Function::Cell => args_signature_scalars(arg_count, 1, 1),
Function::Info => args_signature_scalars(arg_count, 1, 1),
Function::Daverage => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dcount => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dget => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
@@ -890,7 +888,6 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Dvar => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dvarp => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dstdevp => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::BetaDist => args_signature_scalars(arg_count, 4, 2),
Function::BetaInv => args_signature_scalars(arg_count, 3, 2),
Function::BinomDist => args_signature_scalars(arg_count, 4, 0),
@@ -929,6 +926,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::FDistRT => args_signature_scalars(arg_count, 3, 0),
Function::FInv => args_signature_scalars(arg_count, 3, 0),
Function::FInvRT => args_signature_scalars(arg_count, 3, 0),
Function::FTest => vec![Signature::Vector; 2],
Function::Fisher => args_signature_scalars(arg_count, 1, 0),
Function::FisherInv => args_signature_scalars(arg_count, 1, 0),
Function::Gamma => args_signature_scalars(arg_count, 1, 0),
@@ -989,6 +987,26 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
vec![Signature::Error; arg_count]
}
}
Function::Sumx2my2 => vec![Signature::Vector; 2],
Function::Sumx2py2 => vec![Signature::Vector; 2],
Function::Sumxmy2 => vec![Signature::Vector; 2],
Function::Correl => vec![Signature::Vector; 2],
Function::Rsq => vec![Signature::Vector; 2],
Function::Intercept => vec![Signature::Vector; 2],
Function::Slope => vec![Signature::Vector; 2],
Function::Steyx => vec![Signature::Vector; 2],
Function::Gauss => args_signature_scalars(arg_count, 1, 0),
Function::Harmean => vec![Signature::Vector; arg_count],
Function::Kurt => vec![Signature::Vector; arg_count],
Function::Large => vec![Signature::Vector, Signature::Scalar],
Function::MaxA => vec![Signature::Vector; arg_count],
Function::Median => vec![Signature::Vector; arg_count],
Function::MinA => vec![Signature::Vector; arg_count],
Function::RankAvg => vec![Signature::Scalar, Signature::Vector, Signature::Scalar],
Function::RankEq => vec![Signature::Scalar, Signature::Vector, Signature::Scalar],
Function::Skew => vec![Signature::Vector; arg_count],
Function::SkewP => vec![Signature::Vector; arg_count],
Function::Small => vec![Signature::Vector, Signature::Scalar],
}
}
@@ -1014,7 +1032,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Atan => scalar_arguments(args),
Function::Atan2 => scalar_arguments(args),
Function::Atanh => scalar_arguments(args),
Function::Choose => scalar_arguments(args), // static_analysis_choose(args, cell),
Function::Choose => scalar_arguments(args),
Function::Column => not_implemented(args),
Function::Columns => not_implemented(args),
Function::Cos => scalar_arguments(args),
@@ -1061,7 +1079,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Lookup => not_implemented(args),
Function::Match => not_implemented(args),
Function::Offset => static_analysis_offset(args),
// FIXME: Row could return an array
Function::Row => StaticResult::Scalar,
Function::Rows => not_implemented(args),
Function::Vlookup => not_implemented(args),
@@ -1253,7 +1270,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Sheets => scalar_arguments(args),
Function::Cell => scalar_arguments(args),
Function::Info => scalar_arguments(args),
Function::Dget => not_implemented(args),
Function::Dmax => not_implemented(args),
Function::Dmin => not_implemented(args),
@@ -1266,7 +1282,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Dvar => not_implemented(args),
Function::Dvarp => not_implemented(args),
Function::Dstdevp => not_implemented(args),
Function::BetaDist => StaticResult::Scalar,
Function::BetaInv => StaticResult::Scalar,
Function::BinomDist => StaticResult::Scalar,
@@ -1287,6 +1302,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::FDistRT => StaticResult::Scalar,
Function::FInv => StaticResult::Scalar,
Function::FInvRT => StaticResult::Scalar,
Function::FTest => StaticResult::Scalar,
Function::Fisher => StaticResult::Scalar,
Function::FisherInv => StaticResult::Scalar,
Function::Gamma => StaticResult::Scalar,
@@ -1322,5 +1338,25 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::VarA => StaticResult::Scalar,
Function::WeibullDist => StaticResult::Scalar,
Function::ZTest => StaticResult::Scalar,
Function::Sumx2my2 => StaticResult::Scalar,
Function::Sumx2py2 => StaticResult::Scalar,
Function::Sumxmy2 => StaticResult::Scalar,
Function::Correl => StaticResult::Scalar,
Function::Rsq => StaticResult::Scalar,
Function::Intercept => StaticResult::Scalar,
Function::Slope => StaticResult::Scalar,
Function::Steyx => StaticResult::Scalar,
Function::Gauss => StaticResult::Scalar,
Function::Harmean => StaticResult::Scalar,
Function::Kurt => StaticResult::Scalar,
Function::Large => StaticResult::Scalar,
Function::MaxA => StaticResult::Scalar,
Function::Median => StaticResult::Scalar,
Function::MinA => StaticResult::Scalar,
Function::RankAvg => StaticResult::Scalar,
Function::RankEq => StaticResult::Scalar,
Function::Skew => StaticResult::Scalar,
Function::SkewP => StaticResult::Scalar,
Function::Small => StaticResult::Scalar,
}
}

View File

@@ -4,6 +4,7 @@ use chrono::Months;
use chrono::NaiveDateTime;
use chrono::NaiveTime;
use chrono::Timelike;
use chrono_tz::Tz;
const SECONDS_PER_DAY: i32 = 86_400;
const SECONDS_PER_DAY_F64: f64 = SECONDS_PER_DAY as f64;
@@ -770,12 +771,12 @@ impl Model {
Ok(values)
}
// Returns the current date/time as an Excel serial number in the model's configured timezone.
// Returns the current date/time as an Excel serial number in the given timezone.
// Used by TODAY() and NOW().
fn current_excel_serial(&self) -> Option<f64> {
pub(crate) fn current_excel_serial_with_timezone(&self, tz: Tz) -> Option<f64> {
let seconds = get_milliseconds_since_epoch() / 1000;
DateTime::from_timestamp(seconds, 0).map(|dt| {
let local_time = dt.with_timezone(&self.tz);
let local_time = dt.with_timezone(&tz);
let days_from_1900 = local_time.num_days_from_ce() - EXCEL_DATE_BASE;
let fraction = (local_time.num_seconds_from_midnight() as f64) / (60.0 * 60.0 * 24.0);
days_from_1900 as f64 + fraction
@@ -978,7 +979,7 @@ impl Model {
message: "Wrong number of arguments".to_string(),
};
}
match self.current_excel_serial() {
match self.current_excel_serial_with_timezone(self.tz) {
Some(serial) => CalcResult::Number(serial.floor()),
None => CalcResult::Error {
error: Error::ERROR,
@@ -989,14 +990,35 @@ impl Model {
}
pub(crate) fn fn_now(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !args.is_empty() {
if args.len() > 1 {
return CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Wrong number of arguments".to_string(),
};
}
match self.current_excel_serial() {
let tz = match args.first() {
Some(arg0) => {
// Parse timezone argument
let tz_str = match self.get_string(arg0, cell) {
Ok(s) => s,
Err(e) => return e,
};
let tz: Tz = match tz_str.parse() {
Ok(tz) => tz,
Err(_) => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: format!("Invalid timezone: {}", &tz_str),
}
}
};
tz
}
None => self.tz,
};
match self.current_excel_serial_with_timezone(tz) {
Some(serial) => CalcResult::Number(serial),
None => CalcResult::Error {
error: Error::ERROR,

View File

@@ -1,81 +1,15 @@
use crate::cast::NumberOrArray;
use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::constants::{EXCEL_PRECISION, LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::ArrayNode;
use crate::expressions::types::CellReferenceIndex;
use crate::functions::math_util::{from_roman, to_roman_with_form};
use crate::number_format::to_precision;
use crate::number_format::{to_excel_precision, to_precision};
use crate::single_number_fn;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
use statrs::function::gamma::ln_gamma;
use std::f64::consts::LN_2;
use std::f64::consts::PI;
const FACT_TABLE: [f64; 26] = [
1.0, // 0!
1.0, // 1!
2.0, // 2!
6.0, // 3!
24.0, // 4!
120.0, // 5!
720.0, // 6!
5040.0, // 7!
40320.0, // 8!
362880.0, // 9!
3628800.0, // 10!
39916800.0, // 11!
479001600.0, // 12!
6227020800.0, // 13!
87178291200.0, // 14!
1307674368000.0, // 15!
20922789888000.0, // 16!
355687428096000.0, // 17!
6402373705728000.0, // 18!
121645100408832000.0, // 19!
2432902008176640000.0, // 20!
51090942171709440000.0, // 21!
1124000727777607680000.0, // 22!
25852016738884976640000.0, // 23!
620448401733239439360000.0, // 24!
15511210043330985984000000.0, // 25!
];
const FACTDOUBLE_TABLE: [f64; 32] = [
1.0,
1.0,
2.0,
3.0,
8.0,
15.0,
48.0,
105.0,
384.0,
945.0,
3840.0,
10395.0,
46080.0,
135135.0,
645120.0,
2027025.0,
10321920.0,
34459425.0,
185794560.0,
654729075.0,
3715891200.0,
13749310575.0,
81749606400.0,
316234143225.0,
1961990553600.0,
7905853580625.0,
51011754393600.0,
213458046676875.0,
1428329123020800.0,
6190283353629370.0,
42849873690624000.0,
191898783962511000.0,
];
#[cfg(not(target_arch = "wasm32"))]
pub fn random() -> f64 {
rand::random()
@@ -1050,7 +984,9 @@ impl Model {
};
}
let result = f64::floor(value / significance) * significance;
// Apply Excel precision to the ratio to handle floating-point rounding errors
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
let result = f64::floor(ratio) * significance;
CalcResult::Number(result)
}
@@ -1187,10 +1123,14 @@ impl Model {
}
let significance = significance.abs();
if value < 0.0 && mode != 0.0 {
let result = f64::ceil(value / significance) * significance;
// Apply Excel precision to handle floating-point rounding errors
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
let result = f64::ceil(ratio) * significance;
CalcResult::Number(result)
} else {
let result = f64::floor(value / significance) * significance;
// Apply Excel precision to handle floating-point rounding errors
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
let result = f64::floor(ratio) * significance;
CalcResult::Number(result)
}
}
@@ -1220,7 +1160,9 @@ impl Model {
return CalcResult::Number(0.0);
}
let result = f64::floor(value / significance) * significance;
// Apply Excel precision to handle floating-point rounding errors
let ratio = to_excel_precision(value / significance, EXCEL_PRECISION);
let result = f64::floor(ratio) * significance;
CalcResult::Number(result)
}
@@ -1385,78 +1327,35 @@ impl Model {
single_number_fn!(fn_sec, |f| Ok(1.0 / f64::cos(f)));
single_number_fn!(fn_sech, |f| Ok(1.0 / f64::cosh(f)));
single_number_fn!(fn_exp, |f: f64| Ok(f64::exp(f)));
single_number_fn!(fn_fact, |x: f64| {
let x = x.floor();
if x < 0.0 {
return Err(Error::NUM);
}
if x == 0.0 {
return Ok(1.0);
let mut acc = 1.0;
let mut k = 2.0;
while k <= x {
acc *= k;
k += 1.0;
}
if x < FACT_TABLE.len() as f64 {
return Ok(FACT_TABLE[x as usize]);
}
// Use ln Γ(x+1) to avoid overflow while deciding.
let ln_val = ln_gamma(x + 1.0);
// If gamma overflows or is invalid, map to NUM
if !ln_val.is_finite() || ln_val > f64::MAX.ln() {
return Err(Error::NUM);
}
Ok(ln_val.exp())
Ok(acc)
});
single_number_fn!(fn_factdouble, |x: f64| {
let x = x.floor();
if x <= -1.0 {
if x < -1.0 {
return Err(Error::NUM);
}
if x <= 1.0 {
if x < 0.0 {
return Ok(1.0);
}
// From here x > 1 and integer
let n = x as i64;
if n < FACTDOUBLE_TABLE.len() as i64 {
return Ok(FACTDOUBLE_TABLE[n as usize]);
let mut acc = 1.0;
let mut k = if x % 2.0 == 0.0 { 2.0 } else { 1.0 };
while k <= x {
acc *= k;
k += 2.0;
}
// n!! grows very fast, so we compute it using gamma in log-space:
//
// If n is even, n = 2k:
// n!! = 2^k * k!
// If n is odd, n = 2k + 1:
// n!! = (2k+1)! / (2^k * k!)
//
// and we use ln_gamma for factorials.
let ln_val = if n % 2 == 0 {
// even n = 2k
let k = (n / 2) as f64;
// ln(n!!) = k * ln(2) + ln(k!)
k * LN_2 + ln_gamma(k + 1.0)
} else {
// odd n = 2k + 1
let k = ((n - 1) / 2) as f64;
let nn = n as f64;
// ln(n!!) = ln((2k+1)!) - (k * ln(2) + ln(k!))
ln_gamma(nn + 1.0) - (k * LN_2 + ln_gamma(k + 1.0))
};
if !ln_val.is_finite() || ln_val > f64::MAX.ln() {
return Err(Error::NUM);
}
Ok(ln_val.exp())
Ok(acc)
});
single_number_fn!(fn_sign, |f| {
if f == 0.0 {
Ok(0.0)

View File

@@ -0,0 +1,230 @@
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
type TwoMatricesResult = (i32, i32, Vec<Option<f64>>, Vec<Option<f64>>);
// Helper to check if two shapes are the same or compatible 1D shapes
fn is_same_shape_or_1d(rows1: i32, cols1: i32, rows2: i32, cols2: i32) -> bool {
(rows1 == rows2 && cols1 == cols2)
|| (rows1 == 1 && cols2 == 1 && cols1 == rows2)
|| (rows2 == 1 && cols1 == 1 && cols2 == rows1)
}
impl Model {
// SUMX2MY2(array_x, array_y) - Returns the sum of the difference of squares
pub(crate) fn fn_sumx2my2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let result = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(s) => return s,
};
let (_, _, values_left, values_right) = result;
let mut sum = 0.0;
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
let x = x_opt.unwrap_or(0.0);
let y = y_opt.unwrap_or(0.0);
sum += x * x - y * y;
}
CalcResult::Number(sum)
}
// SUMX2PY2(array_x, array_y) - Returns the sum of the sum of squares
pub(crate) fn fn_sumx2py2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let result = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(s) => return s,
};
let (_rows, _cols, values_left, values_right) = result;
let mut sum = 0.0;
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
let x = x_opt.unwrap_or(0.0);
let y = y_opt.unwrap_or(0.0);
sum += x * x + y * y;
}
CalcResult::Number(sum)
}
// SUMXMY2(array_x, array_y) - Returns the sum of squares of differences
pub(crate) fn fn_sumxmy2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let result = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(s) => return s,
};
let (_, _, values_left, values_right) = result;
let mut sum = 0.0;
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
let x = x_opt.unwrap_or(0.0);
let y = y_opt.unwrap_or(0.0);
let diff = x - y;
sum += diff * diff;
}
CalcResult::Number(sum)
}
// Helper function to extract and validate two matrices (ranges or arrays) with compatible shapes.
// Returns (rows, cols, values_left, values_right) or an error.
pub(crate) fn fn_get_two_matrices(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> Result<TwoMatricesResult, CalcResult> {
if args.len() != 2 {
return Err(CalcResult::new_args_number_error(cell));
}
let x_range = self.evaluate_node_in_context(&args[0], cell);
let y_range = self.evaluate_node_in_context(&args[1], cell);
let result = match (x_range, y_range) {
(
CalcResult::Range {
left: l1,
right: r1,
},
CalcResult::Range {
left: l2,
right: r2,
},
) => {
if l1.sheet != l2.sheet {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
));
}
let rows1 = r1.row - l1.row + 1;
let cols1 = r1.column - l1.column + 1;
let rows2 = r2.row - l2.row + 1;
let cols2 = r2.column - l2.column + 1;
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Ranges must be of the same shape".to_string(),
));
}
let values_left = self.values_from_range(l1, r1)?;
let values_right = self.values_from_range(l2, r2)?;
(rows1, cols1, values_left, values_right)
}
(
CalcResult::Array(left),
CalcResult::Range {
left: l2,
right: r2,
},
) => {
let rows2 = r2.row - l2.row + 1;
let cols2 = r2.column - l2.column + 1;
let rows1 = left.len() as i32;
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Array and range must be of the same shape".to_string(),
));
}
let values_left = match self.values_from_array(left) {
Err(error) => {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in first array: {:?}", error),
));
}
Ok(v) => v,
};
let values_right = self.values_from_range(l2, r2)?;
(rows2, cols2, values_left, values_right)
}
(
CalcResult::Range {
left: l1,
right: r1,
},
CalcResult::Array(right),
) => {
let rows1 = r1.row - l1.row + 1;
let cols1 = r1.column - l1.column + 1;
let rows2 = right.len() as i32;
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Range and array must be of the same shape".to_string(),
));
}
let values_left = self.values_from_range(l1, r1)?;
let values_right = match self.values_from_array(right) {
Err(error) => {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in second array: {:?}", error),
));
}
Ok(v) => v,
};
(rows1, cols1, values_left, values_right)
}
(CalcResult::Array(left), CalcResult::Array(right)) => {
let rows1 = left.len() as i32;
let rows2 = right.len() as i32;
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Arrays must be of the same shape".to_string(),
));
}
let values_left = match self.values_from_array(left) {
Err(error) => {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in first array: {:?}", error),
));
}
Ok(v) => v,
};
let values_right = match self.values_from_array(right) {
Err(error) => {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in second array: {:?}", error),
));
}
Ok(v) => v,
};
(rows1, cols1, values_left, values_right)
}
_ => {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Both arguments must be ranges or arrays".to_string(),
));
}
};
Ok(result)
}
}

View File

@@ -19,6 +19,7 @@ mod lookup_and_reference;
mod macros;
mod math_util;
mod mathematical;
mod mathematical_sum;
mod statistical;
mod subtotal;
mod text;
@@ -76,6 +77,9 @@ pub enum Function {
Sum,
Sumif,
Sumifs,
Sumx2my2,
Sumx2py2,
Sumxmy2,
Tan,
Tanh,
Acot,
@@ -203,7 +207,6 @@ pub enum Function {
ChisqTest,
ConfidenceNorm,
ConfidenceT,
// Correl,
CovarianceP,
CovarianceS,
Devsq,
@@ -212,7 +215,7 @@ pub enum Function {
FDistRT,
FInv,
FInvRT,
// FTest,
FTest,
Fisher,
FisherInv,
// Forecast,
@@ -221,20 +224,18 @@ pub enum Function {
GammaInv,
GammaLn,
GammaLnPrecise,
// Gauss,
// Growth,
// Harmean,
Gauss,
Harmean,
HypGeomDist,
// Intercept,
// Kurt,
// Large,
Kurt,
Large,
// Linest,
// Logest,
LogNormDist,
LogNormInv,
// MaxA,
// Median,
// MinA,
MaxA,
Median,
MinA,
// ModeMult,
// ModeSingl,
NegbinomDist,
@@ -254,19 +255,16 @@ pub enum Function {
// Prob,
// QuartileExc,
// QuartileInc,
// RankAvg,
// RankEq,
// Rsq
// Skew,
// SkewP,
// Slope,
// Small,
RankAvg,
RankEq,
Skew,
SkewP,
Small,
Standardize,
StDevP,
StDevS,
Stdeva,
Stdevpa,
// Steyx,
TDist,
TDist2T,
TDistRT,
@@ -417,10 +415,16 @@ pub enum Function {
Dvar,
Dvarp,
Dstdevp,
Correl,
Rsq,
Intercept,
Slope,
Steyx,
}
impl Function {
pub fn into_iter() -> IntoIter<Function, 324> {
pub fn into_iter() -> IntoIter<Function, 345> {
[
Function::And,
Function::False,
@@ -497,6 +501,9 @@ impl Function {
Function::Sum,
Function::Sumif,
Function::Sumifs,
Function::Sumx2my2,
Function::Sumx2py2,
Function::Sumxmy2,
Function::Choose,
Function::Column,
Function::Columns,
@@ -711,6 +718,7 @@ impl Function {
Function::FDistRT,
Function::FInv,
Function::FInvRT,
Function::FTest,
Function::Fisher,
Function::FisherInv,
Function::Gamma,
@@ -746,6 +754,23 @@ impl Function {
Function::VarA,
Function::WeibullDist,
Function::ZTest,
Function::Correl,
Function::Rsq,
Function::Intercept,
Function::Slope,
Function::Steyx,
Function::Large,
Function::Median,
Function::Small,
Function::RankAvg,
Function::RankEq,
Function::Skew,
Function::SkewP,
Function::Harmean,
Function::Gauss,
Function::Kurt,
Function::MaxA,
Function::MinA,
]
.into_iter()
}
@@ -837,6 +862,7 @@ impl Function {
Function::FDistRT => "_xlfn.F.DIST.RT".to_string(),
Function::FInv => "_xlfn.F.INV".to_string(),
Function::FInvRT => "_xlfn.F.INV.RT".to_string(),
Function::FTest => "_xlfn.F.TEST".to_string(),
Function::HypGeomDist => "_xlfn.HYPGEOM.DIST".to_string(),
@@ -867,6 +893,9 @@ impl Function {
Function::WeibullDist => "_xlfn.WEIBULL.DIST".to_string(),
Function::ZTest => "_xlfn.Z.TEST".to_string(),
Function::SkewP => "_xlfn.SKEW.P".to_string(),
Function::RankAvg => "_xlfn.RANK.AVG".to_string(),
Function::RankEq => "_xlfn.RANK.EQ".to_string(),
_ => self.to_string(),
}
@@ -1186,6 +1215,7 @@ impl Function {
"F.DIST.RT" | "_XLFN.F.DIST.RT" => Some(Function::FDistRT),
"F.INV" | "_XLFN.F.INV" => Some(Function::FInv),
"F.INV.RT" | "_XLFN.F.INV.RT" => Some(Function::FInvRT),
"F.TEST" | "_XLFN.F.TEST" => Some(Function::FTest),
"FISHER" => Some(Function::Fisher),
"FISHERINV" => Some(Function::FisherInv),
"GAMMA" | "_XLFN.GAMMA" => Some(Function::Gamma),
@@ -1221,6 +1251,28 @@ impl Function {
"VARA" => Some(Function::VarA),
"WEIBULL.DIST" | "_XLFN.WEIBULL.DIST" => Some(Function::WeibullDist),
"Z.TEST" | "_XLFN.Z.TEST" => Some(Function::ZTest),
"SUMX2MY2" => Some(Function::Sumx2my2),
"SUMX2PY2" => Some(Function::Sumx2py2),
"SUMXMY2" => Some(Function::Sumxmy2),
"CORREL" => Some(Function::Correl),
"RSQ" => Some(Function::Rsq),
"INTERCEPT" => Some(Function::Intercept),
"SLOPE" => Some(Function::Slope),
"STEYX" => Some(Function::Steyx),
"SKEW.P" | "_XLFN.SKEW.P" => Some(Function::SkewP),
"SKEW" => Some(Function::Skew),
"KURT" => Some(Function::Kurt),
"HARMEAN" => Some(Function::Harmean),
"MEDIAN" => Some(Function::Median),
"GAUSS" => Some(Function::Gauss),
"MINA" => Some(Function::MinA),
"MAXA" => Some(Function::MaxA),
"SMALL" => Some(Function::Small),
"LARGE" => Some(Function::Large),
"RANK.EQ" | "_XLFN.RANK.EQ" => Some(Function::RankEq),
"RANK.AVG" | "_XLFN.RANK.AVG" => Some(Function::RankAvg),
_ => None,
}
@@ -1483,7 +1535,6 @@ impl fmt::Display for Function {
Function::Combin => write!(f, "COMBIN"),
Function::Combina => write!(f, "COMBINA"),
Function::Sumsq => write!(f, "SUMSQ"),
Function::N => write!(f, "N"),
Function::Cell => write!(f, "CELL"),
Function::Info => write!(f, "INFO"),
@@ -1500,7 +1551,6 @@ impl fmt::Display for Function {
Function::Dvar => write!(f, "DVAR"),
Function::Dvarp => write!(f, "DVARP"),
Function::Dstdevp => write!(f, "DSTDEVP"),
Function::BetaDist => write!(f, "BETA.DIST"),
Function::BetaInv => write!(f, "BETA.INV"),
Function::BinomDist => write!(f, "BINOM.DIST"),
@@ -1523,6 +1573,7 @@ impl fmt::Display for Function {
Function::FInvRT => write!(f, "F.INV.RT"),
Function::Fisher => write!(f, "FISHER"),
Function::FisherInv => write!(f, "FISHERINV"),
Function::FTest => write!(f, "F.TEST"),
Function::Gamma => write!(f, "GAMMA"),
Function::GammaDist => write!(f, "GAMMA.DIST"),
Function::GammaInv => write!(f, "GAMMA.INV"),
@@ -1556,6 +1607,27 @@ impl fmt::Display for Function {
Function::VarA => write!(f, "VARA"),
Function::WeibullDist => write!(f, "WEIBULL.DIST"),
Function::ZTest => write!(f, "Z.TEST"),
Function::Sumx2my2 => write!(f, "SUMX2MY2"),
Function::Sumx2py2 => write!(f, "SUMX2PY2"),
Function::Sumxmy2 => write!(f, "SUMXMY2"),
Function::Correl => write!(f, "CORREL"),
Function::Rsq => write!(f, "RSQ"),
Function::Intercept => write!(f, "INTERCEPT"),
Function::Slope => write!(f, "SLOPE"),
Function::Steyx => write!(f, "STEYX"),
// new ones
Function::Gauss => write!(f, "GAUSS"),
Function::Harmean => write!(f, "HARMEAN"),
Function::Kurt => write!(f, "KURT"),
Function::Large => write!(f, "LARGE"),
Function::MaxA => write!(f, "MAXA"),
Function::Median => write!(f, "MEDIAN"),
Function::MinA => write!(f, "MINA"),
Function::RankAvg => write!(f, "RANK.AVG"),
Function::RankEq => write!(f, "RANK.EQ"),
Function::Skew => write!(f, "SKEW"),
Function::SkewP => write!(f, "SKEW.P"),
Function::Small => write!(f, "SMALL"),
}
}
}
@@ -1875,6 +1947,7 @@ impl Model {
Function::FInvRT => self.fn_f_inv_rt(args, cell),
Function::Fisher => self.fn_fisher(args, cell),
Function::FisherInv => self.fn_fisher_inv(args, cell),
Function::FTest => self.fn_f_test(args, cell),
Function::Gamma => self.fn_gamma(args, cell),
Function::GammaDist => self.fn_gamma_dist(args, cell),
Function::GammaInv => self.fn_gamma_inv(args, cell),
@@ -1908,6 +1981,26 @@ impl Model {
Function::VarA => self.fn_vara(args, cell),
Function::WeibullDist => self.fn_weibull_dist(args, cell),
Function::ZTest => self.fn_z_test(args, cell),
Function::Sumx2my2 => self.fn_sumx2my2(args, cell),
Function::Sumx2py2 => self.fn_sumx2py2(args, cell),
Function::Sumxmy2 => self.fn_sumxmy2(args, cell),
Function::Correl => self.fn_correl(args, cell),
Function::Rsq => self.fn_rsq(args, cell),
Function::Intercept => self.fn_intercept(args, cell),
Function::Slope => self.fn_slope(args, cell),
Function::Steyx => self.fn_steyx(args, cell),
Function::Gauss => self.fn_gauss(args, cell),
Function::Harmean => self.fn_harmean(args, cell),
Function::Kurt => self.fn_kurt(args, cell),
Function::Large => self.fn_large(args, cell),
Function::MaxA => self.fn_maxa(args, cell),
Function::Median => self.fn_median(args, cell),
Function::MinA => self.fn_mina(args, cell),
Function::RankAvg => self.fn_rank_avg(args, cell),
Function::RankEq => self.fn_rank_eq(args, cell),
Function::Skew => self.fn_skew(args, cell),
Function::SkewP => self.fn_skew_p(args, cell),
Function::Small => self.fn_small(args, cell),
}
}
}

View File

@@ -6,13 +6,6 @@ use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
// Helper to check if two shapes are the same or compatible 1D shapes
pub(crate) fn is_same_shape_or_1d(rows1: i32, cols1: i32, rows2: i32, cols2: i32) -> bool {
(rows1 == rows2 && cols1 == cols2)
|| (rows1 == 1 && cols2 == 1 && cols1 == rows2)
|| (rows2 == 1 && cols1 == 1 && cols2 == rows1)
}
impl Model {
// CHISQ.DIST(x, deg_freedom, cumulative)
pub(crate) fn fn_chisq_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
@@ -310,171 +303,10 @@ impl Model {
// CHISQ.TEST(actual_range, expected_range)
pub(crate) fn fn_chisq_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let actual_range = self.evaluate_node_in_context(&args[0], cell);
let expected_range = self.evaluate_node_in_context(&args[1], cell);
let (width, height, values_left, values_right) = match (actual_range, expected_range) {
(
CalcResult::Range {
left: l1,
right: r1,
},
CalcResult::Range {
left: l2,
right: r2,
},
) => {
if l1.sheet != l2.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
let rows1 = r1.row - l1.row + 1;
let cols1 = r1.column - l1.column + 1;
let rows2 = r2.row - l2.row + 1;
let cols2 = r2.column - l2.column + 1;
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges must be of the same shape".to_string(),
);
}
let values_left = match self.values_from_range(l1, r1) {
Err(error) => {
return error;
}
Ok(v) => v,
};
let values_right = match self.values_from_range(l2, r2) {
Err(error) => {
return error;
}
Ok(v) => v,
};
(rows1, cols1, values_left, values_right)
}
(
CalcResult::Array(left),
CalcResult::Range {
left: l2,
right: r2,
},
) => {
let rows2 = r2.row - l2.row + 1;
let cols2 = r2.column - l2.column + 1;
let rows1 = left.len() as i32;
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return CalcResult::new_error(
Error::VALUE,
cell,
"Array and range must be of the same shape".to_string(),
);
}
let values_left = match self.values_from_array(left) {
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in first array: {:?}", error),
);
}
Ok(v) => v,
};
let values_right = match self.values_from_range(l2, r2) {
Err(error) => {
return error;
}
Ok(v) => v,
};
(rows2, cols2, values_left, values_right)
}
(
CalcResult::Range {
left: l1,
right: r1,
},
CalcResult::Array(right),
) => {
let rows1 = r1.row - l1.row + 1;
let cols1 = r1.column - l1.column + 1;
let rows2 = right.len() as i32;
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return CalcResult::new_error(
Error::VALUE,
cell,
"Range and array must be of the same shape".to_string(),
);
}
let values_left = match self.values_from_range(l1, r1) {
Err(error) => {
return error;
}
Ok(v) => v,
};
let values_right = match self.values_from_array(right) {
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in second array: {:?}", error),
);
}
Ok(v) => v,
};
(rows1, cols1, values_left, values_right)
}
(CalcResult::Array(left), CalcResult::Array(right)) => {
let rows1 = left.len() as i32;
let rows2 = right.len() as i32;
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return CalcResult::new_error(
Error::VALUE,
cell,
"Arrays must be of the same shape".to_string(),
);
}
let values_left = match self.values_from_array(left) {
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in first array: {:?}", error),
);
}
Ok(v) => v,
};
let values_right = match self.values_from_array(right) {
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in second array: {:?}", error),
);
}
Ok(v) => v,
};
(rows1, cols1, values_left, values_right)
}
_ => {
return CalcResult::new_error(
Error::VALUE,
cell,
"Both arguments must be ranges or arrays".to_string(),
);
}
let (width, height, values_left, values_right) = match self.fn_get_two_matrices(args, cell)
{
Ok(v) => v,
Err(r) => return r,
};
let mut values = Vec::with_capacity(values_left.len());

View File

@@ -0,0 +1,227 @@
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl Model {
// CORREL(array1, array2) - Returns the correlation coefficient of two data sets
pub(crate) fn fn_correl(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (_, _, values_left, values_right) = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(e) => return e,
};
let mut n = 0.0;
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut sum_x2 = 0.0;
let mut sum_y2 = 0.0;
let mut sum_xy = 0.0;
for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) {
if let (Some(x), Some(y)) = (x_opt, y_opt) {
n += 1.0;
sum_x += x;
sum_y += y;
sum_x2 += x * x;
sum_y2 += y * y;
sum_xy += x * y;
}
}
// Need at least 2 valid pairs
if n < 2.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"CORREL requires at least two numeric data points in each range".to_string(),
);
}
let num = n * sum_xy - sum_x * sum_y;
let denom_x = n * sum_x2 - sum_x * sum_x;
let denom_y = n * sum_y2 - sum_y * sum_y;
let denom = (denom_x * denom_y).sqrt();
if denom == 0.0 || !denom.is_finite() {
return CalcResult::new_error(
Error::DIV,
cell,
"Division by zero in CORREL".to_string(),
);
}
let r = num / denom;
CalcResult::Number(r)
}
// SLOPE(known_y's, known_x's) - Returns the slope of the linear regression line
pub(crate) fn fn_slope(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(e) => return e,
};
let mut n = 0.0;
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut sum_x2 = 0.0;
let mut sum_xy = 0.0;
let len = values_y.len().min(values_x.len());
for i in 0..len {
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
n += 1.0;
sum_x += x;
sum_y += y;
sum_x2 += x * x;
sum_xy += x * y;
}
}
if n < 2.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"SLOPE requires at least two numeric data points".to_string(),
);
}
let denom = n * sum_x2 - sum_x * sum_x;
if denom == 0.0 || !denom.is_finite() {
return CalcResult::new_error(
Error::DIV,
cell,
"Division by zero in SLOPE".to_string(),
);
}
let num = n * sum_xy - sum_x * sum_y;
let slope = num / denom;
CalcResult::Number(slope)
}
// INTERCEPT(known_y's, known_x's) - Returns the y-intercept of the linear regression line
pub(crate) fn fn_intercept(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(e) => return e,
};
let mut n = 0.0;
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut sum_x2 = 0.0;
let mut sum_xy = 0.0;
let len = values_y.len().min(values_x.len());
for i in 0..len {
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
n += 1.0;
sum_x += x;
sum_y += y;
sum_x2 += x * x;
sum_xy += x * y;
}
}
if n < 2.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"INTERCEPT requires at least two numeric data points".to_string(),
);
}
let denom = n * sum_x2 - sum_x * sum_x;
if denom == 0.0 || !denom.is_finite() {
return CalcResult::new_error(
Error::DIV,
cell,
"Division by zero in INTERCEPT".to_string(),
);
}
let num = n * sum_xy - sum_x * sum_y;
let slope = num / denom;
let intercept = (sum_y - slope * sum_x) / n;
CalcResult::Number(intercept)
}
// STEYX(known_y's, known_x's) - Returns the standard error of the predicted y-values
pub(crate) fn fn_steyx(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (_rows, _cols, values_y, values_x) = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(e) => return e,
};
let mut n = 0.0;
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut sum_x2 = 0.0;
let mut sum_xy = 0.0;
// We need the actual pairs again later for residuals
let mut pairs: Vec<(f64, f64)> = Vec::new();
let len = values_y.len().min(values_x.len());
for i in 0..len {
if let (Some(y), Some(x)) = (values_y[i], values_x[i]) {
n += 1.0;
sum_x += x;
sum_y += y;
sum_x2 += x * x;
sum_xy += x * y;
pairs.push((x, y));
}
}
// Need at least 3 points for STEYX (n - 2 in denominator)
if n < 3.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"STEYX requires at least three numeric data points".to_string(),
);
}
let denom = n * sum_x2 - sum_x * sum_x;
if denom == 0.0 || !denom.is_finite() {
return CalcResult::new_error(
Error::DIV,
cell,
"Division by zero in STEYX".to_string(),
);
}
let num = n * sum_xy - sum_x * sum_y;
let slope = num / denom;
let intercept = (sum_y - slope * sum_x) / n;
// Sum of squared residuals: Σ (y - ŷ)^2, ŷ = intercept + slope * x
let mut sse = 0.0;
for (x, y) in pairs {
let y_hat = intercept + slope * x;
let diff = y - y_hat;
sse += diff * diff;
}
let dof = n - 2.0;
if dof <= 0.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"STEYX has non-positive degrees of freedom".to_string(),
);
}
let sey = (sse / dof).sqrt();
if !sey.is_finite() {
return CalcResult::new_error(Error::DIV, cell, "Numerical error in STEYX".to_string());
}
CalcResult::Number(sey)
}
}

View File

@@ -1,3 +1,5 @@
use std::cmp::Ordering;
use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::ArrayNode;
use crate::expressions::types::CellReferenceIndex;
@@ -6,77 +8,219 @@ use crate::{
};
impl Model {
pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut count = 0.0;
let mut sum = 0.0;
fn for_each_value<F>(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
mut f: F,
) -> Result<(), CalcResult>
where
F: FnMut(f64),
{
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
count += 1.0;
sum += value;
f(value);
}
CalcResult::Boolean(b) => {
if let Node::ReferenceKind { .. } = arg {
} else {
sum += if b { 1.0 } else { 0.0 };
count += 1.0;
CalcResult::Boolean(value) => {
if !matches!(arg, Node::ReferenceKind { .. }) {
f(if value { 1.0 } else { 0.0 });
}
}
CalcResult::String(value) => {
if !matches!(arg, Node::ReferenceKind { .. }) {
if let Some(parsed) = self.cast_number(&value) {
f(parsed);
} else {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Argument cannot be cast into number".to_string(),
));
}
}
}
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
f(value);
}
ArrayNode::Boolean(b) => {
f(if b { 1.0 } else { 0.0 });
}
ArrayNode::Error(error) => {
return Err(CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
});
}
_ => {
// ignore non-numeric
}
}
}
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
));
}
for row in left.row..(right.row + 1) {
for column in left.column..(right.column + 1) {
for row in left.row..=right.row {
for column in left.column..=right.column {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
count += 1.0;
sum += value;
f(value);
}
error @ CalcResult::Error { .. } => return error,
error @ CalcResult::Error { .. } => return Err(error),
CalcResult::Range { .. } => {
return CalcResult::new_error(
return Err(CalcResult::new_error(
Error::ERROR,
cell,
"Unexpected Range".to_string(),
);
));
}
_ => {}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
CalcResult::String(s) => {
if let Node::ReferenceKind { .. } = arg {
// Do nothing
} else if let Ok(t) = s.parse::<f64>() {
sum += t;
count += 1.0;
} else {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Argument cannot be cast into number".to_string(),
};
error @ CalcResult::Error { .. } => return Err(error),
// Everything else is ignored
_ => {}
}
}
Ok(())
}
fn for_each_value_a<F>(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
mut f: F,
) -> Result<(), CalcResult>
where
F: FnMut(f64),
{
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
f(value);
}
CalcResult::Boolean(value) => {
if !matches!(arg, Node::ReferenceKind { .. }) {
f(if value { 1.0 } else { 0.0 });
}
}
_ => {
// Ignore everything else
CalcResult::String(value) => {
if !matches!(arg, Node::ReferenceKind { .. }) {
if let Some(parsed) = self.cast_number(&value) {
f(parsed);
} else {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Argument cannot be cast into number".to_string(),
));
}
}
}
};
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
f(value);
}
ArrayNode::Boolean(b) => {
f(if b { 1.0 } else { 0.0 });
}
ArrayNode::String(_) => {
f(0.0);
}
ArrayNode::Error(error) => {
return Err(CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
});
}
}
}
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
));
}
for row in left.row..=right.row {
for column in left.column..=right.column {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
f(value);
}
CalcResult::Boolean(b) => {
f(if b { 1.0 } else { 0.0 });
}
CalcResult::String(_) => {
f(0.0);
}
error @ CalcResult::Error { .. } => return Err(error),
CalcResult::Range { .. } => {
return Err(CalcResult::new_error(
Error::ERROR,
cell,
"Unexpected Range".to_string(),
));
}
_ => {}
}
}
}
}
error @ CalcResult::Error { .. } => return Err(error),
// Everything else is ignored
_ => {}
}
}
Ok(())
}
pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut count = 0.0;
let mut sum = 0.0;
if let Err(e) = self.for_each_value(args, cell, |f| {
count += 1.0;
sum += f;
}) {
return e;
}
if count == 0.0 {
return CalcResult::Error {
error: Error::DIV,
@@ -86,6 +230,7 @@ impl Model {
}
CalcResult::Number(sum / count)
}
pub(crate) fn fn_averagea(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
@@ -443,4 +588,484 @@ impl Model {
CalcResult::Number(sum_abs_dev / n)
}
pub(crate) fn fn_median(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut values: Vec<f64> = Vec::new();
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
return e;
}
if values.is_empty() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "No numeric values for MEDIAN".to_string(),
};
}
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let n = values.len();
let median = if n % 2 == 1 {
// odd
values[n / 2]
} else {
// even: average of the two middle values
let a = values[(n / 2) - 1];
let b = values[n / 2];
(a + b) / 2.0
};
CalcResult::Number(median)
}
pub(crate) fn fn_harmean(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut values: Vec<f64> = Vec::new();
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
return e;
}
if values.is_empty() {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Division by Zero".to_string(),
};
}
// Excel HARMEAN: all values must be > 0
if values.iter().any(|&v| v <= 0.0) {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "HARMEAN requires strictly positive values".to_string(),
};
}
let n = values.len() as f64;
let sum_recip: f64 = values.iter().map(|v| 1.0 / v).sum();
if sum_recip == 0.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Division by Zero".to_string(),
};
}
CalcResult::Number(n / sum_recip)
}
pub(crate) fn fn_mina(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut mina: Option<f64> = None;
if let Err(e) = self.for_each_value_a(args, cell, |f| {
if let Some(m) = mina {
mina = Some(m.min(f));
} else {
mina = Some(f);
}
}) {
return e;
}
if let Some(mina) = mina {
CalcResult::Number(mina)
} else {
CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No numeric values for MINA".to_string(),
}
}
}
pub(crate) fn fn_maxa(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut maxa: Option<f64> = None;
if let Err(e) = self.for_each_value_a(args, cell, |f| {
if let Some(m) = maxa {
maxa = Some(m.max(f));
} else {
maxa = Some(f);
}
}) {
return e;
}
if let Some(maxa) = maxa {
CalcResult::Number(maxa)
} else {
CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No numeric values for MAXA".to_string(),
}
}
}
pub(crate) fn fn_skew(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
// Sample skewness (Excel SKEW)
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut values: Vec<f64> = Vec::new();
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
return e;
}
let n = values.len();
if n < 3 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "SKEW requires at least 3 data points".to_string(),
};
}
let n_f = n as f64;
let mean = values.iter().sum::<f64>() / n_f;
let mut m2 = 0.0;
for &x in &values {
let d = x - mean;
m2 += d * d;
}
if m2 == 0.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Zero variance in SKEW".to_string(),
};
}
let s = (m2 / (n_f - 1.0)).sqrt();
let mut sum_cubed = 0.0;
for &x in &values {
let z = (x - mean) / s;
sum_cubed += z * z * z;
}
let skew = (n_f / ((n_f - 1.0) * (n_f - 2.0))) * sum_cubed;
CalcResult::Number(skew)
}
pub(crate) fn fn_skew_p(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
// Population skewness (Excel SKEW.P)
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut values: Vec<f64> = Vec::new();
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
return e;
}
let n = values.len();
if n < 2 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "SKEW.P requires at least 2 data points".to_string(),
};
}
let n_f = n as f64;
let mean = values.iter().sum::<f64>() / n_f;
let mut m2 = 0.0;
for &x in &values {
let d = x - mean;
m2 += d * d;
}
if m2 == 0.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Zero variance in SKEW.P".to_string(),
};
}
let sigma = (m2 / n_f).sqrt();
let mut sum_cubed = 0.0;
for &x in &values {
let z = (x - mean) / sigma;
sum_cubed += z * z * z;
}
let skew_p = sum_cubed / n_f;
CalcResult::Number(skew_p)
}
pub(crate) fn fn_kurt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut values: Vec<f64> = Vec::new();
if let Err(e) = self.for_each_value(args, cell, |f| values.push(f)) {
return e;
}
let n = values.len();
if n < 4 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "KURT requires at least 4 data points".to_string(),
};
}
let n_f = n as f64;
let mean = values.iter().sum::<f64>() / n_f;
let mut m2 = 0.0;
for &x in &values {
let d = x - mean;
m2 += d * d;
}
if m2 == 0.0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Zero variance in KURT".to_string(),
};
}
let s = (m2 / (n_f - 1.0)).sqrt();
let mut sum_fourth = 0.0;
for &x in &values {
let z = (x - mean) / s;
sum_fourth += z * z * z * z;
}
let term1 = (n_f * (n_f + 1.0)) / ((n_f - 1.0) * (n_f - 2.0) * (n_f - 3.0)) * sum_fourth;
let term2 = 3.0 * (n_f - 1.0) * (n_f - 1.0) / ((n_f - 2.0) * (n_f - 3.0));
let kurt = term1 - term2;
CalcResult::Number(kurt)
}
pub(crate) fn fn_large(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let values = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Array(array) => match self.values_from_array(array) {
Ok(v) => v,
Err(e) => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: format!("Unsupported array argument: {}", e),
}
}
},
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
Ok(v) => v,
Err(e) => return e,
},
CalcResult::Boolean(value) => {
if !matches!(args[0], Node::ReferenceKind { .. }) {
vec![Some(if value { 1.0 } else { 0.0 })]
} else {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Unsupported argument type".to_string(),
};
}
}
CalcResult::Number(value) => {
if !matches!(args[0], Node::ReferenceKind { .. }) {
vec![Some(value)]
} else {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Unsupported argument type".to_string(),
};
}
}
CalcResult::String(value) => {
if !matches!(args[0], Node::ReferenceKind { .. }) {
if let Some(parsed) = self.cast_number(&value) {
vec![Some(parsed)]
} else {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Unsupported argument type".to_string(),
};
}
} else {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Unsupported argument type".to_string(),
};
}
}
_ => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Unsupported argument type".to_string(),
}
}
};
let k = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f.trunc(),
Err(s) => return s,
};
if k < 1.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "K must be >= 1".to_string(),
};
}
let mut numeric_values: Vec<f64> = values.into_iter().flatten().collect();
if numeric_values.is_empty() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "No numeric values for LARGE".to_string(),
};
}
numeric_values.sort_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
let k_usize = k as usize;
if k_usize > numeric_values.len() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "K is larger than the number of data points".to_string(),
};
}
CalcResult::Number(numeric_values[k_usize - 1])
}
pub(crate) fn fn_small(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let values = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Array(array) => match self.values_from_array(array) {
Ok(v) => v,
Err(e) => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: format!("Unsupported array argument: {}", e),
}
}
},
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
Ok(v) => v,
Err(e) => return e,
},
CalcResult::Boolean(value) => {
if !matches!(args[0], Node::ReferenceKind { .. }) {
vec![Some(if value { 1.0 } else { 0.0 })]
} else {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Unsupported argument type".to_string(),
};
}
}
CalcResult::Number(value) => {
if !matches!(args[0], Node::ReferenceKind { .. }) {
vec![Some(value)]
} else {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Unsupported argument type".to_string(),
};
}
}
CalcResult::String(value) => {
if !matches!(args[0], Node::ReferenceKind { .. }) {
if let Some(parsed) = self.cast_number(&value) {
vec![Some(parsed)]
} else {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Unsupported argument type".to_string(),
};
}
} else {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Unsupported argument type".to_string(),
};
}
}
_ => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Unsupported argument type".to_string(),
}
}
};
let k = match self.get_number_no_bools(&args[1], cell) {
Ok(f) => f.trunc(),
Err(s) => return s,
};
if k < 1.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "K must be >= 1".to_string(),
};
}
let mut numeric_values: Vec<f64> = values.into_iter().flatten().collect();
if numeric_values.is_empty() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "No numeric values for SMALL".to_string(),
};
}
// For SMALL, sort ascending
numeric_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let k_usize = k as usize;
if k_usize > numeric_values.len() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "K is larger than the number of data points".to_string(),
};
}
CalcResult::Number(numeric_values[k_usize - 1])
}
}

View File

@@ -1,6 +1,7 @@
use statrs::distribution::{Continuous, ContinuousCDF, FisherSnedecor};
use crate::expressions::types::CellReferenceIndex;
use crate::functions::statistical::t_dist::sample_var;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
@@ -296,4 +297,122 @@ impl Model {
CalcResult::Number(x)
}
// F.TEST(array1, array2)
pub(crate) fn fn_f_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let values1_opts = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
Ok(v) => v,
Err(error) => return error,
},
CalcResult::Array(a) => match self.values_from_array(a) {
Ok(v) => v,
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in first array: {:?}", error),
);
}
},
_ => {
return CalcResult::new_error(
Error::VALUE,
cell,
"First argument must be a range or array".to_string(),
);
}
};
// Get second sample as Vec<Option<f64>>
let values2_opts = match self.evaluate_node_in_context(&args[1], cell) {
CalcResult::Range { left, right } => match self.values_from_range(left, right) {
Ok(v) => v,
Err(error) => return error,
},
CalcResult::Array(a) => match self.values_from_array(a) {
Ok(v) => v,
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in second array: {:?}", error),
);
}
},
_ => {
return CalcResult::new_error(
Error::VALUE,
cell,
"Second argument must be a range or array".to_string(),
);
}
};
let values1: Vec<f64> = values1_opts.into_iter().flatten().collect();
let values2: Vec<f64> = values2_opts.into_iter().flatten().collect();
let n1 = values1.len();
let n2 = values2.len();
// If fewer than 2 numeric values in either sample -> #DIV/0!
if n1 < 2 || n2 < 2 {
return CalcResult::new_error(
Error::DIV,
cell,
"F.TEST requires at least two numeric values in each sample".to_string(),
);
}
let v1 = sample_var(&values1);
let v2 = sample_var(&values2);
if v1 <= 0.0 || v2 <= 0.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"Variance of one sample is zero in F.TEST".to_string(),
);
}
// F ratio: larger variance / smaller variance
let mut f = v1 / v2;
let mut df1 = (n1 - 1) as f64;
let mut df2 = (n2 - 1) as f64;
if f < 1.0 {
f = 1.0 / f;
std::mem::swap(&mut df1, &mut df2);
}
let dist = match FisherSnedecor::new(df1, df2) {
Ok(d) => d,
Err(_) => {
return CalcResult::new_error(
Error::NUM,
cell,
"Invalid parameters for F distribution in F.TEST".to_string(),
);
}
};
// One-tailed right-tail probability
let tail = 1.0 - dist.cdf(f);
// F.TEST is two-tailed: p = 2 * tail (with F >= 1)
let mut p = 2.0 * tail;
// Clamp tiny FP noise
if p < 0.0 && p > -1e-15 {
p = 0.0;
}
if p > 1.0 && p < 1.0 + 1e-15 {
p = 1.0;
}
CalcResult::Number(p)
}
}

View File

@@ -0,0 +1,39 @@
use statrs::distribution::{ContinuousCDF, Normal};
use crate::expressions::token::Error;
use crate::expressions::types::CellReferenceIndex;
use crate::{calc_result::CalcResult, expressions::parser::Node, model::Model};
impl Model {
pub(crate) fn fn_gauss(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let z = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(s) => return s,
};
let dist = match Normal::new(0.0, 1.0) {
Ok(d) => d,
Err(_) => {
return CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Failed to construct standard normal distribution".to_string(),
}
}
};
let result = dist.cdf(z) - 0.5;
if !result.is_finite() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid result for GAUSS".to_string(),
};
}
CalcResult::Number(result)
}
}

View File

@@ -1,12 +1,14 @@
mod beta;
mod binom;
mod chisq;
mod correl;
mod count_and_average;
mod covariance;
mod devsq;
mod exponential;
mod fisher;
mod gamma;
mod gauss;
mod geomean;
mod hypegeom;
mod if_ifs;
@@ -15,6 +17,7 @@ mod normal;
mod pearson;
mod phi;
mod poisson;
mod rank_eq_avg;
mod standard_dev;
mod standardize;
mod t_dist;

View File

@@ -1,5 +1,4 @@
use crate::expressions::types::CellReferenceIndex;
use crate::functions::statistical::chisq::is_same_shape_or_1d;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
@@ -7,176 +6,9 @@ use crate::{
impl Model {
// PEARSON(array1, array2)
pub(crate) fn fn_pearson(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let left_arg = self.evaluate_node_in_context(&args[0], cell);
let right_arg = self.evaluate_node_in_context(&args[1], cell);
let (values_left, values_right) = match (left_arg, right_arg) {
(
CalcResult::Range {
left: l1,
right: r1,
},
CalcResult::Range {
left: l2,
right: r2,
},
) => {
if l1.sheet != l2.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
let rows1 = r1.row - l1.row + 1;
let cols1 = r1.column - l1.column + 1;
let rows2 = r2.row - l2.row + 1;
let cols2 = r2.column - l2.column + 1;
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges must be of the same shape".to_string(),
);
}
let values_left = match self.values_from_range(l1, r1) {
Err(error) => return error,
Ok(v) => v,
};
let values_right = match self.values_from_range(l2, r2) {
Err(error) => return error,
Ok(v) => v,
};
(values_left, values_right)
}
(
CalcResult::Array(left),
CalcResult::Range {
left: l2,
right: r2,
},
) => {
let rows2 = r2.row - l2.row + 1;
let cols2 = r2.column - l2.column + 1;
let rows1 = left.len() as i32;
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return CalcResult::new_error(
Error::VALUE,
cell,
"Array and range must be of the same shape".to_string(),
);
}
let values_left = match self.values_from_array(left) {
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in first array: {:?}", error),
);
}
Ok(v) => v,
};
let values_right = match self.values_from_range(l2, r2) {
Err(error) => return error,
Ok(v) => v,
};
(values_left, values_right)
}
(
CalcResult::Range {
left: l1,
right: r1,
},
CalcResult::Array(right),
) => {
let rows1 = r1.row - l1.row + 1;
let cols1 = r1.column - l1.column + 1;
let rows2 = right.len() as i32;
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return CalcResult::new_error(
Error::VALUE,
cell,
"Range and array must be of the same shape".to_string(),
);
}
let values_left = match self.values_from_range(l1, r1) {
Err(error) => return error,
Ok(v) => v,
};
let values_right = match self.values_from_array(right) {
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in second array: {:?}", error),
);
}
Ok(v) => v,
};
(values_left, values_right)
}
(CalcResult::Array(left), CalcResult::Array(right)) => {
let rows1 = left.len() as i32;
let rows2 = right.len() as i32;
let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 };
let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 };
if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) {
return CalcResult::new_error(
Error::VALUE,
cell,
"Arrays must be of the same shape".to_string(),
);
}
let values_left = match self.values_from_array(left) {
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in first array: {:?}", error),
);
}
Ok(v) => v,
};
let values_right = match self.values_from_array(right) {
Err(error) => {
return CalcResult::new_error(
Error::VALUE,
cell,
format!("Error in second array: {:?}", error),
);
}
Ok(v) => v,
};
(values_left, values_right)
}
_ => {
return CalcResult::new_error(
Error::VALUE,
cell,
"Both arguments must be ranges or arrays".to_string(),
);
}
let (_, _, values_left, values_right) = match self.fn_get_two_matrices(args, cell) {
Ok(result) => result,
Err(e) => return e,
};
// Flatten into (x, y) pairs, skipping non-numeric entries (None)
@@ -228,8 +60,54 @@ impl Model {
}
let denom = (denom_x * denom_y).sqrt();
let r = num / denom;
CalcResult::Number(r)
CalcResult::Number(num / denom)
}
// RSQ(array1, array2) = CORREL(array1, array2)^2
pub(crate) fn fn_rsq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (_rows, _cols, values1, values2) = match self.fn_get_two_matrices(args, cell) {
Ok(s) => s,
Err(e) => return e,
};
let mut n = 0.0_f64;
let mut sum_x = 0.0_f64;
let mut sum_y = 0.0_f64;
let mut sum_x2 = 0.0_f64;
let mut sum_y2 = 0.0_f64;
let mut sum_xy = 0.0_f64;
let len = values1.len().min(values2.len());
for i in 0..len {
if let (Some(x), Some(y)) = (values1[i], values2[i]) {
n += 1.0;
sum_x += x;
sum_y += y;
sum_x2 += x * x;
sum_y2 += y * y;
sum_xy += x * y;
}
}
if n < 2.0 {
return CalcResult::new_error(
Error::DIV,
cell,
"RSQ requires at least two numeric data points in each range".to_string(),
);
}
let num = n * sum_xy - sum_x * sum_y;
let denom_x = n * sum_x2 - sum_x * sum_x;
let denom_y = n * sum_y2 - sum_y * sum_y;
let denom = (denom_x * denom_y).sqrt();
if denom == 0.0 || !denom.is_finite() {
return CalcResult::new_error(Error::DIV, cell, "Division by zero in RSQ".to_string());
}
let r = num / denom;
CalcResult::Number(r * r)
}
}

View File

@@ -0,0 +1,202 @@
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl Model {
// Helper to collect numeric values from the 2nd argument of RANK.*
fn collect_rank_values(
&mut self,
arg: &Node,
cell: CellReferenceIndex,
) -> Result<Vec<f64>, CalcResult> {
let values = match self.evaluate_node_in_context(arg, cell) {
CalcResult::Array(array) => match self.values_from_array(array) {
Ok(v) => v,
Err(e) => {
return Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: format!("Unsupported array argument: {}", e),
})
}
},
CalcResult::Range { left, right } => self.values_from_range(left, right)?,
CalcResult::Boolean(value) => {
if !matches!(arg, Node::ReferenceKind { .. }) {
vec![Some(if value { 1.0 } else { 0.0 })]
} else {
return Err(CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Unsupported argument type".to_string(),
});
}
}
_ => {
return Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Unsupported argument type".to_string(),
})
}
};
let numeric_values: Vec<f64> = values.into_iter().flatten().collect();
Ok(numeric_values)
}
// RANK.EQ(number, ref, [order])
pub(crate) fn fn_rank_eq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !(2..=3).contains(&args.len()) {
return CalcResult::new_args_number_error(cell);
}
// number
let number = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
// ref
let mut values = match self.collect_rank_values(&args[1], cell) {
Ok(v) => v,
Err(e) => return e,
};
if values.is_empty() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "No numeric values for RANK.EQ".to_string(),
};
}
// order: default 0 (descending)
let order = if args.len() == 2 {
0.0
} else {
match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
}
};
values.retain(|v| !v.is_nan());
// "better" = greater (descending) or smaller (ascending)
let mut better = 0;
let mut equal = 0;
if order == 0.0 {
// descending
for v in &values {
if *v > number {
better += 1;
} else if *v == number {
equal += 1;
}
}
} else {
// ascending
for v in &values {
if *v < number {
better += 1;
} else if *v == number {
equal += 1;
}
}
}
if equal == 0 {
return CalcResult::Error {
error: Error::NA,
origin: cell,
message: "Number not found in reference for RANK.EQ".to_string(),
};
}
let rank = (better as f64) + 1.0;
CalcResult::Number(rank)
}
// RANK.AVG(number, ref, [order])
pub(crate) fn fn_rank_avg(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !(2..=3).contains(&args.len()) {
return CalcResult::new_args_number_error(cell);
}
// number
let number = match self.get_number_no_bools(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
// ref
let mut values = match self.collect_rank_values(&args[1], cell) {
Ok(v) => v,
Err(e) => return e,
};
if values.is_empty() {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "No numeric values for RANK.AVG".to_string(),
};
}
// order: default 0 (descending)
let order = if args.len() == 2 {
0.0
} else {
match self.get_number_no_bools(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
}
};
values.retain(|v| !v.is_nan());
// > or < depending on order
let mut better = 0;
let mut equal = 0;
if order == 0.0 {
// descending
for v in &values {
if *v > number {
better += 1;
} else if *v == number {
equal += 1;
}
}
} else {
// ascending
for v in &values {
if *v < number {
better += 1;
} else if *v == number {
equal += 1;
}
}
}
if equal == 0 {
return CalcResult::Error {
error: Error::NA,
origin: cell,
message: "Number not found in reference for RANK.AVG".to_string(),
};
}
// For ties, average of the ranks. If the equal values occupy positions
// (better+1) ..= (better+equal), the average is:
// better + (equal + 1) / 2
let better_f = better as f64;
let equal_f = equal as f64;
let rank = better_f + (equal_f + 1.0) / 2.0;
CalcResult::Number(rank)
}
}

View File

@@ -17,7 +17,7 @@ fn mean(xs: &[f64]) -> f64 {
s / (n as f64)
}
fn sample_var(xs: &[f64]) -> f64 {
pub(crate) fn sample_var(xs: &[f64]) -> f64 {
let n = xs.len();
if n < 2 {
return 0.0;

View File

@@ -1,4 +1,5 @@
mod test_actions;
mod test_arabic_roman;
mod test_binary_search;
mod test_cell;
mod test_cell_clear_contents;
@@ -18,7 +19,6 @@ mod test_fn_concatenate;
mod test_fn_count;
mod test_fn_day;
mod test_fn_exact;
mod test_fn_fact;
mod test_fn_financial;
mod test_fn_formulatext;
mod test_fn_if;
@@ -40,12 +40,14 @@ mod test_metadata;
mod test_model_cell_clear_all;
mod test_model_is_empty_cell;
mod test_move_formula;
mod test_mround_trunc_int;
mod test_quote_prefix;
mod test_row_column_styles;
mod test_set_user_input;
mod test_sheet_markup;
mod test_sheets;
mod test_styles;
mod test_sumsq;
mod test_trigonometric;
mod test_true_false;
mod test_weekday_return_types;
@@ -66,6 +68,7 @@ mod test_escape_quotes;
mod test_even_odd;
mod test_exp_sign;
mod test_extend;
mod test_floor;
mod test_fn_datevalue_timevalue;
mod test_fn_fv;
mod test_fn_round;

View File

@@ -7,7 +7,9 @@ mod test_fn_covariance;
mod test_fn_devsq;
mod test_fn_expon_dist;
mod test_fn_f;
mod test_fn_f_test;
mod test_fn_fisher;
mod test_fn_gauss;
mod test_fn_hyp_geom_dist;
mod test_fn_log_norm;
mod test_fn_norm_dist;

View File

@@ -0,0 +1,35 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn test_fn_f_test_sanity() {
let mut model = new_empty_model();
// Valid call
model._set("A1", "=F.TEST(A2:A7, B2:B7)");
model._set("A2", "9");
model._set("A3", "12");
model._set("A4", "14");
model._set("A5", "16");
model._set("A6", "18");
model._set("A7", "20");
model._set("B2", "11");
model._set("B3", "10");
model._set("B4", "15");
model._set("B5", "17");
model._set("B6", "19");
model._set("B7", "21");
// Too few args
model._set("A8", "=F.TEST(A2:A7)");
// Too many args
model._set("A9", "=F.TEST(A2:A7, B2:B7, C2:C7)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"0.859284302");
assert_eq!(model._get_text("A8"), *"#ERROR!");
assert_eq!(model._get_text("A9"), *"#ERROR!");
}

View File

@@ -0,0 +1,35 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn test_fn_gauss_smoke() {
let mut model = new_empty_model();
model._set("A1", "=GAUSS(-3)");
model._set("A2", "=GAUSS(-2.3)");
model._set("A3", "=GAUSS(-1.7)");
model._set("A4", "=GAUSS(0)");
model._set("A5", "=GAUSS(0.5)");
model._set("A6", "=GAUSS(1)");
model._set("A7", "=GAUSS(1.3)");
model._set("A8", "=GAUSS(3)");
model._set("A9", "=GAUSS(4)");
model._set("G6", "=GAUSS()");
model._set("G7", "=GAUSS(1, 1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"-0.498650102");
assert_eq!(model._get_text("A2"), *"-0.48927589");
assert_eq!(model._get_text("A3"), *"-0.455434537");
assert_eq!(model._get_text("A4"), *"0");
assert_eq!(model._get_text("A5"), *"0.191462461");
assert_eq!(model._get_text("A6"), *"0.341344746");
assert_eq!(model._get_text("A7"), *"0.403199515");
assert_eq!(model._get_text("A8"), *"0.498650102");
assert_eq!(model._get_text("A9"), *"0.499968329");
assert_eq!(model._get_text("G6"), *"#ERROR!");
assert_eq!(model._get_text("G7"), *"#ERROR!");
}

View File

@@ -6,8 +6,8 @@ use crate::test::util::new_empty_model;
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=ARABIC()");
model._set("A2", "=ARABIC(V)");
model._set("A3", "=ARABIC(V, 2)");
model._set("A2", "=ARABIC(\"V\")");
model._set("A3", "=ARABIC(\"V\", 2)");
model._set("A4", "=ROMAN()");
model._set("A5", "=ROMAN(5)");

View File

@@ -0,0 +1,35 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=CELL("address",A1)");
model._set("A2", "=CELL()");
model._set("A3", "=INFO("system")");
model._set("A4", "=INFO()");
model._set("A5", "=N(TRUE)");
model._set("A6", "=N()");
model._set("A7", "=N(1, 2)");
model._set("A8", "=SHEETS()");
model._set("A9", "=SHEETS(1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"$A$1");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"#N/IMPL!");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"1");
assert_eq!(model._get_text("A6"), *"#ERROR!");
assert_eq!(model._get_text("A7"), *"#ERROR!");
assert_eq!(model._get_text("A8"), *"1");
assert_eq!(model._get_text("A9"), *"#N/IMPL!");
}

123
base/src/test/test_floor.rs Normal file
View File

@@ -0,0 +1,123 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn test_floor_floating_point_precision() {
// This test specifically checks the floating-point precision bug fix
// Bug: FLOOR(7.1, 0.1) was returning 7.0 instead of 7.1
let mut model = new_empty_model();
// FLOOR tests
model._set("C5", "=FLOOR(7.1, 0.1)");
model._set("H7", "=FLOOR(-7.1, -0.1)");
// FLOOR.PRECISE tests
model._set("C53", "=FLOOR.PRECISE(7.1, 0.1)");
model._set("H53", "=FLOOR.PRECISE(7.1, -0.1)");
// FLOOR.MATH tests
model._set("C101", "=FLOOR.MATH(7.1, 0.1)");
model._set("H101", "=FLOOR.MATH(7.1, -0.1)");
model.evaluate();
// All should return 7.1
assert_eq!(model._get_text("C5"), *"7.1");
assert_eq!(model._get_text("H7"), *"-7.1");
assert_eq!(model._get_text("C53"), *"7.1");
assert_eq!(model._get_text("H53"), *"7.1");
assert_eq!(model._get_text("C101"), *"7.1");
assert_eq!(model._get_text("H101"), *"7.1");
}
#[test]
fn test_floor_additional_precision_cases() {
let mut model = new_empty_model();
model._set("A1", "=FLOOR(7.9, 0.1)");
model._set("A2", "=FLOOR(2.6, 0.5)");
model._set("A3", "=FLOOR(0.3, 0.1)"); // 0.1 + 0.2 type scenario
model.evaluate();
assert_eq!(model._get_text("A1"), *"7.9");
assert_eq!(model._get_text("A2"), *"2.5");
assert_eq!(model._get_text("A3"), *"0.3");
}
#[test]
fn test_floor_basic_cases() {
let mut model = new_empty_model();
model._set("A1", "=FLOOR(3.7, 2)");
model._set("A2", "=FLOOR(3.2, 1)");
model._set("A3", "=FLOOR(10, 3)");
model._set("A4", "=FLOOR(7, 2)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"2");
assert_eq!(model._get_text("A2"), *"3");
assert_eq!(model._get_text("A3"), *"9");
assert_eq!(model._get_text("A4"), *"6");
}
#[test]
fn test_floor_negative_numbers() {
let mut model = new_empty_model();
// Both negative: rounds toward zero
model._set("A1", "=FLOOR(-2.5, -2)");
model._set("A2", "=FLOOR(-11, -3)");
// Negative number, positive significance: rounds away from zero
model._set("A3", "=FLOOR(-11, 3)");
model._set("A4", "=FLOOR(-2.5, 2)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"-2");
assert_eq!(model._get_text("A2"), *"-9");
assert_eq!(model._get_text("A3"), *"-12");
assert_eq!(model._get_text("A4"), *"-4");
}
#[test]
fn test_floor_error_cases() {
let mut model = new_empty_model();
// Positive number with negative significance should error
model._set("A1", "=FLOOR(2.5, -2)");
model._set("A2", "=FLOOR(10, -3)");
// Division by zero
model._set("A3", "=FLOOR(5, 0)");
// Wrong number of arguments
model._set("A4", "=FLOOR(5)");
model._set("A5", "=FLOOR(5, 1, 1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#NUM!");
assert_eq!(model._get_text("A2"), *"#NUM!");
assert_eq!(model._get_text("A3"), *"#DIV/0!");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"#ERROR!");
}
#[test]
fn test_floor_edge_cases() {
let mut model = new_empty_model();
// Zero value
model._set("A1", "=FLOOR(0, 5)");
model._set("A2", "=FLOOR(0, 0)");
// Exact multiples
model._set("A3", "=FLOOR(10, 5)");
model._set("A4", "=FLOOR(9, 3)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"0");
assert_eq!(model._get_text("A2"), *"0");
assert_eq!(model._get_text("A3"), *"10");
assert_eq!(model._get_text("A4"), *"9");
}

View File

@@ -1,29 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn large_numbers() {
let mut model = new_empty_model();
model._set("A1", "=FACT(170)");
model._set("A2", "=FACTDOUBLE(36)");
model._set("B1", "=FACT(6)");
model._set("B2", "=FACTDOUBLE(6)");
model._set("C3", "=FACTDOUBLE(15)");
model._set("F3", "=FACT(-0.1)");
model._set("F4", "=FACT(0)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"7.25742E+306");
assert_eq!(model._get_text("A2"), *"1.67834E+21");
assert_eq!(model._get_text("B1"), *"720");
assert_eq!(model._get_text("B2"), *"48");
assert_eq!(model._get_text("C3"), *"2027025");
assert_eq!(model._get_text("F3"), *"#NUM!");
assert_eq!(model._get_text("F4"), *"1");
}

View File

@@ -20,7 +20,6 @@ fn arguments() {
model._set("A11", "=INT(10.22, 1)");
model._set("A12", "=INT(10.22, 1, 2)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
@@ -29,7 +28,7 @@ fn arguments() {
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"#ERROR!");
assert_eq!(model._get_text("A6"), *"#ERROR!");
assert_eq!(model._get_text("A6"), *"10");
assert_eq!(model._get_text("A7"), *"10.2");
assert_eq!(model._get_text("A8"), *"#ERROR!");

View File

@@ -7,15 +7,24 @@ const TIMESTAMP_2023: i64 = 1679319865208;
#[test]
fn arguments() {
mock_time::set_mock_time(TIMESTAMP_2023);
let mut model = new_empty_model();
model._set("A1", "=NOW(1)");
model._set("A1", "=NOW(1, 1)");
model._set("A2", "=NOW(\"Europe/Berlin\")");
model._set("A3", "=NOW(\"faketimezone\")");
model.evaluate();
assert_eq!(
model._get_text("A1"),
"#ERROR!",
"NOW should not accept arguments"
"Wrong number of arguments"
);
assert_eq!(model._get_text("A2"), *"20/03/2023 14:44:25");
assert_eq!(
model._get_text("A3"),
"#VALUE!",
"Invalid timezone: faketimezone"
);
}

View File

@@ -0,0 +1,17 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=SUMSQ()");
model._set("A2", "=SUMSQ(2)");
model._set("A3", "=SUMSQ(1, 2)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"4");
assert_eq!(model._get_text("A3"), *"5");
}

830
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| Function | Status | Documentation |
| ---------- | ---------------------------------------------- | ------------- |
| CELL | <Badge type="info" text="Not implemented yet" /> | |
| CELL | <Badge type="tip" text="Available" /> | |
| ERROR.TYPE | <Badge type="tip" text="Available" /> | |
| INFO | <Badge type="info" text="Not implemented yet" /> | |
| ISBLANK | <Badge type="tip" text="Available" /> | |
@@ -20,7 +20,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| ISEVEN | <Badge type="tip" text="Available" /> | |
| ISFORMULA | <Badge type="tip" text="Available" /> | |
| ISLOGICAL | <Badge type="tip" text="Available" /> | |
| ISNA | <Badge type="info" text="Not implemented yet" /> | |
| ISNA | <Badge type="tip" text="Available" /> | |
| ISNONTEXT | <Badge type="tip" text="Available" /> | |
| ISNUMBER | <Badge type="tip" text="Available" /> | |
| ISODD | <Badge type="tip" text="Available" /> | |
@@ -30,5 +30,5 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| N | <Badge type="info" text="Not implemented yet" /> | |
| NA | <Badge type="tip" text="Available" /> | |
| SHEET | <Badge type="tip" text="Available" /> | |
| SHEETS | <Badge type="info" text="Not implemented yet" /> | |
| SHEETS | <Badge type="tip" text="Available" /> | |
| TYPE | <Badge type="tip" text="Available" /> | |

View File

@@ -7,6 +7,5 @@ lang: en-US
# CELL
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
:::

View File

@@ -7,6 +7,5 @@ lang: en-US
# N
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
:::

View File

@@ -7,6 +7,5 @@ lang: en-US
# SHEETS
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
:::

View File

@@ -6,26 +6,33 @@ lang: en-US
# COLUMN function
## Overview
The COLUMN Function in IronCalc is a lookup & reference formula that is used to query and return the column number of a referenced Column or Cell.
The COLUMN Function in IronCalc is a lookup & reference formula that is used to query and return the column number of a referenced column or cell.
## Usage
### Syntax
**COLUMN(<span title="Reference" style="color:#1E88E5">reference</span>) => <span title="Number" style="color:#1E88E5">column</span>**
### Argument descriptions
* *reference* ([cell](/features/value-types#references), [optional](/features/optional-arguments.md)). The number of the cell you wish to reference the column number of.
* *reference* ([cell](/features/value-types#references), [optional](/features/optional-arguments.md)). The cell, column, range, or [Named Range](/web-application/name-manager.html) for which you wish to find the column number.
### Additional guidance
* When referencing a range of cells, only the column number of the left most cell will be returned.
* When referencing a range of cells, only the column number of the leftmost cell will be returned.
* You are also able to reference complete columns instead of individual cells.
* When using a Named Range as a reference, the reference is not case sensitive.
* IronCalc supports the use of both *Absolute* ($A$1) and *Relative* (A1) references.
* Cross-sheet references are also supported.
### Returned value
COLUMN returns the [number](/features/value-types#numbers) of the specific cell or column which is being referenced.
COLUMN returns the [number](/features/value-types#numbers) of the specific cell or column which is being referenced. If no reference is included, the column number of the cell where the formula is entered will be returned.
### Error conditions
* IronCalc currently does not support the referencing of cells with names.
* A [#NAME?](/features/error-types.html#name) error is returned if a Named Range being referenced is deleted.
* A [#REF!](/features/error-types.html#ref) error is returned if a cell being referenced is deleted.
* A [#VALUE!](/features/error-types.html#value) error is returned if a column being referenced is deleted.
## Details
The COLUMN Function can only be used to display the correlating number of a single column within a Sheet. If you wish to show the number of columns used within a specific range, you can use the COLUMNS Function.
The COLUMN Function can only be used to display the correlating number of a single column within a Sheet. If you wish to show the number of columns used within a specific range, you can use the [COLUMNS](/functions/lookup_and_reference/columns) Function.
## Examples
### No Cell Reference
When no cell reference is made, the formula uses **=COLUMN()**. This will then output the column number of the cell where the formula is placed.<br><br>For example, if the formula is placed in cell A1, then "1" will be displayed.
When no cell reference is made, the formula uses **=COLUMN()**. This will output the column number of the cell where the formula is entered.<br><br>For example, if the formula is placed in cell A1, then "1" will be displayed.
### With Cell Reference
When a cell reference is made, the formula uses **=COLUMN([Referenced Cell])**. This will then output the column number of the referenced cell, regardless of where the formula is placed in the sheet.<br><br>For example, if the cell B1 is the referenced cell, "2" will be the output of the formula no matter where it is placed in the sheet.<br><br>**Note:** references do not always have to be specific cells, you can also reference complete columns. For example, **=COLUMN(B:B)** would also result in an output of "2".
When a cell reference is made, the formula uses **=COLUMN(<span title="Reference" style="color:#1E88E5">Referenced Cell</span>)**. This will then output the column number of the referenced cell, regardless of where the formula is placed in the sheet.<br><br>For example, if B1 is the referenced cell, then "2" will be the output of the formula, regardless of where the formula is placed in the sheet.<br><br>**Note:** references do not have to be specific cells, you can also reference complete columns. For example, **=COLUMN(B:B)** would also result in an output of "2".
### Range References
The COLUMN function can also be used to reference a range of Cells or Columns. In this case only the most left-hand column will be the resulting output.<br><br>For example, **=COLUMN(A1:J1)** will result in the ouput of "1".
## Links
The COLUMN function can also be used to reference a range of cells or columns. In this case only the leftmost column will be the resulting output.<br><br>For example, **=COLUMN(A1:J1)** will result in the output of "1".
## Links
* Visit Microsoft Excel's [Column function](https://support.microsoft.com/en-us/office/column-function-44e8c754-711c-4df3-9da4-47a55042554b) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093373) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/COLUMN) provide versions of the COLUMN function.

View File

@@ -86,7 +86,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| SUMIF | <Badge type="tip" text="Available" /> | |
| SUMIFS | <Badge type="tip" text="Available" /> | |
| SUMPRODUCT | <Badge type="info" text="Not implemented yet" /> | |
| SUMSQ | <Badge type="info" text="Not implemented yet" /> | |
| SUMSQ | <Badge type="tip" text="Available" /> | |
| SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | |
| SUMX2PY2 | <Badge type="info" text="Not implemented yet" /> | |
| SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | |

View File

@@ -7,6 +7,5 @@ lang: en-US
# SUMSQ
::: warning
🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
:::

View File

@@ -1,5 +1,7 @@
import type { Model } from "@ironcalc/wasm";
import { styled } from "@mui/material";
import { ChevronDown } from "lucide-react";
import { useState } from "react";
import { Fx } from "../../icons";
import { theme } from "../../theme";
import { FORMULA_BAR_HEIGHT } from "../constants";
@@ -9,6 +11,7 @@ import {
ROW_HEIGH_SCALE,
} from "../WorksheetCanvas/constants";
import type { WorkbookState } from "../workbookState";
import FormulaBarMenu from "./FormulaBarMenu";
type FormulaBarProps = {
cellAddress: string;
@@ -17,6 +20,8 @@ type FormulaBarProps = {
workbookState: WorkbookState;
onChange: () => void;
onTextUpdated: () => void;
openDrawer: () => void;
canEdit: boolean;
};
function FormulaBar(properties: FormulaBarProps) {
@@ -28,10 +33,27 @@ function FormulaBar(properties: FormulaBarProps) {
onTextUpdated,
workbookState,
} = properties;
const [isMenuOpen, setIsMenuOpen] = useState(false);
const handleMenuOpenChange = (isOpen: boolean): void => {
setIsMenuOpen(isOpen);
};
return (
<Container>
<AddressContainer>
<CellBarAddress>{cellAddress}</CellBarAddress>
<AddressContainer $active={isMenuOpen}>
<FormulaBarMenu
onMenuOpenChange={handleMenuOpenChange}
openDrawer={properties.openDrawer}
canEdit={properties.canEdit}
model={model}
onUpdate={onChange}
>
<CellBarAddress>{cellAddress}</CellBarAddress>
<StyledIcon>
<ChevronDown size={16} />
</StyledIcon>
</FormulaBarMenu>
</AddressContainer>
<Divider />
<FormulaContainer>
@@ -101,7 +123,7 @@ const Divider = styled("div")`
background-color: ${theme.palette.grey["300"]};
min-width: 1px;
height: 16px;
margin: 0px 16px;
margin: 0px 16px 0px 8px;
`;
const FormulaContainer = styled("div")`
@@ -123,20 +145,43 @@ const Container = styled("div")`
height: ${FORMULA_BAR_HEIGHT}px;
`;
const AddressContainer = styled("div")`
padding-left: 16px;
const AddressContainer = styled("div")<{ $active?: boolean }>`
color: ${theme.palette.common.black};
font-style: normal;
font-weight: normal;
font-size: 12px;
display: flex;
font-weight: 600;
flex-grow: row;
align-items: center;
gap: 2px;
border-radius: 4px;
margin-left: 8px;
cursor: pointer;
background-color: ${(props) =>
props.$active ? theme.palette.action.selected : "transparent"};
&:hover {
background-color: ${(props) =>
props.$active ? theme.palette.action.selected : theme.palette.grey["100"]};
}
`;
const CellBarAddress = styled("div")`
width: 100%;
text-align: "center";
box-sizing: border-box;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding-left: 8px;
background-color: transparent;
`;
const StyledIcon = styled("div")`
display: flex;
align-items: center;
justify-content: center;
padding: 4px 2px;
background-color: transparent;
`;
const EditorWrapper = styled("div")`

View File

@@ -0,0 +1,170 @@
import type { Model } from "@ironcalc/wasm";
import { Menu, MenuItem, styled } from "@mui/material";
import { Tag } from "lucide-react";
import { useCallback, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { theme } from "../../theme";
import { parseRangeInSheet } from "../Editor/util";
type FormulaBarMenuProps = {
children: React.ReactNode;
onMenuOpenChange: (isOpen: boolean) => void;
openDrawer: () => void;
canEdit: boolean;
model: Model;
onUpdate: () => void;
};
const FormulaBarMenu = (properties: FormulaBarMenuProps) => {
const { t } = useTranslation();
const [isMenuOpen, setMenuOpen] = useState(false);
const anchorElement = useRef<HTMLDivElement>(null);
const handleMenuOpen = useCallback((): void => {
setMenuOpen(true);
properties.onMenuOpenChange(true);
}, [properties.onMenuOpenChange]);
const handleMenuClose = useCallback((): void => {
setMenuOpen(false);
properties.onMenuOpenChange(false);
}, [properties.onMenuOpenChange]);
const definedNameList = properties.model.getDefinedNameList();
return (
<>
<ChildrenWrapper onClick={handleMenuOpen} ref={anchorElement}>
{properties.children}
</ChildrenWrapper>
<StyledMenu
open={isMenuOpen}
onClose={handleMenuClose}
anchorEl={anchorElement.current}
marginThreshold={0}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
>
{definedNameList.length > 0 ? (
<>
{definedNameList.map((definedName) => {
return (
<MenuItemWrapper
key={`${definedName.name}-${definedName.scope}`}
disableRipple
onClick={() => {
// select the area corresponding to the defined name
const formula = definedName.formula;
const range = parseRangeInSheet(properties.model, formula);
if (range) {
const [
sheetIndex,
rowStart,
columnStart,
rowEnd,
columnEnd,
] = range;
properties.model.setSelectedSheet(sheetIndex);
properties.model.setSelectedCell(rowStart, columnStart);
properties.model.setSelectedRange(
rowStart,
columnStart,
rowEnd,
columnEnd,
);
}
properties.onUpdate();
handleMenuClose();
}}
>
<Tag />
<MenuItemText>{definedName.name}</MenuItemText>
<MenuItemExample>{definedName.formula}</MenuItemExample>
</MenuItemWrapper>
);
})}
<MenuDivider />
</>
) : null}
<MenuItemWrapper
onClick={() => {
properties.openDrawer();
handleMenuClose();
}}
disabled={!properties.canEdit}
disableRipple
>
<MenuItemText>{t("formula_bar.manage_named_ranges")}</MenuItemText>
</MenuItemWrapper>
</StyledMenu>
</>
);
};
const StyledMenu = styled(Menu)`
top: 4px;
min-width: 260px;
max-width: 460px;
& .MuiPaper-root {
border-radius: 8px;
padding: 4px 0px;
margin-left: -4px;
}
& .MuiList-root {
padding: 0;
}
`;
const MenuItemWrapper = styled(MenuItem)`
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
gap: 8px;
width: calc(100% - 8px);
min-width: 172px;
margin: 0px 4px;
border-radius: 4px;
padding: 8px;
height: 32px;
& svg {
width: 12px;
height: 12px;
flex-shrink: 0;
color: ${theme.palette.grey[600]};
}
`;
const ChildrenWrapper = styled("div")`
display: flex;
`;
const MenuDivider = styled("div")`
width: 100%;
margin: auto;
margin-top: 4px;
margin-bottom: 4px;
border-top: 1px solid ${theme.palette.grey[200]};
`;
const MenuItemText = styled("div")`
flex: 1;
min-width: 0;
color: ${theme.palette.common.black};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const MenuItemExample = styled("div")`
color: ${theme.palette.grey[400]};
margin-left: 12px;
`;
export default FormulaBarMenu;

View File

@@ -398,7 +398,9 @@ const ListItem = styled("div")<{ $isSelected: boolean }>(({ $isSelected }) => ({
display: "flex",
alignItems: "flex-start",
justifyContent: "space-between",
gap: "8px",
padding: "8px 12px",
cursor: "pointer",
minHeight: "40px",
boxSizing: "border-box",
borderBottom: `1px solid ${theme.palette.grey[200]}`,
@@ -438,6 +440,8 @@ const NameText = styled("span")({
fontSize: "12px",
color: theme.palette.common.black,
fontWeight: 600,
wordBreak: "break-all",
overflowWrap: "break-word",
});
const IconsWrapper = styled("div")({

View File

@@ -33,7 +33,6 @@ import {
Redo2,
RemoveFormatting,
Strikethrough,
Tags,
Type,
Underline,
Undo2,
@@ -87,7 +86,6 @@ type ToolbarProperties = {
numFmt: string;
showGridLines: boolean;
onToggleShowGridLines: (show: boolean) => void;
openDrawer: () => void;
};
function Toolbar(properties: ToolbarProperties) {
@@ -514,18 +512,6 @@ function Toolbar(properties: ToolbarProperties) {
{properties.showGridLines ? <Grid2x2Check /> : <Grid2x2X />}
</StyledButton>
</Tooltip>
<Tooltip title={t("toolbar.named_ranges")}>
<StyledButton
type="button"
$pressed={false}
onClick={() => {
properties.openDrawer();
}}
disabled={!canEdit}
>
<Tags />
</StyledButton>
</Tooltip>
<Tooltip title={t("toolbar.selected_png")}>
<StyledButton
type="button"

View File

@@ -665,9 +665,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
model.setShowGridLines(sheet, show);
setRedrawId((id) => id + 1);
}}
openDrawer={() => {
setDrawerOpen(true);
}}
/>
<WorksheetAreaLeft $drawerWidth={isDrawerOpen ? drawerWidth : 0}>
<FormulaBar
@@ -682,6 +679,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}}
model={model}
workbookState={workbookState}
openDrawer={() => {
setDrawerOpen(true);
}}
canEdit={true}
/>
<Worksheet
model={model}
@@ -764,7 +765,7 @@ const WorksheetAreaLeft = styled("div")<WorksheetAreaLeftProps>(
position: "absolute",
top: `${TOOLBAR_HEIGHT + 1}px`,
width: `calc(100% - ${$drawerWidth}px)`,
height: `calc(100% - ${TOOLBAR_HEIGHT + 1}px)`,
height: `calc(100% - ${TOOLBAR_HEIGHT}px)`,
}),
);

View File

@@ -1,5 +1,6 @@
import type { CellStyle, Model } from "@ironcalc/wasm";
import { columnNameFromNumber } from "@ironcalc/wasm";
import { theme } from "../../theme";
import { getColor } from "../Editor/util";
import type { Cell } from "../types";
import type { WorkbookState } from "../workbookState";
@@ -721,7 +722,7 @@ export default class WorksheetCanvas {
const style = this.model.getCellStyle(selectedSheet, row, column);
// first the background
let backgroundColor = "#FFFFFF";
let backgroundColor = theme.palette.common.white;
if (style.fill.fg_color) {
backgroundColor = style.fill.fg_color;
}
@@ -1036,14 +1037,21 @@ export default class WorksheetCanvas {
width: number,
div: HTMLDivElement,
selected: boolean,
isFullColumnSelected: boolean,
): void {
div.style.boxSizing = "border-box";
div.style.width = `${width}px`;
div.style.height = `${headerRowHeight}px`;
div.style.backgroundColor = selected
? headerSelectedBackground
? isFullColumnSelected
? theme.palette.primary.main
: headerSelectedBackground
: headerBackground;
div.style.color = selected ? headerSelectedColor : headerTextColor;
div.style.color = selected
? isFullColumnSelected
? theme.palette.common.white
: headerSelectedColor
: headerTextColor;
div.style.fontWeight = "bold";
div.style.borderLeft = `1px solid ${headerBorderColor}`;
div.style.borderTop = `1px solid ${headerBorderColor}`;
@@ -1071,9 +1079,15 @@ export default class WorksheetCanvas {
const { sheet: selectedSheet, range } = this.model.getSelectedView();
let rowStart = range[0];
let rowEnd = range[2];
let columnStart = range[1];
let columnEnd = range[3];
if (rowStart > rowEnd) {
[rowStart, rowEnd] = [rowEnd, rowStart];
}
if (columnStart > columnEnd) {
[columnStart, columnEnd] = [columnEnd, columnStart];
}
const isFullRowSelected = columnStart === 1 && columnEnd === LAST_COLUMN;
const context = this.ctx;
let topLeftCornerY = headerRowHeight + 0.5;
@@ -1085,7 +1099,9 @@ export default class WorksheetCanvas {
context.fillStyle = headerBorderColor;
context.fillRect(0.5, topLeftCornerY, headerColumnWidth, rowHeight);
context.fillStyle = selected
? headerSelectedBackground
? isFullRowSelected
? theme.palette.primary.main
: headerSelectedBackground
: headerBackground;
context.fillRect(
0.5,
@@ -1097,7 +1113,11 @@ export default class WorksheetCanvas {
context.fillStyle = outlineColor;
context.fillRect(headerColumnWidth - 1, topLeftCornerY, 1, rowHeight);
}
context.fillStyle = selected ? headerSelectedColor : headerTextColor;
context.fillStyle = selected
? isFullRowSelected
? theme.palette.common.white
: headerSelectedColor
: headerTextColor;
context.font = `bold 12px ${defaultCellFontFamily}`;
context.fillText(
`${row}`,
@@ -1122,11 +1142,17 @@ export default class WorksheetCanvas {
const { columnHeaders } = this;
let deltaX = 0;
const { range } = this.model.getSelectedView();
let rowStart = range[0];
let rowEnd = range[2];
let columnStart = range[1];
let columnEnd = range[3];
if (columnStart > columnEnd) {
[columnStart, columnEnd] = [columnEnd, columnStart];
}
if (rowStart > rowEnd) {
[rowStart, rowEnd] = [rowEnd, rowStart];
}
const isFullColumnSelected = rowStart === 1 && rowEnd === LAST_ROW;
for (const header of columnHeaders.querySelectorAll(".column-header"))
header.remove();
for (const handle of columnHeaders.querySelectorAll(
@@ -1146,7 +1172,12 @@ export default class WorksheetCanvas {
// Frozen headers
for (let column = 1; column <= frozenColumns; column += 1) {
const selected = column >= columnStart && column <= columnEnd;
deltaX += this.addColumnHeader(deltaX, column, selected);
deltaX += this.addColumnHeader(
deltaX,
column,
selected,
isFullColumnSelected,
);
}
if (frozenColumns !== 0) {
@@ -1162,7 +1193,12 @@ export default class WorksheetCanvas {
for (let column = firstColumn; column <= lastColumn; column += 1) {
const selected = column >= columnStart && column <= columnEnd;
deltaX += this.addColumnHeader(deltaX, column, selected);
deltaX += this.addColumnHeader(
deltaX,
column,
selected,
isFullColumnSelected,
);
}
columnHeaders.style.width = `${deltaX}px`;
@@ -1172,6 +1208,7 @@ export default class WorksheetCanvas {
deltaX: number,
column: number,
selected: boolean,
isFullColumnSelected: boolean,
): number {
const columnWidth = this.getColumnWidth(
this.model.getSelectedSheet(),
@@ -1182,7 +1219,7 @@ export default class WorksheetCanvas {
div.textContent = columnNameFromNumber(column);
this.columnHeaders.insertBefore(div, null);
this.styleColumnHeader(columnWidth, div, selected);
this.styleColumnHeader(columnWidth, div, selected, isFullColumnSelected);
this.addColumnResizeHandle(deltaX + columnWidth, column, columnWidth);
return columnWidth;
}

View File

@@ -92,6 +92,9 @@
"label": "Formula",
"title": "Update formula"
},
"formula_bar": {
"manage_named_ranges": "Manage Named Ranges"
},
"navigation": {
"add_sheet": "Add sheet",
"sheet_list": "Sheet list",

View File

@@ -91,6 +91,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -570,6 +571,7 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -613,6 +615,7 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -2026,6 +2029,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.4.tgz",
"integrity": "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -2122,6 +2126,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
@@ -2691,6 +2696,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -2758,6 +2764,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2767,6 +2774,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -2978,6 +2986,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -3023,6 +3032,7 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -3113,21 +3123,6 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true,
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
}
}
}

View File

@@ -227,16 +227,19 @@ const Wrapper = styled("div")`
`;
const DRAWER_WIDTH = 264;
const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 440;
export const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 768;
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
margin-left: ${({ isDrawerOpen }) => (isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`)};
width: ${({ isDrawerOpen }) => (isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : "100%")};
margin-left: ${({ isDrawerOpen }) =>
isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`};
width: ${({ isDrawerOpen }) =>
isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : "100%"};
display: flex;
flex-direction: column;
position: relative;
@media (max-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px) {
${({ isDrawerOpen }) => isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`}
${({ isDrawerOpen }) =>
isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`}
}
`;
@@ -247,7 +250,7 @@ const MobileOverlay = styled("div")`
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
z-index: 1;
z-index: 100;
cursor: pointer;
@media (min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE + 1}px) {

View File

@@ -3,6 +3,7 @@ import type { Model } from "@ironcalc/workbook";
import { IconButton, Tooltip } from "@mui/material";
import { CloudOff, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { useLayoutEffect, useRef, useState } from "react";
import { MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE } from "../App";
import { FileMenu } from "./FileMenu";
import { HelpMenu } from "./HelpMenu";
import { downloadModel } from "./rpc";
@@ -77,7 +78,7 @@ export function FileBar(properties: {
{properties.isDrawerOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
</DrawerButton>
</Tooltip>
{width > 440 && (
{width > MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE && (
<FileMenu
newModel={properties.newModel}
newModelFromTemplate={properties.newModelFromTemplate}
@@ -92,7 +93,7 @@ export function FileBar(properties: {
onDelete={properties.onDelete}
/>
)}
{width > 440 && <HelpMenu />}
{width > MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE && <HelpMenu />}
<WorkbookTitleWrapper>
<WorkbookTitle
name={properties.model.getName()}
@@ -112,7 +113,8 @@ export function FileBar(properties: {
<div style={{ fontWeight: "bold" }}>{cloudWarningText2}</div>
</div>
}
placement="bottom-start"
placement="bottom"
enterTouchDelay={0}
enterDelay={500}
slotProps={{
popper: {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.