Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4953ddd
update: add 3 more border styles
dg-ac Jan 24, 2026
27367b1
update: use a popper instead of a popover
dg-ac Jan 25, 2026
009b06e
update: add a timeout in nested menu
dg-ac Jan 25, 2026
6b1e9b7
chore: remove duplications in worksheetcanvas
dg-ac Jan 30, 2026
ebb0446
update: add 3 more styles
dg-ac Jan 30, 2026
88410df
fix: comments
dg-ac Feb 2, 2026
163fa3b
fix: treat leading comma as text
rezat-c Feb 4, 2026
8acef66
fix: add support for booleans and upper bound for df (#734 and #735)
elsaminsut Feb 3, 2026
af0a180
update: add unit test for bug fix
elsaminsut Feb 3, 2026
e22549c
fix: add max deg of freedom as constant
elsaminsut Feb 3, 2026
38c4787
update: adds locale testing for database functions
elsaminsut Jan 26, 2026
d6ea5e6
fix: changes to locale name, use correct formula delimitter for de
elsaminsut Jan 26, 2026
d4015cd
FIX: Cut and paste on partially selected range
nhatcher Feb 7, 2026
21631e0
FIX: Adds missing test
nhatcher Feb 7, 2026
893284d
Add combinatorial functions
BrianHung Jul 31, 2025
3ef9028
test coverage
BrianHung Jul 31, 2025
256134e
integer overflow underflow tests
BrianHung Jul 31, 2025
d817ebe
Fix formatting for CI
BrianHung Jan 31, 2026
48a6f9e
Remove accidentally committed xlsx test output files
BrianHung Feb 1, 2026
e5bddaa
Force GitHub to recalculate diff
BrianHung Feb 1, 2026
9f5ae28
Fix duplicate match arms and test module order for lint/CI
BrianHung Feb 11, 2026
80b0339
Fix COMBINA overflow: cap n/k and check infinity to prevent 6h test hang
BrianHung Feb 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions base/src/expressions/parser/static_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,11 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Round => args_signature_scalars(arg_count, 2, 0),
Function::Rounddown => args_signature_scalars(arg_count, 2, 0),
Function::Roundup => args_signature_scalars(arg_count, 2, 0),
Function::Fact => args_signature_scalars(arg_count, 1, 0),
Function::Combin => args_signature_scalars(arg_count, 2, 0),
Function::Combina => args_signature_scalars(arg_count, 2, 0),
Function::Permut => args_signature_scalars(arg_count, 2, 0),
Function::Permutationa => args_signature_scalars(arg_count, 2, 0),
Function::Sin => args_signature_scalars(arg_count, 1, 0),
Function::Sinh => args_signature_scalars(arg_count, 1, 0),
Function::Sqrt => args_signature_scalars(arg_count, 1, 0),
Expand Down Expand Up @@ -844,7 +849,6 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Sec => args_signature_scalars(arg_count, 1, 0),
Function::Sech => args_signature_scalars(arg_count, 1, 0),
Function::Exp => args_signature_scalars(arg_count, 1, 0),
Function::Fact => args_signature_scalars(arg_count, 1, 0),
Function::Factdouble => args_signature_scalars(arg_count, 1, 0),
Function::Sign => args_signature_scalars(arg_count, 1, 0),
Function::Radians => args_signature_scalars(arg_count, 1, 0),
Expand All @@ -869,8 +873,6 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Decimal => args_signature_scalars(arg_count, 2, 0),
Function::Roman => args_signature_scalars(arg_count, 1, 1),
Function::Arabic => args_signature_scalars(arg_count, 1, 0),
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),
Expand Down Expand Up @@ -1045,6 +1047,11 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Round => scalar_arguments(args),
Function::Rounddown => scalar_arguments(args),
Function::Roundup => scalar_arguments(args),
Function::Fact => scalar_arguments(args),
Function::Combin => scalar_arguments(args),
Function::Combina => scalar_arguments(args),
Function::Permut => scalar_arguments(args),
Function::Permutationa => scalar_arguments(args),
Function::Ln => scalar_arguments(args),
Function::Log => scalar_arguments(args),
Function::Log10 => scalar_arguments(args),
Expand Down Expand Up @@ -1238,7 +1245,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Sec => scalar_arguments(args),
Function::Sech => scalar_arguments(args),
Function::Exp => scalar_arguments(args),
Function::Fact => scalar_arguments(args),
Function::Factdouble => scalar_arguments(args),
Function::Sign => scalar_arguments(args),
Function::Radians => scalar_arguments(args),
Expand All @@ -1263,8 +1269,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Decimal => scalar_arguments(args),
Function::Roman => scalar_arguments(args),
Function::Arabic => scalar_arguments(args),
Function::Combin => scalar_arguments(args),
Function::Combina => scalar_arguments(args),
Function::Sumsq => StaticResult::Scalar,
Function::N => scalar_arguments(args),
Function::Sheets => scalar_arguments(args),
Expand Down
5 changes: 5 additions & 0 deletions base/src/formatter/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,11 @@ fn parse_number(
} else {
1.0
};

if bytes[position] == group_separator {
return Err("Cannot parse number".to_string());
}

// numbers before the decimal point
while position < len {
let x = bytes[position];
Expand Down
82 changes: 82 additions & 0 deletions base/src/functions/mathematical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1663,11 +1663,93 @@ impl<'a> Model<'a> {
message: "Arguments must be non-negative integers".to_string(),
};
}
// Avoid huge loops and overflow: Excel returns #NUM! for very large n or k
const MAX_N: f64 = 1e15;
const MAX_K: f64 = 1e6;
if n > MAX_N || k > MAX_K {
return CalcResult::new_error(Error::NUM, cell, "overflow".to_string());
}
let k = k as usize;
let mut result = 1.0;
for i in 0..k {
let t = i as f64;
result *= (n + t) / (t + 1.0);
if result.is_infinite() {
return CalcResult::new_error(Error::NUM, cell, "overflow".to_string());
}
}
CalcResult::Number(result)
}

pub(crate) fn fn_permut(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let n = match self.get_number(&args[0], cell) {
Ok(f) => f.floor(),
Err(s) => return s,
};
let k = match self.get_number(&args[1], cell) {
Ok(f) => f.floor(),
Err(s) => return s,
};
if n < 0.0 || k < 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Arguments must be non-negative integers".to_string(),
};
}
if k > n {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "k cannot be greater than n".to_string(),
};
}
let n = n as usize;
let k = k as usize;
let mut result = 1.0;
for i in 0..k {
result *= (n - i) as f64;
}
if result.is_infinite() {
return CalcResult::new_error(Error::NUM, cell, "overflow".to_string());
}
CalcResult::Number(result)
}

pub(crate) fn fn_permutationa(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let n = match self.get_number(&args[0], cell) {
Ok(f) => f.floor(),
Err(s) => return s,
};
let k = match self.get_number(&args[1], cell) {
Ok(f) => f.floor(),
Err(s) => return s,
};
if n < 0.0 || k < 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Arguments must be non-negative integers".to_string(),
};
}
let n = n as usize;
let k = k as usize;
let mut result = 1.0;
for _ in 0..k {
result *= n as f64;
}
if result.is_infinite() {
return CalcResult::new_error(Error::NUM, cell, "overflow".to_string());
}
CalcResult::Number(result)
}
Expand Down
21 changes: 17 additions & 4 deletions base/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ pub enum Function {
Arabic,
Combin,
Combina,
Permut,
Permutationa,
Sumsq,

// Information
Expand Down Expand Up @@ -428,6 +430,11 @@ macro_rules! impl_function_lookup {
impl Functions {
pub fn lookup(&self, name: &str) -> Option<Function> {
let key = name.to_uppercase();
match key.as_str() {
"PERMUT" => return Some(Function::Permut),
"PERMUTATIONA" => return Some(Function::Permutationa),
_ => {}
}
$(
if self.$field == key {
return Some(Function::$variant);
Expand Down Expand Up @@ -906,6 +913,8 @@ impl Function {
Function::Arabic => functions.arabic.clone(),
Function::Combin => functions.combin.clone(),
Function::Combina => functions.combina.clone(),
Function::Permut => "PERMUT".to_string(),
Function::Permutationa => "PERMUTATIONA".to_string(),
Function::Sumsq => functions.sumsq.clone(),
Function::ErrorType => functions.errortype.clone(),
Function::Formulatext => functions.formulatext.clone(),
Expand Down Expand Up @@ -1168,7 +1177,7 @@ impl Function {
Function::Steyx => functions.steyx.clone(),
}
}
pub fn into_iter() -> IntoIter<Function, 345> {
pub fn into_iter() -> IntoIter<Function, 347> {
[
Function::And,
Function::False,
Expand Down Expand Up @@ -1425,6 +1434,8 @@ impl Function {
Function::Arabic,
Function::Combin,
Function::Combina,
Function::Permut,
Function::Permutationa,
Function::Sumsq,
Function::N,
Function::Cell,
Expand Down Expand Up @@ -1701,6 +1712,11 @@ impl<'a> Model<'a> {
Function::Round => self.fn_round(args, cell),
Function::Rounddown => self.fn_rounddown(args, cell),
Function::Roundup => self.fn_roundup(args, cell),
Function::Fact => self.fn_fact(args, cell),
Function::Combin => self.fn_combin(args, cell),
Function::Combina => self.fn_combina(args, cell),
Function::Permut => self.fn_permut(args, cell),
Function::Permutationa => self.fn_permutationa(args, cell),
Function::Sum => self.fn_sum(args, cell),
Function::Sumif => self.fn_sumif(args, cell),
Function::Sumifs => self.fn_sumifs(args, cell),
Expand Down Expand Up @@ -1886,7 +1902,6 @@ impl<'a> Model<'a> {
Function::Sec => self.fn_sec(args, cell),
Function::Sech => self.fn_sech(args, cell),
Function::Exp => self.fn_exp(args, cell),
Function::Fact => self.fn_fact(args, cell),
Function::Factdouble => self.fn_factdouble(args, cell),
Function::Sign => self.fn_sign(args, cell),
Function::Radians => self.fn_radians(args, cell),
Expand All @@ -1911,8 +1926,6 @@ impl<'a> Model<'a> {
Function::Decimal => self.fn_decimal(args, cell),
Function::Roman => self.fn_roman(args, cell),
Function::Arabic => self.fn_arabic(args, cell),
Function::Combin => self.fn_combin(args, cell),
Function::Combina => self.fn_combina(args, cell),
Function::Sumsq => self.fn_sumsq(args, cell),
Function::N => self.fn_n(args, cell),
Function::Cell => self.fn_cell(args, cell),
Expand Down
39 changes: 23 additions & 16 deletions base/src/functions/statistical/chisq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
const MAX_DEGREES_OF_FREEDOM: f64 = 10_000_000_000.0;

impl<'a> Model<'a> {
// CHISQ.DIST(x, deg_freedom, cumulative)
Expand All @@ -13,12 +14,12 @@ impl<'a> Model<'a> {
return CalcResult::new_args_number_error(cell);
}

let x = match self.get_number_no_bools(&args[0], cell) {
let x = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};

let df = match self.get_number_no_bools(&args[1], cell) {
let df = match self.get_number(&args[1], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
Expand All @@ -35,11 +36,12 @@ impl<'a> Model<'a> {
"x must be >= 0 in CHISQ.DIST".to_string(),
);
}
if df < 1.0 {
// if degrees of freedom < 1 or > 10^10 → #NUM!
if !(1.0..=MAX_DEGREES_OF_FREEDOM).contains(&df) {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in CHISQ.DIST".to_string(),
"degrees of freedom must be in [1, 10^10] in CHISQ.DIST".to_string(),
);
}

Expand Down Expand Up @@ -77,12 +79,12 @@ impl<'a> Model<'a> {
return CalcResult::new_args_number_error(cell);
}

let x = match self.get_number_no_bools(&args[0], cell) {
let x = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};

let df_raw = match self.get_number_no_bools(&args[1], cell) {
let df_raw = match self.get_number(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
Expand All @@ -96,11 +98,13 @@ impl<'a> Model<'a> {
"x must be >= 0 in CHISQ.DIST.RT".to_string(),
);
}
if df < 1.0 {

// if degrees of freedom < 1 or > 10^10 → #NUM!
if !(1.0..=MAX_DEGREES_OF_FREEDOM).contains(&df) {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in CHISQ.DIST.RT".to_string(),
"degrees of freedom must be in [1, 10^10] in CHISQ.DIST.RT".to_string(),
);
}

Expand Down Expand Up @@ -136,12 +140,12 @@ impl<'a> Model<'a> {
return CalcResult::new_args_number_error(cell);
}

let p = match self.get_number_no_bools(&args[0], cell) {
let p = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};

let df = match self.get_number_no_bools(&args[1], cell) {
let df = match self.get_number(&args[1], cell) {
Ok(f) => f.trunc(),
Err(e) => return e,
};
Expand All @@ -154,11 +158,13 @@ impl<'a> Model<'a> {
"probability must be in [0,1] in CHISQ.INV".to_string(),
);
}
if df < 1.0 {

// if degrees of freedom < 1 or > 10^10 → #NUM!
if !(1.0..=MAX_DEGREES_OF_FREEDOM).contains(&df) {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in CHISQ.INV".to_string(),
"degrees of freedom must be in [1, 10^10] in CHISQ.INV".to_string(),
);
}

Expand Down Expand Up @@ -196,12 +202,12 @@ impl<'a> Model<'a> {
return CalcResult::new_args_number_error(cell);
}

let p = match self.get_number_no_bools(&args[0], cell) {
let p = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};

let df_raw = match self.get_number_no_bools(&args[1], cell) {
let df_raw = match self.get_number(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
Expand All @@ -216,11 +222,12 @@ impl<'a> Model<'a> {
"probability must be in [0,1] in CHISQ.INV.RT".to_string(),
);
}
if df < 1.0 {
// if degrees of freedom < 1 or > 10^10 → #NUM!
if !(1.0..=MAX_DEGREES_OF_FREEDOM).contains(&df) {
return CalcResult::new_error(
Error::NUM,
cell,
"degrees of freedom must be >= 1 in CHISQ.INV.RT".to_string(),
"degrees of freedom must be in [1, 10^10] in CHISQ.INV.RT".to_string(),
);
}

Expand Down
5 changes: 5 additions & 0 deletions base/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,13 @@ mod test_even_odd;
mod test_exp_sign;
mod test_extend;
mod test_floor;
mod test_fn_combin;
mod test_fn_combina;
mod test_fn_datevalue_timevalue;
mod test_fn_fact;
mod test_fn_fv;
mod test_fn_permut;
mod test_fn_permutationa;
mod test_fn_round;
mod test_fn_type;
mod test_frozen_rows_and_columns;
Expand Down
Loading