diff --git a/src/kernels/arithmetic/types.rs b/src/kernels/arithmetic/types.rs index 1895674..e1d7f8d 100644 --- a/src/kernels/arithmetic/types.rs +++ b/src/kernels/arithmetic/types.rs @@ -1072,6 +1072,104 @@ mod tests { } } + #[cfg(feature = "scalar_type")] + #[test] + fn scalar_operands_apply_the_requested_operator() { + use crate::Scalar; + use crate::enums::operators::ArithmeticOperator; + use crate::kernels::broadcast::broadcast_value; + + let apply = |op| { + broadcast_value( + op, + Value::Scalar(Scalar::Int32(7)), + Value::Scalar(Scalar::Int32(2)), + ) + .unwrap() + }; + + let Value::Scalar(Scalar::Int32(diff)) = apply(ArithmeticOperator::Subtract) else { + panic!("Expected Int32 scalar from subtraction"); + }; + assert_eq!(diff, 5); + + let Value::Scalar(Scalar::Int32(prod)) = apply(ArithmeticOperator::Multiply) else { + panic!("Expected Int32 scalar from multiplication"); + }; + assert_eq!(prod, 14); + + // Integer scalar division is true division with a Float64 result. + let Value::Scalar(Scalar::Float64(quot)) = apply(ArithmeticOperator::Divide) else { + panic!("Expected Float64 scalar from division"); + }; + assert_eq!(quot, 3.5); + + let Value::Scalar(Scalar::Int32(rem)) = apply(ArithmeticOperator::Remainder) else { + panic!("Expected Int32 scalar from remainder"); + }; + assert_eq!(rem, 1); + + let Value::Scalar(Scalar::Int32(pow)) = apply(ArithmeticOperator::Power) else { + panic!("Expected Int32 scalar from power"); + }; + assert_eq!(pow, 49); + + let Value::Scalar(Scalar::Int32(floor)) = apply(ArithmeticOperator::FloorDiv) else { + panic!("Expected Int32 scalar from floor division"); + }; + assert_eq!(floor, 3); + + // Remainder keeps the dividend's sign and floor division rounds + // towards negative infinity. + let negative = |op| { + broadcast_value( + op, + Value::Scalar(Scalar::Int32(-7)), + Value::Scalar(Scalar::Int32(2)), + ) + .unwrap() + }; + let Value::Scalar(Scalar::Int32(rem)) = negative(ArithmeticOperator::Remainder) else { + panic!("Expected Int32 scalar from remainder"); + }; + assert_eq!(rem, -1); + let Value::Scalar(Scalar::Int32(floor)) = negative(ArithmeticOperator::FloorDiv) else { + panic!("Expected Int32 scalar from floor division"); + }; + assert_eq!(floor, -4); + + let float = |op| { + broadcast_value( + op, + Value::Scalar(Scalar::Float64(7.5)), + Value::Scalar(Scalar::Float64(2.0)), + ) + .unwrap() + }; + let Value::Scalar(Scalar::Float64(rem)) = float(ArithmeticOperator::Remainder) else { + panic!("Expected Float64 scalar from remainder"); + }; + assert_eq!(rem, 1.5); + let Value::Scalar(Scalar::Float64(pow)) = float(ArithmeticOperator::Power) else { + panic!("Expected Float64 scalar from power"); + }; + assert!((pow - 56.25).abs() < 1e-9); + let Value::Scalar(Scalar::Float64(floor)) = float(ArithmeticOperator::FloorDiv) else { + panic!("Expected Float64 scalar from floor division"); + }; + assert_eq!(floor, 3.0); + + // A zero divisor and a negative integer exponent return errors. + let zero = Value::Scalar(Scalar::Int32(0)); + let seven = Value::Scalar(Scalar::Int32(7)); + let negative_one = Value::Scalar(Scalar::Int32(-1)); + assert!( + broadcast_value(ArithmeticOperator::Remainder, seven.clone(), zero.clone()).is_err() + ); + assert!(broadcast_value(ArithmeticOperator::FloorDiv, seven.clone(), zero).is_err()); + assert!(broadcast_value(ArithmeticOperator::Power, seven, negative_one).is_err()); + } + #[test] fn test_reference_operations() { let arr1 = diff --git a/src/kernels/broadcast/mod.rs b/src/kernels/broadcast/mod.rs index f2c39ea..a0a062c 100644 --- a/src/kernels/broadcast/mod.rs +++ b/src/kernels/broadcast/mod.rs @@ -276,7 +276,7 @@ pub fn broadcast_value( // Scalar + Scalar = Scalar #[cfg(feature = "scalar_type")] (Value::Scalar(l), Value::Scalar(r)) => { - scalar_arithmetic(l, r, ArithmeticOperator::Add).map(Value::Scalar) + scalar_arithmetic(l, r, op).map(Value::Scalar) } // Array types - use resolve_binary_arithmetic diff --git a/src/kernels/routing/arithmetic.rs b/src/kernels/routing/arithmetic.rs index 1e464a0..421e8c5 100644 --- a/src/kernels/routing/arithmetic.rs +++ b/src/kernels/routing/arithmetic.rs @@ -28,6 +28,10 @@ use crate::kernels::arithmetic::{ }, string_ops::apply_str_str, }; +#[cfg(feature = "scalar_type")] +use crate::kernels::arithmetic::std::int_dense_body_std; +#[cfg(feature = "scalar_type")] +use num_traits::ToPrimitive; #[cfg(feature = "decimal")] use crate::kernels::arithmetic::decimal::{decimal_binary, integer_to_decimal, max_precision}; @@ -43,6 +47,38 @@ pub fn scalar_arithmetic( use ArithmeticOperator::*; use Scalar; + // Computes `Remainder`, `Power` or `FloorDiv` for two integer scalars with the + // integer array kernel `int_dense_body_std`, applied to one-element slices. + // + // ## Behaviour + // - Remainder keeps the dividend's sign. + // - Floor division rounds towards negative infinity and keeps the integer type. + // - Power uses wrapping exponentiation by squaring. + // - The kernel panics on a zero divisor, an overflowing division and an + // exponent outside `u32`. These inputs are checked first and return + // `MinarrowError::KernelError`. + macro_rules! integer_scalar_op { + ($l:expr, $r:expr, $op:expr) => {{ + let (l, r, op) = ($l, $r, $op); + if op == Power { + if r.to_u32().is_none() { + return Err(MinarrowError::KernelError(Some(format!( + "Power exponent {} is outside the u32 range. Consider casting to float first.", + r + )))); + } + } else if l.checked_rem(r).is_none() { + return Err(MinarrowError::KernelError(Some(format!( + "Integer {:?} of {} by {} has a zero divisor or overflows", + op, l, r + )))); + } + let mut out = [l]; + int_dense_body_std(op, &[l], &[r], &mut out); + out[0] + }}; + } + let result = match (lhs, rhs, op) { // Int32 operations (Scalar::Int32(l), Scalar::Int32(r), Add) => Scalar::Int32(l + r), @@ -51,24 +87,37 @@ pub fn scalar_arithmetic( // Integer scalar division is true division and returns a Float64 // scalar, so 7 / 2 is 3.5 and division by zero follows IEEE 754. (Scalar::Int32(l), Scalar::Int32(r), Divide) => Scalar::Float64(l as f64 / r as f64), + (Scalar::Int32(l), Scalar::Int32(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::Int32(integer_scalar_op!(l, r, op)) + } // Int64 operations (Scalar::Int64(l), Scalar::Int64(r), Add) => Scalar::Int64(l + r), (Scalar::Int64(l), Scalar::Int64(r), Subtract) => Scalar::Int64(l - r), (Scalar::Int64(l), Scalar::Int64(r), Multiply) => Scalar::Int64(l * r), (Scalar::Int64(l), Scalar::Int64(r), Divide) => Scalar::Float64(l as f64 / r as f64), + (Scalar::Int64(l), Scalar::Int64(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::Int64(integer_scalar_op!(l, r, op)) + } // Float32 operations (Scalar::Float32(l), Scalar::Float32(r), Add) => Scalar::Float32(l + r), (Scalar::Float32(l), Scalar::Float32(r), Subtract) => Scalar::Float32(l - r), (Scalar::Float32(l), Scalar::Float32(r), Multiply) => Scalar::Float32(l * r), (Scalar::Float32(l), Scalar::Float32(r), Divide) => Scalar::Float32(l / r), + (Scalar::Float32(l), Scalar::Float32(r), Remainder) => Scalar::Float32(l % r), + // Float power uses logarithmic exponentiation, matching the float array kernels. + (Scalar::Float32(l), Scalar::Float32(r), Power) => Scalar::Float32((r * l.ln()).exp()), + (Scalar::Float32(l), Scalar::Float32(r), FloorDiv) => Scalar::Float32((l / r).floor()), // Float64 operations (Scalar::Float64(l), Scalar::Float64(r), Add) => Scalar::Float64(l + r), (Scalar::Float64(l), Scalar::Float64(r), Subtract) => Scalar::Float64(l - r), (Scalar::Float64(l), Scalar::Float64(r), Multiply) => Scalar::Float64(l * r), (Scalar::Float64(l), Scalar::Float64(r), Divide) => Scalar::Float64(l / r), + (Scalar::Float64(l), Scalar::Float64(r), Remainder) => Scalar::Float64(l % r), + (Scalar::Float64(l), Scalar::Float64(r), Power) => Scalar::Float64((r * l.ln()).exp()), + (Scalar::Float64(l), Scalar::Float64(r), FloorDiv) => Scalar::Float64((l / r).floor()), // Mixed type promotion (Int + Float = Float) (Scalar::Int32(l), Scalar::Float32(r), op) => { @@ -93,6 +142,10 @@ pub fn scalar_arithmetic( (Scalar::Int8(l), Scalar::Int8(r), Multiply) => Scalar::Int8(l * r), #[cfg(feature = "extended_numeric_types")] (Scalar::Int8(l), Scalar::Int8(r), Divide) => Scalar::Float64(l as f64 / r as f64), + #[cfg(feature = "extended_numeric_types")] + (Scalar::Int8(l), Scalar::Int8(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::Int8(integer_scalar_op!(l, r, op)) + } // Int16 #[cfg(feature = "extended_numeric_types")] @@ -103,6 +156,10 @@ pub fn scalar_arithmetic( (Scalar::Int16(l), Scalar::Int16(r), Multiply) => Scalar::Int16(l * r), #[cfg(feature = "extended_numeric_types")] (Scalar::Int16(l), Scalar::Int16(r), Divide) => Scalar::Float64(l as f64 / r as f64), + #[cfg(feature = "extended_numeric_types")] + (Scalar::Int16(l), Scalar::Int16(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::Int16(integer_scalar_op!(l, r, op)) + } // UInt8 #[cfg(feature = "extended_numeric_types")] @@ -113,6 +170,10 @@ pub fn scalar_arithmetic( (Scalar::UInt8(l), Scalar::UInt8(r), Multiply) => Scalar::UInt8(l * r), #[cfg(feature = "extended_numeric_types")] (Scalar::UInt8(l), Scalar::UInt8(r), Divide) => Scalar::Float64(l as f64 / r as f64), + #[cfg(feature = "extended_numeric_types")] + (Scalar::UInt8(l), Scalar::UInt8(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::UInt8(integer_scalar_op!(l, r, op)) + } // UInt16 #[cfg(feature = "extended_numeric_types")] @@ -123,18 +184,28 @@ pub fn scalar_arithmetic( (Scalar::UInt16(l), Scalar::UInt16(r), Multiply) => Scalar::UInt16(l * r), #[cfg(feature = "extended_numeric_types")] (Scalar::UInt16(l), Scalar::UInt16(r), Divide) => Scalar::Float64(l as f64 / r as f64), + #[cfg(feature = "extended_numeric_types")] + (Scalar::UInt16(l), Scalar::UInt16(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::UInt16(integer_scalar_op!(l, r, op)) + } // UInt32 (Scalar::UInt32(l), Scalar::UInt32(r), Add) => Scalar::UInt32(l + r), (Scalar::UInt32(l), Scalar::UInt32(r), Subtract) => Scalar::UInt32(l - r), (Scalar::UInt32(l), Scalar::UInt32(r), Multiply) => Scalar::UInt32(l * r), (Scalar::UInt32(l), Scalar::UInt32(r), Divide) => Scalar::Float64(l as f64 / r as f64), + (Scalar::UInt32(l), Scalar::UInt32(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::UInt32(integer_scalar_op!(l, r, op)) + } // UInt64 (Scalar::UInt64(l), Scalar::UInt64(r), Add) => Scalar::UInt64(l + r), (Scalar::UInt64(l), Scalar::UInt64(r), Subtract) => Scalar::UInt64(l - r), (Scalar::UInt64(l), Scalar::UInt64(r), Multiply) => Scalar::UInt64(l * r), (Scalar::UInt64(l), Scalar::UInt64(r), Divide) => Scalar::Float64(l as f64 / r as f64), + (Scalar::UInt64(l), Scalar::UInt64(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::UInt64(integer_scalar_op!(l, r, op)) + } // String concatenation (Scalar::String32(l), Scalar::String32(r), Add) => Scalar::String32(format!("{}{}", l, r)), #[cfg(feature = "large_string")]