diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 23022a6..835a4ba 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -864,6 +864,8 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec args_signature_scalars(arg_count, 1, 1), Function::Gcd => vec![Signature::Vector; arg_count], Function::Lcm => vec![Signature::Vector; arg_count], + Function::Base => args_signature_scalars(arg_count, 2, 1), + Function::Decimal => args_signature_scalars(arg_count, 2, 0), } } @@ -1116,5 +1118,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Trunc => scalar_arguments(args), Function::Gcd => not_implemented(args), Function::Lcm => not_implemented(args), + Function::Base => scalar_arguments(args), + Function::Decimal => scalar_arguments(args), } } diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index 91ad94e..5be0521 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -133,6 +133,118 @@ impl Model { CalcResult::Number(result) } + pub(crate) fn fn_base(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + let arg_count = args.len(); + if !(2..=3).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + + // number to convert + let mut value = match self.get_number(&args[0], cell) { + Ok(f) => f.trunc() as i64, + Err(s) => return s, + }; + // radix + let radix = match self.get_number(&args[1], cell) { + Ok(f) => f.trunc() as i64, + Err(s) => return s, + }; + // optional min_length + let min_length = if arg_count == 3 { + match self.get_number(&args[2], cell) { + Ok(f) => { + if f < 0.0 { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Minimum length must be non-negative".to_string(), + }; + } + f.trunc() as usize + } + Err(s) => return s, + } + } else { + 0 + }; + + if !(2..=36).contains(&radix) { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Radix must be between 2 and 36".to_string(), + }; + } + + // number must be >= 0 + if value < 0 { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Number must be non-negative".to_string(), + }; + } + + let mut buf = String::new(); + if value == 0 { + buf.push('0'); + } else { + while value > 0 { + let digit = (value % radix) as u8; + let ch = match digit { + 0..=9 => (b'0' + digit) as char, + 10..=35 => (b'A' + (digit - 10)) as char, + _ => unreachable!(), + }; + buf.push(ch); + value /= radix; + } + // we built it in reverse + buf = buf.chars().rev().collect(); + } + + // pad with leading zeros if needed + if buf.len() < min_length { + let mut padded = String::with_capacity(min_length); + for _ in 0..(min_length - buf.len()) { + padded.push('0'); + } + padded.push_str(&buf); + buf = padded; + } + + CalcResult::String(buf) + } + + pub(crate) fn fn_decimal(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let text = match self.get_string(&args[0], cell) { + Ok(s) => s, + Err(s) => return s, + }; + let radix = match self.get_number(&args[1], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + }; + if !(2..=36).contains(&radix) { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Radix must be between 2 and 36".to_string(), + }; + } + match i64::from_str_radix(&text, radix as u32) { + Ok(n) => CalcResult::Number(n as f64), + Err(_) => CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: format!("'{}' is not a valid number in base {}", text, radix), + }, + } + } + pub(crate) fn fn_gcd(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { if args.is_empty() { return CalcResult::new_args_number_error(cell); diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 3f13716..a6506b1 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -84,15 +84,12 @@ pub enum Function { Csch, Sec, Sech, - Exp, Fact, Factdouble, Sign, - Radians, Degrees, - Int, Even, Odd, @@ -107,9 +104,10 @@ pub enum Function { Quotient, Mround, Trunc, - Gcd, Lcm, + Base, + Decimal, // Information ErrorType, @@ -304,7 +302,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -366,6 +364,8 @@ impl Function { Function::Trunc, Function::Gcd, Function::Lcm, + Function::Base, + Function::Decimal, Function::Max, Function::Min, Function::Product, @@ -593,13 +593,14 @@ impl Function { Function::Sheet => "_xlfn.SHEET".to_string(), Function::Formulatext => "_xlfn.FORMULATEXT".to_string(), Function::Isoweeknum => "_xlfn.ISOWEEKNUM".to_string(), - Function::Ceiling => "_xlfn.CEILING".to_string(), Function::CeilingMath => "_xlfn.CEILING.MATH".to_string(), Function::CeilingPrecise => "_xlfn.CEILING.PRECISE".to_string(), Function::FloorMath => "_xlfn.FLOOR.MATH".to_string(), Function::FloorPrecise => "_xlfn.FLOOR.PRECISE".to_string(), Function::IsoCeiling => "_xlfn.ISO.CEILING".to_string(), + Function::Base => "_xlfn.BASE".to_string(), + Function::Decimal => "_xlfn.DECIMAL".to_string(), _ => self.to_string(), } @@ -623,23 +624,18 @@ impl Function { "SWITCH" | "_XLFN.SWITCH" => Some(Function::Switch), "TRUE" => Some(Function::True), "XOR" | "_XLFN.XOR" => Some(Function::Xor), - "SIN" => Some(Function::Sin), "COS" => Some(Function::Cos), "TAN" => Some(Function::Tan), - "ASIN" => Some(Function::Asin), "ACOS" => Some(Function::Acos), "ATAN" => Some(Function::Atan), - "SINH" => Some(Function::Sinh), "COSH" => Some(Function::Cosh), "TANH" => Some(Function::Tanh), - "ASINH" => Some(Function::Asinh), "ACOSH" => Some(Function::Acosh), "ATANH" => Some(Function::Atanh), - "ACOT" => Some(Function::Acot), "COTH" => Some(Function::Coth), "COT" => Some(Function::Cot), @@ -648,15 +644,12 @@ impl Function { "SEC" => Some(Function::Sec), "SECH" => Some(Function::Sech), "ACOTH" => Some(Function::Acoth), - "FACT" => Some(Function::Fact), "FACTDOUBLE" => Some(Function::Factdouble), "EXP" => Some(Function::Exp), "SIGN" => Some(Function::Sign), - "RADIANS" => Some(Function::Radians), "DEGREES" => Some(Function::Degrees), - "INT" => Some(Function::Int), "EVEN" => Some(Function::Even), "ODD" => Some(Function::Odd), @@ -671,21 +664,19 @@ impl Function { "QUOTIENT" => Some(Function::Quotient), "MROUND" => Some(Function::Mround), "TRUNC" => Some(Function::Trunc), - "GCD" => Some(Function::Gcd), "LCM" => Some(Function::Lcm), - + "BASE" | "_XLFN.BASE" => Some(Function::Base), + "DECIMAL" | "_XLFN.DECIMAL" => Some(Function::Decimal), "PI" => Some(Function::Pi), "ABS" => Some(Function::Abs), "SQRT" => Some(Function::Sqrt), "SQRTPI" => Some(Function::Sqrtpi), "POWER" => Some(Function::Power), "ATAN2" => Some(Function::Atan2), - "LN" => Some(Function::Ln), "LOG" => Some(Function::Log), "LOG10" => Some(Function::Log10), - "MAX" => Some(Function::Max), "MIN" => Some(Function::Min), "PRODUCT" => Some(Function::Product), @@ -1138,6 +1129,8 @@ impl fmt::Display for Function { Function::Trunc => write!(f, "TRUNC"), Function::Gcd => write!(f, "GCD"), Function::Lcm => write!(f, "LCM"), + Function::Base => write!(f, "BASE"), + Function::Decimal => write!(f, "DECIMAL"), } } } @@ -1411,6 +1404,8 @@ impl Model { Function::Trunc => self.fn_trunc(args, cell), Function::Gcd => self.fn_gcd(args, cell), Function::Lcm => self.fn_lcm(args, cell), + Function::Base => self.fn_base(args, cell), + Function::Decimal => self.fn_decimal(args, cell), } } } diff --git a/xlsx/tests/calc_tests/BASE.xlsx b/xlsx/tests/calc_tests/BASE.xlsx new file mode 100644 index 0000000..f69fdd7 Binary files /dev/null and b/xlsx/tests/calc_tests/BASE.xlsx differ