From 169a89f803291744427d75cf418ffa6c7960efa9 Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Sun, 13 Jul 2025 00:25:00 -0700 Subject: [PATCH 1/5] Add CEILING and FLOOR functions --- .../src/expressions/parser/static_analysis.rs | 4 ++ base/src/functions/mathematical.rs | 66 +++++++++++++++++++ base/src/functions/mod.rs | 12 +++- base/src/test/mod.rs | 1 + base/src/test/test_fn_ceiling_floor.rs | 39 +++++++++++ docs/src/functions/math-and-trigonometry.md | 4 +- .../math_and_trigonometry/ceiling.md | 3 +- .../functions/math_and_trigonometry/floor.md | 3 +- 8 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 base/src/test/test_fn_ceiling_floor.rs diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 280ac2484..192606b32 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -612,6 +612,8 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec args_signature_no_args(arg_count), Function::Power => args_signature_scalars(arg_count, 2, 0), Function::Product => vec![Signature::Vector; arg_count], + Function::Ceiling => args_signature_scalars(arg_count, 2, 0), + Function::Floor => args_signature_scalars(arg_count, 2, 0), 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), @@ -813,6 +815,8 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Pi => StaticResult::Scalar, Function::Power => scalar_arguments(args), Function::Product => not_implemented(args), + Function::Ceiling => scalar_arguments(args), + Function::Floor => scalar_arguments(args), Function::Round => scalar_arguments(args), Function::Rounddown => scalar_arguments(args), Function::Roundup => scalar_arguments(args), diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index 82f4b8b45..c4106851b 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -378,6 +378,72 @@ impl Model { } } + pub(crate) fn fn_ceiling(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let value = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let significance = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + if significance == 0.0 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Divide by 0".to_string(), + }; + } + if value.signum() * significance.signum() < 0.0 { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Invalid sign".to_string(), + }; + } + if significance > 0.0 { + CalcResult::Number((value / significance).ceil() * significance) + } else { + CalcResult::Number((value / significance).floor() * significance) + } + } + + pub(crate) fn fn_floor(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let value = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let significance = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + if significance == 0.0 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Divide by 0".to_string(), + }; + } + if value.signum() * significance.signum() < 0.0 { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Invalid sign".to_string(), + }; + } + if significance > 0.0 { + CalcResult::Number((value / significance).floor() * significance) + } else { + CalcResult::Number((value / significance).ceil() * significance) + } + } + single_number_fn!(fn_sin, |f| Ok(f64::sin(f))); single_number_fn!(fn_cos, |f| Ok(f64::cos(f))); single_number_fn!(fn_tan, |f| Ok(f64::tan(f))); diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 45da02525..74b2af6bb 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -61,6 +61,8 @@ pub enum Function { Product, Rand, Randbetween, + Ceiling, + Floor, Round, Rounddown, Roundup, @@ -250,7 +252,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -286,6 +288,8 @@ impl Function { Function::Product, Function::Rand, Function::Randbetween, + Function::Ceiling, + Function::Floor, Function::Round, Function::Rounddown, Function::Roundup, @@ -539,6 +543,8 @@ impl Function { "PRODUCT" => Some(Function::Product), "RAND" => Some(Function::Rand), "RANDBETWEEN" => Some(Function::Randbetween), + "CEILING" => Some(Function::Ceiling), + "FLOOR" => Some(Function::Floor), "ROUND" => Some(Function::Round), "ROUNDDOWN" => Some(Function::Rounddown), "ROUNDUP" => Some(Function::Roundup), @@ -757,6 +763,8 @@ impl fmt::Display for Function { Function::Product => write!(f, "PRODUCT"), Function::Rand => write!(f, "RAND"), Function::Randbetween => write!(f, "RANDBETWEEN"), + Function::Ceiling => write!(f, "CEILING"), + Function::Floor => write!(f, "FLOOR"), Function::Round => write!(f, "ROUND"), Function::Rounddown => write!(f, "ROUNDDOWN"), Function::Roundup => write!(f, "ROUNDUP"), @@ -990,6 +998,8 @@ impl Model { Function::Product => self.fn_product(args, cell), Function::Rand => self.fn_rand(args, cell), Function::Randbetween => self.fn_randbetween(args, cell), + Function::Ceiling => self.fn_ceiling(args, cell), + Function::Floor => self.fn_floor(args, cell), Function::Round => self.fn_round(args, cell), Function::Rounddown => self.fn_rounddown(args, cell), Function::Roundup => self.fn_roundup(args, cell), diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index 8e1b4ebe1..89c4e1b4e 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -23,6 +23,7 @@ mod test_fn_minifs; mod test_fn_or_xor; mod test_fn_product; mod test_fn_rept; +mod test_fn_ceiling_floor; mod test_fn_sum; mod test_fn_sumifs; mod test_fn_textbefore; diff --git a/base/src/test/test_fn_ceiling_floor.rs b/base/src/test/test_fn_ceiling_floor.rs new file mode 100644 index 000000000..657b1e610 --- /dev/null +++ b/base/src/test/test_fn_ceiling_floor.rs @@ -0,0 +1,39 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn simple_cases() { + let mut model = new_empty_model(); + model._set("A1", "=CEILING(4.3,2)"); + model._set("A2", "=CEILING(-4.3,-2)"); + model._set("A3", "=CEILING(-4.3,2)"); + model._set("B1", "=FLOOR(4.3,2)"); + model._set("B2", "=FLOOR(-4.3,-2)"); + model._set("B3", "=FLOOR(4.3,-2)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"6"); + assert_eq!(model._get_text("A2"), *"-4"); + assert_eq!(model._get_text("A3"), *"#NUM!"); + assert_eq!(model._get_text("B1"), *"4"); + assert_eq!(model._get_text("B2"), *"-6"); + assert_eq!(model._get_text("B3"), *"#NUM!"); +} + +#[test] +fn wrong_number_of_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=CEILING(1)"); + model._set("A2", "=CEILING(1,2,3)"); + model._set("B1", "=FLOOR(1)"); + model._set("B2", "=FLOOR(1,2,3)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("B1"), *"#ERROR!"); + assert_eq!(model._get_text("B2"), *"#ERROR!"); +} diff --git a/docs/src/functions/math-and-trigonometry.md b/docs/src/functions/math-and-trigonometry.md index 03593584f..33d6d2f63 100644 --- a/docs/src/functions/math-and-trigonometry.md +++ b/docs/src/functions/math-and-trigonometry.md @@ -24,7 +24,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | ATAN2 | | – | | ATANH | | – | | BASE | | – | -| CEILING | | – | +| CEILING | | [CEILING](math_and_trigonometry/ceiling) | | CEILING.MATH | | – | | CEILING.PRECISE | | – | | COMBIN | | – | @@ -41,7 +41,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | EXP | | – | | FACT | | – | | FACTDOUBLE | | – | -| FLOOR | | – | +| FLOOR | | [FLOOR](math_and_trigonometry/floor) | | FLOOR.MATH | | – | | FLOOR.PRECISE | | – | | GCD | | – | diff --git a/docs/src/functions/math_and_trigonometry/ceiling.md b/docs/src/functions/math_and_trigonometry/ceiling.md index e6640045b..5ddc750cb 100644 --- a/docs/src/functions/math_and_trigonometry/ceiling.md +++ b/docs/src/functions/math_and_trigonometry/ceiling.md @@ -7,6 +7,5 @@ lang: en-US # CEILING ::: 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). ::: \ No newline at end of file diff --git a/docs/src/functions/math_and_trigonometry/floor.md b/docs/src/functions/math_and_trigonometry/floor.md index 6e49e2571..e5c739cdb 100644 --- a/docs/src/functions/math_and_trigonometry/floor.md +++ b/docs/src/functions/math_and_trigonometry/floor.md @@ -7,6 +7,5 @@ lang: en-US # FLOOR ::: 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). ::: \ No newline at end of file From 3a9151103d1058264b1a1be2f375a1b2ba836731 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 13 Jul 2025 10:32:39 -0700 Subject: [PATCH 2/5] helper func for ceil and floor --- base/src/functions/mathematical.rs | 80 +++++++++++++++--------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index c4106851b..210f98f80 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -8,6 +8,13 @@ use crate::{ }; use std::f64::consts::PI; +/// Specifies which rounding behaviour to apply when calling `round_to_multiple`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RoundKind { + Ceiling, + Floor, +} + #[cfg(not(target_arch = "wasm32"))] pub fn random() -> f64 { rand::random() @@ -378,18 +385,32 @@ impl Model { } } - pub(crate) fn fn_ceiling(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + /// Helper used by CEILING and FLOOR to round a value to the nearest multiple of + /// `significance`. When `is_ceiling` is true the behaviour matches CEILING (i.e. use + /// `ceil` when `significance` is positive, `floor` when it's negative). When false it + /// matches FLOOR (the opposite rounding direction). + fn round_to_multiple( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + kind: RoundKind, + ) -> CalcResult { + // 1. Validate argument count. if args.len() != 2 { return CalcResult::new_args_number_error(cell); } + + // 2. Get numeric arguments, propagating errors. let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + Ok(v) => v, + Err(e) => return e, }; let significance = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, + Ok(v) => v, + Err(e) => return e, }; + + // 3. Error conditions identical for CEILING and FLOOR. if significance == 0.0 { return CalcResult::Error { error: Error::DIV, @@ -404,44 +425,25 @@ impl Model { message: "Invalid sign".to_string(), }; } - if significance > 0.0 { - CalcResult::Number((value / significance).ceil() * significance) + + // 4. Perform rounding. + let quotient = value / significance; + let use_ceil = (significance > 0.0) == matches!(kind, RoundKind::Ceiling); + let rounded_multiple = if use_ceil { + quotient.ceil() * significance } else { - CalcResult::Number((value / significance).floor() * significance) - } + quotient.floor() * significance + }; + + CalcResult::Number(rounded_multiple) + } + + pub(crate) fn fn_ceiling(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + self.round_to_multiple(args, cell, RoundKind::Ceiling) } pub(crate) fn fn_floor(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 2 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let significance = match self.get_number(&args[1], cell) { - Ok(f) => f, - Err(s) => return s, - }; - if significance == 0.0 { - return CalcResult::Error { - error: Error::DIV, - origin: cell, - message: "Divide by 0".to_string(), - }; - } - if value.signum() * significance.signum() < 0.0 { - return CalcResult::Error { - error: Error::NUM, - origin: cell, - message: "Invalid sign".to_string(), - }; - } - if significance > 0.0 { - CalcResult::Number((value / significance).floor() * significance) - } else { - CalcResult::Number((value / significance).ceil() * significance) - } + self.round_to_multiple(args, cell, RoundKind::Floor) } single_number_fn!(fn_sin, |f| Ok(f64::sin(f))); From 764abd8f26782599a45e0525e29010cbd183e732 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 13 Jul 2025 10:32:45 -0700 Subject: [PATCH 3/5] helper func for round --- base/src/functions/mathematical.rs | 121 ++++++++++++++--------------- 1 file changed, 57 insertions(+), 64 deletions(-) diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index 210f98f80..4b8366be7 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -15,6 +15,14 @@ enum RoundKind { Floor, } +/// Rounding mode used by the classic ROUND family (ROUND, ROUNDUP, ROUNDDOWN). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RoundDecimalKind { + Nearest, // ROUND + Up, // ROUNDUP + Down, // ROUNDDOWN +} + #[cfg(not(target_arch = "wasm32"))] pub fn random() -> f64 { rand::random() @@ -312,95 +320,70 @@ impl Model { CalcResult::Number(total) } - pub(crate) fn fn_round(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + /// Shared implementation for Excel's ROUND / ROUNDUP / ROUNDDOWN functions + /// that round a scalar to a specified number of decimal digits. + fn round_decimal_fn( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + mode: RoundDecimalKind, + ) -> CalcResult { if args.len() != 2 { - // Incorrect number of arguments return CalcResult::new_args_number_error(cell); } + + // Extract value and number_of_digits, propagating errors. let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + Ok(v) => v, + Err(e) => return e, }; - let number_of_digits = match self.get_number(&args[1], cell) { - Ok(f) => { - if f > 0.0 { - f.floor() - } else { - f.ceil() - } - } - Err(s) => return s, + let digits_raw = match self.get_number(&args[1], cell) { + Ok(v) => v, + Err(e) => return e, }; - let scale = 10.0_f64.powf(number_of_digits); - CalcResult::Number((value * scale).round() / scale) - } - pub(crate) fn fn_roundup(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 2 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, + + // Excel truncates non-integer digit counts toward zero. + let digits = if digits_raw > 0.0 { + digits_raw.floor() + } else { + digits_raw.ceil() }; - let number_of_digits = match self.get_number(&args[1], cell) { - Ok(f) => { - if f > 0.0 { - f.floor() + + let scale = 10.0_f64.powf(digits); + + let rounded = match mode { + RoundDecimalKind::Nearest => (value * scale).round() / scale, + RoundDecimalKind::Up => { + if value > 0.0 { + (value * scale).ceil() / scale } else { - f.ceil() + (value * scale).floor() / scale } } - Err(s) => return s, - }; - let scale = 10.0_f64.powf(number_of_digits); - if value > 0.0 { - CalcResult::Number((value * scale).ceil() / scale) - } else { - CalcResult::Number((value * scale).floor() / scale) - } - } - pub(crate) fn fn_rounddown(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 2 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let number_of_digits = match self.get_number(&args[1], cell) { - Ok(f) => { - if f > 0.0 { - f.floor() + RoundDecimalKind::Down => { + if value > 0.0 { + (value * scale).floor() / scale } else { - f.ceil() + (value * scale).ceil() / scale } } - Err(s) => return s, }; - let scale = 10.0_f64.powf(number_of_digits); - if value > 0.0 { - CalcResult::Number((value * scale).floor() / scale) - } else { - CalcResult::Number((value * scale).ceil() / scale) - } + + CalcResult::Number(rounded) } /// Helper used by CEILING and FLOOR to round a value to the nearest multiple of - /// `significance`. When `is_ceiling` is true the behaviour matches CEILING (i.e. use - /// `ceil` when `significance` is positive, `floor` when it's negative). When false it - /// matches FLOOR (the opposite rounding direction). + /// `significance`, taking into account the Excel sign rule. fn round_to_multiple( &mut self, args: &[Node], cell: CellReferenceIndex, kind: RoundKind, ) -> CalcResult { - // 1. Validate argument count. if args.len() != 2 { return CalcResult::new_args_number_error(cell); } - // 2. Get numeric arguments, propagating errors. let value = match self.get_number(&args[0], cell) { Ok(v) => v, Err(e) => return e, @@ -410,7 +393,6 @@ impl Model { Err(e) => return e, }; - // 3. Error conditions identical for CEILING and FLOOR. if significance == 0.0 { return CalcResult::Error { error: Error::DIV, @@ -426,7 +408,6 @@ impl Model { }; } - // 4. Perform rounding. let quotient = value / significance; let use_ceil = (significance > 0.0) == matches!(kind, RoundKind::Ceiling); let rounded_multiple = if use_ceil { @@ -446,6 +427,18 @@ impl Model { self.round_to_multiple(args, cell, RoundKind::Floor) } + pub(crate) fn fn_round(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + self.round_decimal_fn(args, cell, RoundDecimalKind::Nearest) + } + + pub(crate) fn fn_roundup(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + self.round_decimal_fn(args, cell, RoundDecimalKind::Up) + } + + pub(crate) fn fn_rounddown(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + self.round_decimal_fn(args, cell, RoundDecimalKind::Down) + } + single_number_fn!(fn_sin, |f| Ok(f64::sin(f))); single_number_fn!(fn_cos, |f| Ok(f64::cos(f))); single_number_fn!(fn_tan, |f| Ok(f64::tan(f))); From 8e90cf2c62679f44ffde210488fa990c66128eea Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 13 Jul 2025 11:07:06 -0700 Subject: [PATCH 4/5] fmt --- base/src/test/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index 89c4e1b4e..b0ba354aa 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -10,6 +10,7 @@ mod test_date_and_time; mod test_error_propagation; mod test_fn_average; mod test_fn_averageifs; +mod test_fn_ceiling_floor; mod test_fn_choose; mod test_fn_concatenate; mod test_fn_count; @@ -23,7 +24,6 @@ mod test_fn_minifs; mod test_fn_or_xor; mod test_fn_product; mod test_fn_rept; -mod test_fn_ceiling_floor; mod test_fn_sum; mod test_fn_sumifs; mod test_fn_textbefore; From 7d7fcccd88d7c10bb1c49173d54a96ee1472bf0c Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 13 Jul 2025 11:46:38 -0700 Subject: [PATCH 5/5] add more test cases --- base/src/test/test_fn_ceiling_floor.rs | 105 +++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/base/src/test/test_fn_ceiling_floor.rs b/base/src/test/test_fn_ceiling_floor.rs index 657b1e610..ca0d876b6 100644 --- a/base/src/test/test_fn_ceiling_floor.rs +++ b/base/src/test/test_fn_ceiling_floor.rs @@ -37,3 +37,108 @@ fn wrong_number_of_arguments() { assert_eq!(model._get_text("B1"), *"#ERROR!"); assert_eq!(model._get_text("B2"), *"#ERROR!"); } + +#[test] +fn zero_significance() { + let mut model = new_empty_model(); + model._set("A1", "=CEILING(4.3,0)"); + model._set("B1", "=FLOOR(4.3,0)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#DIV/0!"); + assert_eq!(model._get_text("B1"), *"#DIV/0!"); +} + +#[test] +fn already_multiple() { + let mut model = new_empty_model(); + model._set("A1", "=CEILING(6,3)"); + model._set("B1", "=FLOOR(6,3)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"6"); + assert_eq!(model._get_text("B1"), *"6"); +} + +#[test] +fn smaller_than_significance() { + let mut model = new_empty_model(); + model._set("A1", "=CEILING(1.3,2)"); + model._set("B1", "=FLOOR(1.3,2)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"2"); + assert_eq!(model._get_text("B1"), *"0"); +} + +#[test] +fn fractional_significance() { + let mut model = new_empty_model(); + model._set("A1", "=CEILING(4.3,2.5)"); + model._set("B1", "=FLOOR(4.3,2.5)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"5"); + assert_eq!(model._get_text("B1"), *"2.5"); +} + +#[test] +fn opposite_sign_error() { + let mut model = new_empty_model(); + model._set("A1", "=CEILING(4.3,-2)"); + model._set("B1", "=FLOOR(-4.3,2)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#NUM!"); + assert_eq!(model._get_text("B1"), *"#NUM!"); +} + +#[test] +fn zero_value() { + let mut model = new_empty_model(); + model._set("A1", "=CEILING(0,2)"); + model._set("B1", "=FLOOR(0,2)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0"); + assert_eq!(model._get_text("B1"), *"0"); +} + +#[test] +fn coercion_cases() { + let mut model = new_empty_model(); + model._set("B1", "'4.3"); // text that can be coerced + model._set("B2", "TRUE"); // boolean + // B3 left blank + + model._set("C1", "=CEILING(B1,2)"); + model._set("C2", "=FLOOR(B2,1)"); + model._set("C3", "=CEILING(B3,2)"); + + model.evaluate(); + + assert_eq!(model._get_text("C1"), *"6"); + assert_eq!(model._get_text("C2"), *"1"); + assert_eq!(model._get_text("C3"), *"0"); +} + +#[test] +fn error_propagation() { + let mut model = new_empty_model(); + model._set("A1", "=1/0"); // #DIV/0! in value + model._set("A2", "#REF!"); // #REF! error literal as significance + + model._set("B1", "=CEILING(A1,2)"); + model._set("B2", "=FLOOR(4.3,A2)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"#DIV/0!"); + assert_eq!(model._get_text("B2"), *"#REF!"); +}