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 @@ -610,6 +610,8 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Column => args_signature_row(arg_count),
Function::Columns => args_signature_one_vector(arg_count),
Function::Ln => args_signature_scalars(arg_count, 1, 0),
Function::Gcd => vec![Signature::Vector; arg_count],
Function::Lcm => vec![Signature::Vector; arg_count],
Function::Log => args_signature_scalars(arg_count, 1, 1),
Function::Log10 => args_signature_scalars(arg_count, 1, 0),
Function::Cos => args_signature_scalars(arg_count, 1, 0),
Expand Down Expand Up @@ -824,6 +826,8 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Rounddown => scalar_arguments(args),
Function::Roundup => scalar_arguments(args),
Function::Ln => scalar_arguments(args),
Function::Gcd => StaticResult::Scalar,
Function::Lcm => StaticResult::Scalar,
Function::Log => scalar_arguments(args),
Function::Log10 => scalar_arguments(args),
Function::Sin => scalar_arguments(args),
Expand Down
222 changes: 222 additions & 0 deletions base/src/functions/mathematical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,228 @@ impl Model {
CalcResult::Number(result)
}

pub(crate) fn fn_gcd(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}

fn gcd(mut a: u128, mut b: u128) -> u128 {
while b != 0 {
let r = a % b;
a = b;
b = r;
}
a
}

let mut result: Option<u128> = None;
let mut update = |value: f64| -> Result<(), CalcResult> {
if value < 0.0 {
return Err(CalcResult::new_error(
Error::NUM,
cell,
"numbers must be positive".to_string(),
));
}
if !value.is_finite() {
return Err(CalcResult::new_error(
Error::NUM,
cell,
"value must be finite".to_string(),
));
}
let truncated = value.trunc();
if truncated > u128::MAX as f64 {
return Err(CalcResult::new_error(
Error::NUM,
cell,
"value too large".to_string(),
));
}
let v = truncated as u128;
result = Some(match result {
None => v,
Some(r) => gcd(r, v),
});
Ok(())
};

for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(v) => {
if let Err(e) = update(v) {
return e;
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return 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(v) => {
if let Err(e) = update(v) {
return e;
}
}
error @ CalcResult::Error { .. } => return error,
_ => {}
}
}
}
}
CalcResult::Array(arr) => {
for row in arr {
for value in row {
match value {
ArrayNode::Number(v) => {
if let Err(e) = update(v) {
return e;
}
}
ArrayNode::Error(err) => {
return CalcResult::Error {
error: err,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
_ => {}
}
}

CalcResult::Number(result.unwrap_or(0) as f64)
}

pub(crate) fn fn_lcm(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}

fn gcd(mut a: u128, mut b: u128) -> u128 {
while b != 0 {
let r = a % b;
a = b;
b = r;
}
a
}

let mut result: Option<u128> = None;
let mut update = |value: f64| -> Result<(), CalcResult> {
if value < 0.0 {
return Err(CalcResult::new_error(
Error::NUM,
cell,
"numbers must be positive".to_string(),
));
}
if !value.is_finite() {
return Err(CalcResult::new_error(
Error::NUM,
cell,
"value must be finite".to_string(),
));
}
let truncated = value.trunc();
if truncated > u128::MAX as f64 {
return Err(CalcResult::new_error(
Error::NUM,
cell,
"value too large".to_string(),
));
}
let v = truncated as u128;
result = Some(match result {
None => v,
Some(r) => {
if r == 0 || v == 0 {
0
} else {
r / gcd(r, v) * v
}
}
});
Ok(())
};

for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(v) => {
if let Err(e) = update(v) {
return e;
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return 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(v) => {
if let Err(e) = update(v) {
return e;
}
}
error @ CalcResult::Error { .. } => return error,
_ => {}
}
}
}
}
CalcResult::Array(arr) => {
for row in arr {
for value in row {
match value {
ArrayNode::Number(v) => {
if let Err(e) = update(v) {
return e;
}
}
ArrayNode::Error(err) => {
return CalcResult::Error {
error: err,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
_ => {}
}
}

CalcResult::Number(result.unwrap_or(0) as f64)
}

pub(crate) fn fn_rand(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !args.is_empty() {
return CalcResult::new_args_number_error(cell);
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 @@ -57,6 +57,8 @@ pub enum Function {
Log,
Log10,
Ln,
Gcd,
Lcm,
Max,
Min,
Pi,
Expand Down Expand Up @@ -253,7 +255,7 @@ pub enum Function {
}

impl Function {
pub fn into_iter() -> IntoIter<Function, 198> {
pub fn into_iter() -> IntoIter<Function, 200> {
[
Function::And,
Function::False,
Expand Down Expand Up @@ -281,6 +283,8 @@ impl Function {
Function::Abs,
Function::Pi,
Function::Ln,
Function::Gcd,
Function::Lcm,
Function::Log,
Function::Log10,
Function::Sqrt,
Expand Down Expand Up @@ -541,6 +545,8 @@ impl Function {
"ATAN2" => Some(Function::Atan2),

"LN" => Some(Function::Ln),
"GCD" => Some(Function::Gcd),
"LCM" => Some(Function::Lcm),
"LOG" => Some(Function::Log),
"LOG10" => Some(Function::Log10),

Expand Down Expand Up @@ -747,6 +753,8 @@ impl fmt::Display for Function {
Function::Log => write!(f, "LOG"),
Function::Log10 => write!(f, "LOG10"),
Function::Ln => write!(f, "LN"),
Function::Gcd => write!(f, "GCD"),
Function::Lcm => write!(f, "LCM"),
Function::Sin => write!(f, "SIN"),
Function::Cos => write!(f, "COS"),
Function::Tan => write!(f, "TAN"),
Expand Down Expand Up @@ -977,6 +985,8 @@ impl Model {
Function::Log => self.fn_log(args, cell),
Function::Log10 => self.fn_log10(args, cell),
Function::Ln => self.fn_ln(args, cell),
Function::Gcd => self.fn_gcd(args, cell),
Function::Lcm => self.fn_lcm(args, cell),
Function::Sin => self.fn_sin(args, cell),
Function::Cos => self.fn_cos(args, cell),
Function::Tan => self.fn_tan(args, cell),
Expand Down
2 changes: 2 additions & 0 deletions base/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ mod test_extend;
mod test_fn_fv;
mod test_fn_type;
mod test_frozen_rows_and_columns;
mod test_gcd;
mod test_geomean;
mod test_get_cell_content;
mod test_implicit_intersection;
mod test_issue_155;
mod test_lcm;
mod test_ln;
mod test_log;
mod test_log10;
Expand Down
75 changes: 75 additions & 0 deletions base/src/test/test_gcd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;

#[test]
fn test_fn_gcd_arguments() {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs more comprehensive tests.

let mut model = new_empty_model();
model._set("A1", "=GCD()");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
}

#[test]
fn test_fn_gcd_basic() {
let mut model = new_empty_model();
model._set("A1", "=GCD(12)");
model._set("A2", "=GCD(60,36)");
model._set("A3", "=GCD(15,25,35)");
model._set("A4", "=GCD(12.7,8.3)"); // Decimal truncation
model.evaluate();

assert_eq!(model._get_text("A1"), *"12");
assert_eq!(model._get_text("A2"), *"12");
assert_eq!(model._get_text("A3"), *"5");
assert_eq!(model._get_text("A4"), *"4");
}

#[test]
fn test_fn_gcd_zeros_and_edge_cases() {
let mut model = new_empty_model();
model._set("A1", "=GCD(0)");
model._set("A2", "=GCD(0,12)");
model._set("A3", "=GCD(12,0)");
model._set("A4", "=GCD(1,2,3,4,5)");
model.evaluate();

assert_eq!(model._get_text("A1"), *"0");
assert_eq!(model._get_text("A2"), *"12");
assert_eq!(model._get_text("A3"), *"12");
assert_eq!(model._get_text("A4"), *"1");
}

#[test]
fn test_fn_gcd_error_cases() {
let mut model = new_empty_model();
model._set("A1", "=GCD(-5)");
model._set("A2", "=GCD(12,-8)");
model._set("B1", "=1/0"); // Infinity
model._set("A3", "=GCD(B1)");
model.evaluate();

assert_eq!(model._get_text("A1"), *"#NUM!");
assert_eq!(model._get_text("A2"), *"#NUM!");
assert_eq!(model._get_text("A3"), *"#DIV/0!");
}

#[test]
fn test_fn_gcd_ranges() {
let mut model = new_empty_model();
// Range with numbers
model._set("B1", "12");
model._set("B2", "18");
model._set("B3", "24");
model._set("A1", "=GCD(B1:B3)");

// Range with mixed data (text ignored)
model._set("C1", "12");
model._set("C2", "text");
model._set("C3", "6");
model._set("A2", "=GCD(C1:C3)");

model.evaluate();

assert_eq!(model._get_text("A1"), *"6");
assert_eq!(model._get_text("A2"), *"6");
}
Loading