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:
committed by
GitHub
parent
efb3b66777
commit
3e2b177ffe
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
xlsx/tests/calc_tests/GCD_LCM.xlsx
Normal file
BIN
xlsx/tests/calc_tests/GCD_LCM.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user