Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions base/src/expressions/parser/static_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,8 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Pi => 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),
Expand Down Expand Up @@ -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),
Expand Down
163 changes: 112 additions & 51 deletions base/src/functions/mathematical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ 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,
}

/// 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()
Expand Down Expand Up @@ -305,77 +320,123 @@ 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 digits_raw = match self.get_number(&args[1], cell) {
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()

// Excel truncates non-integer digit counts toward zero.
let digits = if digits_raw > 0.0 {
digits_raw.floor()
} else {
digits_raw.ceil()
};

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);
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,
};
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).ceil() / scale)
} else {
CalcResult::Number((value * scale).floor() / scale)
}

CalcResult::Number(rounded)
}
pub(crate) fn fn_rounddown(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {

/// Helper used by CEILING and FLOOR to round a value to the nearest multiple of
/// `significance`, taking into account the Excel sign rule.
fn round_to_multiple(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
kind: RoundKind,
) -> 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,
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 significance = match self.get_number(&args[1], cell) {
Ok(v) => v,
Err(e) => return e,
};
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)

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(),
};
}

let quotient = value / significance;
let use_ceil = (significance > 0.0) == matches!(kind, RoundKind::Ceiling);
let rounded_multiple = if use_ceil {
quotient.ceil() * significance
} else {
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 {
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)));
Expand Down
12 changes: 11 additions & 1 deletion base/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ pub enum Function {
Product,
Rand,
Randbetween,
Ceiling,
Floor,
Round,
Rounddown,
Roundup,
Expand Down Expand Up @@ -250,7 +252,7 @@ pub enum Function {
}

impl Function {
pub fn into_iter() -> IntoIter<Function, 195> {
pub fn into_iter() -> IntoIter<Function, 197> {
[
Function::And,
Function::False,
Expand Down Expand Up @@ -286,6 +288,8 @@ impl Function {
Function::Product,
Function::Rand,
Function::Randbetween,
Function::Ceiling,
Function::Floor,
Function::Round,
Function::Rounddown,
Function::Roundup,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions base/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading