UPDATE: Adds GCD and LCM functions (#502)

* UPDATE: Adds GCD and LCM functions

They follow SUM and accept arrays

* FIX: Implement copilot suggestions
This commit is contained in:
Nicolás Hatcher Andrés
2025-11-02 19:50:58 +01:00
committed by GitHub
parent efb3b66777
commit 3e2b177ffe
4 changed files with 345 additions and 12 deletions

View File

@@ -851,17 +851,19 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Int => args_signature_scalars(arg_count, 1, 0),
Function::Even => args_signature_scalars(arg_count, 1, 0),
Function::Odd => args_signature_scalars(arg_count, 1, 0),
Function::Ceiling => args_signature_scalars(arg_count, 2, 0), // (number, significance)
Function::CeilingMath => args_signature_scalars(arg_count, 1, 2), // (number, [significance], [mode])
Function::CeilingPrecise => args_signature_scalars(arg_count, 1, 1), // (number, [significance])
Function::Floor => args_signature_scalars(arg_count, 2, 0), // (number, significance)
Function::FloorMath => args_signature_scalars(arg_count, 1, 2), // (number, [significance], [mode])
Function::FloorPrecise => args_signature_scalars(arg_count, 1, 1), // (number, [significance])
Function::IsoCeiling => args_signature_scalars(arg_count, 1, 1), // (number, [significance])
Function::Mod => args_signature_scalars(arg_count, 2, 0), // (number, divisor)
Function::Quotient => args_signature_scalars(arg_count, 2, 0), // (number, denominator)
Function::Mround => args_signature_scalars(arg_count, 2, 0), // (number, multiple)
Function::Trunc => args_signature_scalars(arg_count, 1, 1), // (num, [num_digits])
Function::Ceiling => args_signature_scalars(arg_count, 2, 0),
Function::CeilingMath => args_signature_scalars(arg_count, 1, 2),
Function::CeilingPrecise => args_signature_scalars(arg_count, 1, 1),
Function::Floor => args_signature_scalars(arg_count, 2, 0),
Function::FloorMath => args_signature_scalars(arg_count, 1, 2),
Function::FloorPrecise => args_signature_scalars(arg_count, 1, 1),
Function::IsoCeiling => args_signature_scalars(arg_count, 1, 1),
Function::Mod => args_signature_scalars(arg_count, 2, 0),
Function::Quotient => args_signature_scalars(arg_count, 2, 0),
Function::Mround => args_signature_scalars(arg_count, 2, 0),
Function::Trunc => args_signature_scalars(arg_count, 1, 1),
Function::Gcd => vec![Signature::Vector; arg_count],
Function::Lcm => vec![Signature::Vector; arg_count],
}
}
@@ -1112,5 +1114,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Quotient => scalar_arguments(args),
Function::Mround => scalar_arguments(args),
Function::Trunc => scalar_arguments(args),
Function::Gcd => not_implemented(args),
Function::Lcm => not_implemented(args),
}
}

View File

@@ -14,6 +14,32 @@ pub fn random() -> f64 {
rand::random()
}
// Euclidean gcd for i64 (non-negative inputs expected)
fn gcd_i64(mut a: i64, mut b: i64) -> i64 {
while b != 0 {
let r = a % b;
a = b;
b = r;
}
a
}
// lcm(a, b) = a / gcd(a, b) * b
// we do it in i128 to reduce overflow risk, then back to i64/f64
fn lcm_i64(a: i64, b: i64) -> Option<i64> {
if a == 0 || b == 0 {
return Some(0);
}
let g = gcd_i64(a, b);
let a_div_g = (a / g) as i128;
let prod = a_div_g * (b as i128);
if prod > i64::MAX as i128 {
None
} else {
Some(prod as i64)
}
}
#[cfg(target_arch = "wasm32")]
pub fn random() -> f64 {
use js_sys::Math;
@@ -107,6 +133,297 @@ 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);
}
let mut acc: Option<i64> = None;
let mut saw_number = false;
let mut has_range = false;
// Returns Some(CalcResult) if an error occurred
let mut handle_number = |value: f64| -> Option<CalcResult> {
if !value.is_finite() {
return Some(CalcResult::new_error(
Error::VALUE,
cell,
"Non-finite number in GCD".to_string(),
));
}
let n = value.trunc() as i64;
if n < 0 {
return Some(CalcResult::new_error(
Error::NUM,
cell,
"GCD only accepts non-negative integers".to_string(),
));
}
saw_number = true;
acc = Some(match acc {
Some(cur) => gcd_i64(cur, n),
None => n,
});
None
};
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
if let Some(res) = handle_number(value) {
return res;
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
has_range = true;
let row1 = left.row;
let mut row2 = right.row;
let column1 = left.column;
let mut column2 = right.column;
if row1 == 1 && row2 == LAST_ROW {
row2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_row,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
if column1 == 1 && column2 == LAST_COLUMN {
column2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_column,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
for row in row1..=row2 {
for column in column1..=column2 {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
if let Some(res) = handle_number(value) {
return res;
}
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore strings / booleans
}
}
}
}
}
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
if let Some(res) = handle_number(value) {
return res;
}
}
ArrayNode::Error(error) => {
return CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {
// ignore strings / booleans
}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore strings / booleans
}
}
}
if !saw_number && !has_range {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No valid numbers found".to_string(),
};
}
CalcResult::Number(acc.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);
}
let mut acc: Option<i64> = None;
let mut saw_number = false;
let mut has_range = false;
// Returns Some(CalcResult) if an error occurred
let mut handle_number = |value: f64| -> Option<CalcResult> {
if !value.is_finite() {
return Some(CalcResult::new_error(
Error::VALUE,
cell,
"Non-finite number in LCM".to_string(),
));
}
let n = value.trunc() as i64;
if n < 0 {
return Some(CalcResult::new_error(
Error::NUM,
cell,
"LCM only accepts non-negative integers".to_string(),
));
}
saw_number = true;
acc = Some(match acc {
Some(cur) => match lcm_i64(cur, n) {
Some(v) => v,
None => {
return Some(CalcResult::new_error(
Error::NUM,
cell,
"LCM result too large".to_string(),
));
}
},
None => n,
});
None
};
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
if let Some(res) = handle_number(value) {
return res;
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
has_range = true;
let row1 = left.row;
let mut row2 = right.row;
let column1 = left.column;
let mut column2 = right.column;
if row1 == 1 && row2 == LAST_ROW {
row2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_row,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
if column1 == 1 && column2 == LAST_COLUMN {
column2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_column,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
for row in row1..=row2 {
for column in column1..=column2 {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
if let Some(res) = handle_number(value) {
return res;
}
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore strings / booleans
}
}
}
}
}
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
if let Some(res) = handle_number(value) {
return res;
}
}
ArrayNode::Error(error) => {
return CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {
// ignore strings / booleans
}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore strings / booleans
}
}
}
if !saw_number && !has_range {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No valid numbers found".to_string(),
};
}
CalcResult::Number(acc.unwrap_or(0) as f64)
}
pub(crate) fn fn_sum(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);

View File

@@ -108,6 +108,9 @@ pub enum Function {
Mround,
Trunc,
Gcd,
Lcm,
// Information
ErrorType,
Formulatext,
@@ -301,7 +304,7 @@ pub enum Function {
}
impl Function {
pub fn into_iter() -> IntoIter<Function, 243> {
pub fn into_iter() -> IntoIter<Function, 245> {
[
Function::And,
Function::False,
@@ -361,6 +364,8 @@ impl Function {
Function::Quotient,
Function::Mround,
Function::Trunc,
Function::Gcd,
Function::Lcm,
Function::Max,
Function::Min,
Function::Product,
@@ -667,6 +672,9 @@ impl Function {
"MROUND" => Some(Function::Mround),
"TRUNC" => Some(Function::Trunc),
"GCD" => Some(Function::Gcd),
"LCM" => Some(Function::Lcm),
"PI" => Some(Function::Pi),
"ABS" => Some(Function::Abs),
"SQRT" => Some(Function::Sqrt),
@@ -1128,6 +1136,8 @@ impl fmt::Display for Function {
Function::Quotient => write!(f, "QUOTIENT"),
Function::Mround => write!(f, "MROUND"),
Function::Trunc => write!(f, "TRUNC"),
Function::Gcd => write!(f, "GCD"),
Function::Lcm => write!(f, "LCM"),
}
}
}
@@ -1399,6 +1409,8 @@ impl Model {
Function::Quotient => self.fn_quotient(args, cell),
Function::Mround => self.fn_mround(args, cell),
Function::Trunc => self.fn_trunc(args, cell),
Function::Gcd => self.fn_gcd(args, cell),
Function::Lcm => self.fn_lcm(args, cell),
}
}
}

Binary file not shown.