This is an automated email from the ASF dual-hosted git repository.
Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 291efc247b Add interval mul/div support for Float64 (#10409)
291efc247b is described below
commit 291efc247b9639c0bf87d2621f679b6cce4942bf
Author: Peter Lee <[email protected]>
AuthorDate: Wed Aug 5 09:18:44 2026 +0800
Add interval mul/div support for Float64 (#10409)
# Which issue does this PR close?
- Closes https://github.com/apache/arrow-rs/issues/6335.
- Closes https://github.com/apache/arrow-rs/issues/9030.
# Rationale for this change
#10336 added interval multiplication by `Int64`. Floating-point
multiplication and division require preserving calendar components
rather than flattening the entire interval into a duration.
This PR adapts DuckDB's `INTERVAL * DOUBLE` behavior. Whole months and
days remain calendar components, while fractional months cascade to days
using 30 days per month and fractional days cascade to nanoseconds using
24 hours per day.
DuckDB explicitly attributes its implementation to PostgreSQL's
`interval_mul`:
- [DuckDB
implementation](https://github.com/duckdb/duckdb/blob/21aca0424f1faf78b593b1e6fbfdd4846624c987/src/function/scalar/operator/multiply.cpp#L48-L123)
- [PostgreSQL
implementation](https://github.com/postgres/postgres/blob/78758d37306cd89ab060f00cb06f249018d5b8da/src/backend/utils/adt/timestamp.c#L3627-L3744)
# What changes are included in this PR?
Add checked arithmetic for:
- `Interval(MonthDayNano) * Float64` (communicative)
- `Interval(MonthDayNano) / Float64`
- Other interval type or the factor type are expected to be coerced by
the caller.
All results use `Interval(MonthDayNano)`.
# Are these changes tested?
- reuses the checked integer path for integral `Float64` factors,
preserving exact nanoseconds
- implements division by multiplying by the reciprocal, following
[DuckDB](https://github.com/duckdb/duckdb/blob/21aca0424f1faf78b593b1e6fbfdd4846624c987/src/function/scalar/operator/arithmetic.cpp#L1102-L1110)
- uses ties-to-even rounding at nanosecond precision
- checks division by zero and component overflow
- preserves array, scalar, and null behavior
`Interval / Int64`, `Float64 / Interval`, wrapping multiplication, and
additional numeric factor types are not added.
# Are these changes tested?
Yes. Tests cover all three interval units, both multiplication operand
orders, division, array and scalar operands, null propagation, component
cascading, integral-factor precision, negative values, ties-to-even
rounding, non-finite values, division by zero, and overflow.
```shell
cargo fmt --all -- --check
cargo test -p arrow-arith
cargo clippy -p arrow-arith --all-targets --all-features -- -D warnings
```
# Are there any user-facing changes?
Yes. The checked `mul` and `div` kernels now accept the combinations
listed above. Floating-point interval arithmetic always returns
`Interval(MonthDayNano)`.
There are no public API signature changes.
---------
Co-authored-by: Jeffrey Vo <[email protected]>
---
arrow-arith/src/numeric.rs | 329 ++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 313 insertions(+), 16 deletions(-)
diff --git a/arrow-arith/src/numeric.rs b/arrow-arith/src/numeric.rs
index cc94006fae..533c6d330b 100644
--- a/arrow-arith/src/numeric.rs
+++ b/arrow-arith/src/numeric.rs
@@ -22,11 +22,13 @@ use std::fmt::Formatter;
use std::sync::Arc;
use arrow_array::cast::AsArray;
+use arrow_array::temporal_conversions::{NANOSECONDS, SECONDS_IN_DAY};
use arrow_array::timezone::Tz;
use arrow_array::types::*;
use arrow_array::*;
use arrow_buffer::{ArrowNativeType, IntervalDayTime, IntervalMonthDayNano};
use arrow_schema::{ArrowError, DataType, IntervalUnit, TimeUnit};
+use num_traits::ToPrimitive;
use crate::arity::{binary, try_binary};
@@ -212,7 +214,10 @@ impl std::fmt::Display for Op {
impl Op {
fn commutative(&self) -> bool {
- matches!(self, Self::Add | Self::AddWrapping)
+ matches!(
+ self,
+ Self::Add | Self::AddWrapping | Self::Mul | Self::MulWrapping
+ )
}
}
@@ -243,15 +248,10 @@ fn arithmetic_op(op: Op, lhs: &dyn Datum, rhs: &dyn
Datum) -> Result<ArrayRef, A
(Duration(Millisecond), Duration(Millisecond)) =>
duration_op::<DurationMillisecondType>(op, l, l_scalar, r, r_scalar),
(Duration(Microsecond), Duration(Microsecond)) =>
duration_op::<DurationMicrosecondType>(op, l, l_scalar, r, r_scalar),
(Duration(Nanosecond), Duration(Nanosecond)) =>
duration_op::<DurationNanosecondType>(op, l, l_scalar, r, r_scalar),
- (Interval(YearMonth), Int64) if matches!(op, Op::Mul) =>
interval_mul_op::<IntervalYearMonthType>(l, l_scalar, r, r_scalar),
- (Interval(DayTime), Int64) if matches!(op, Op::Mul) =>
interval_mul_op::<IntervalDayTimeType>(l, l_scalar, r, r_scalar),
- (Interval(MonthDayNano), Int64) if matches!(op, Op::Mul) =>
interval_mul_op::<IntervalMonthDayNanoType>(l, l_scalar, r, r_scalar),
- (Int64, Interval(YearMonth)) if matches!(op, Op::Mul) =>
interval_mul_op::<IntervalYearMonthType>(r, r_scalar, l, l_scalar),
- (Int64, Interval(DayTime)) if matches!(op, Op::Mul) =>
interval_mul_op::<IntervalDayTimeType>(r, r_scalar, l, l_scalar),
- (Int64, Interval(MonthDayNano)) if matches!(op, Op::Mul) =>
interval_mul_op::<IntervalMonthDayNanoType>(r, r_scalar, l, l_scalar),
- (Interval(YearMonth), Interval(YearMonth)) =>
interval_op::<IntervalYearMonthType>(op, l, l_scalar, r, r_scalar),
- (Interval(DayTime), Interval(DayTime)) =>
interval_op::<IntervalDayTimeType>(op, l, l_scalar, r, r_scalar),
- (Interval(MonthDayNano), Interval(MonthDayNano)) =>
interval_op::<IntervalMonthDayNanoType>(op, l, l_scalar, r, r_scalar),
+ (Interval(YearMonth), Interval(YearMonth) | Int64) =>
interval_op::<IntervalYearMonthType>(op, l, l_scalar, r, r_scalar),
+ (Interval(DayTime), Interval(DayTime) | Int64) =>
interval_op::<IntervalDayTimeType>(op, l, l_scalar, r, r_scalar),
+ (Interval(MonthDayNano), Interval(MonthDayNano) | Int64) =>
interval_op::<IntervalMonthDayNanoType>(op, l, l_scalar, r, r_scalar),
+ (Interval(MonthDayNano), Float64) => interval_f64_op(op, l, l_scalar,
r, r_scalar),
(Date32, _) => date_op::<Date32Type>(op, l, l_scalar, r, r_scalar),
(Date64, _) => date_op::<Date64Type>(op, l, l_scalar, r, r_scalar),
(Decimal32(_, _), Decimal32(_, _)) => decimal_op::<Decimal32Type>(op,
l, l_scalar, r, r_scalar),
@@ -262,6 +262,11 @@ fn arithmetic_op(op: Op, lhs: &dyn Datum, rhs: &dyn Datum)
-> Result<ArrayRef, A
(Duration(_) | Interval(_), Date32 | Date64 | Timestamp(_, _)) if
op.commutative() => {
arithmetic_op(op, rhs, lhs)
}
+ (Int64, Interval(_)) | (Float64, Interval(MonthDayNano))
+ if matches!(op, Op::Mul) =>
+ {
+ arithmetic_op(op, rhs, lhs)
+ }
_ => Err(ArrowError::InvalidArgumentError(
format!("Invalid arithmetic operation: {l_t} {op} {r_t}")
))
@@ -724,6 +729,123 @@ fn interval_mul_op<T: IntervalOp>(
))
}
+/// Multiplies an `IntervalMonthDayNano` by an `f64`, mirroring DuckDB's
+/// `interval_t` layout of months, days, and a sub-day component (nanoseconds
in
+/// Arrow, microseconds in DuckDB).
+///
<https://github.com/duckdb/duckdb/blob/21aca0424f1faf78b593b1e6fbfdd4846624c987/src/include/duckdb/common/types/interval.hpp#L24-L27>
+///
+/// Algorithm:
+///
+/// 1. use checked integer multiplication when `factor` fits in `i64` (early
return).
+/// 2. multiply months and days separately
+/// 3. cascade remainders: convert fractional months to days using 30
+/// days per month, then fractional days to a sub-day value using 24 hours
+/// per day.
+/// 4. combine the cascaded remainder with the scaled input nanoseconds and
round ties-to-even at nanosecond precision.
+/// 5. return an overflow error if any output component is out of
+/// range.
+fn interval_mul_f64(
+ interval: IntervalMonthDayNano,
+ factor: f64,
+) -> Result<IntervalMonthDayNano, ArrowError> {
+ const DAYS_PER_MONTH: f64 = 30.;
+ const NANOS_PER_SECOND: f64 = NANOSECONDS as f64;
+ const SECONDS_PER_DAY: f64 = SECONDS_IN_DAY as f64;
+
+ // Keep integral factors exact instead of round-tripping i64 nanoseconds
through f64.
+ if factor.fract() == 0. {
+ if let Some(factor) = ToPrimitive::to_i64(&factor) {
+ return IntervalMonthDayNanoType::mul_i64(interval, factor);
+ }
+ }
+
+ // Based on DuckDB's INTERVAL * DOUBLE implementation, which is referenced
from PostgreSQL's interval_mul:
+ //
https://github.com/duckdb/duckdb/blob/21aca0424f1faf78b593b1e6fbfdd4846624c987/src/function/scalar/operator/multiply.cpp#L48-L123
+ // PostgreSQL's interval_mul:
+ //
https://github.com/postgres/postgres/blob/78758d37306cd89ab060f00cb06f249018d5b8da/src/backend/utils/adt/timestamp.c#L3627-L3744
+ let overflow =
+ |component| ArrowError::ArithmeticOverflow(format!("Overflow in
interval {component}"));
+ let timestamp_round =
+ |value: f64| (value * NANOS_PER_SECOND).round_ties_even() /
NANOS_PER_SECOND;
+
+ let months_product = f64::from(interval.months) * factor;
+ if !months_product.is_finite()
+ || months_product < f64::from(i32::MIN)
+ || months_product > f64::from(i32::MAX)
+ {
+ return Err(overflow("months"));
+ }
+ let months = months_product.to_i32().ok_or_else(|| overflow("months"))?;
+
+ let days_product = f64::from(interval.days) * factor;
+ if !days_product.is_finite()
+ || days_product < f64::from(i32::MIN)
+ || days_product > f64::from(i32::MAX)
+ {
+ return Err(overflow("days"));
+ }
+ let mut days = days_product.to_i32().ok_or_else(|| overflow("days"))?;
+
+ let month_remainder = timestamp_round(months_product.fract() *
DAYS_PER_MONTH);
+ let month_remainder_days = month_remainder
+ .to_i32()
+ .ok_or_else(|| overflow("month remainder"))?;
+ let mut seconds_remainder =
+ timestamp_round((days_product.fract() + month_remainder.fract()) *
SECONDS_PER_DAY);
+
+ if seconds_remainder.abs() >= SECONDS_PER_DAY {
+ let remainder_days = (seconds_remainder / SECONDS_PER_DAY)
+ .to_i32()
+ .ok_or_else(|| overflow("day remainder"))?;
+ days = days
+ .checked_add(remainder_days)
+ .ok_or_else(|| overflow("days"))?;
+ seconds_remainder -= f64::from(remainder_days) * SECONDS_PER_DAY;
+ }
+ days = days
+ .checked_add(month_remainder_days)
+ .ok_or_else(|| overflow("days"))?;
+
+ let nanoseconds = ((interval.nanoseconds as f64) * factor
+ + seconds_remainder * NANOS_PER_SECOND)
+ .round_ties_even();
+ let nanoseconds = ToPrimitive::to_i64(&nanoseconds).ok_or_else(|| {
+ ArrowError::ArithmeticOverflow(format!("Overflow in interval
nanoseconds: {nanoseconds}"))
+ })?;
+
+ Ok(IntervalMonthDayNano::new(months, days, nanoseconds))
+}
+
+fn interval_f64_op(
+ op: Op,
+ interval: &dyn Array,
+ interval_scalar: bool,
+ factor: &dyn Array,
+ factor_scalar: bool,
+) -> Result<ArrayRef, ArrowError> {
+ let interval = interval.as_primitive::<IntervalMonthDayNanoType>();
+ let factor = factor.as_primitive::<Float64Type>();
+ Ok(try_op_ref!(
+ IntervalMonthDayNanoType,
+ interval,
+ interval_scalar,
+ factor,
+ factor_scalar,
+ {
+ match op {
+ Op::Mul => interval_mul_f64(interval, factor),
+ Op::Div if factor == 0. => Err(ArrowError::DivideByZero),
+ // DuckDB defines interval division as multiplication by the
reciprocal:
+ //
https://github.com/duckdb/duckdb/blob/21aca0424f1faf78b593b1e6fbfdd4846624c987/src/function/scalar/operator/arithmetic.cpp#L1102-L1110
+ Op::Div => interval_mul_f64(interval, 1. / factor),
+ _ => Err(ArrowError::InvalidArgumentError(format!(
+ "Invalid interval arithmetic operation:
Interval(MonthDayNano) {op} Float64"
+ ))),
+ }
+ }
+ ))
+}
+
/// Perform arithmetic operation on an interval array
fn interval_op<T: IntervalOp>(
op: Op,
@@ -732,11 +854,18 @@ fn interval_op<T: IntervalOp>(
r: &dyn Array,
r_s: bool,
) -> Result<ArrayRef, ArrowError> {
- let l = l.as_primitive::<T>();
- let r = r.as_primitive::<T>();
- match op {
- Op::Add | Op::AddWrapping => Ok(try_op_ref!(T, l, l_s, r, r_s,
T::add(l, r))),
- Op::Sub | Op::SubWrapping => Ok(try_op_ref!(T, l, l_s, r, r_s,
T::sub(l, r))),
+ match (op, r.data_type()) {
+ (Op::Add | Op::AddWrapping, data_type) if data_type == l.data_type()
=> {
+ let l = l.as_primitive::<T>();
+ let r = r.as_primitive::<T>();
+ Ok(try_op_ref!(T, l, l_s, r, r_s, T::add(l, r)))
+ }
+ (Op::Sub | Op::SubWrapping, data_type) if data_type == l.data_type()
=> {
+ let l = l.as_primitive::<T>();
+ let r = r.as_primitive::<T>();
+ Ok(try_op_ref!(T, l, l_s, r, r_s, T::sub(l, r)))
+ }
+ (Op::Mul, DataType::Int64) => interval_mul_op::<T>(l, l_s, r, r_s),
_ => Err(ArrowError::InvalidArgumentError(format!(
"Invalid interval arithmetic operation: {} {op} {}",
l.data_type(),
@@ -1648,10 +1777,178 @@ mod tests {
assert_eq!(mul(&factor, &interval).unwrap().as_ref(), &expected);
let float_factor = Float64Array::new_scalar(2.);
- assert!(mul(&interval, &float_factor).is_err());
+ assert!(mul_wrapping(&float_factor, &interval).is_err());
assert!(mul_wrapping(&factor, &interval).is_err());
}
+ #[test]
+ fn test_interval_mul_div_f64() {
+ const HOUR_NANOS: i64 = 3_600_000_000_000;
+ const MINUTE_NANOS: i64 = 60_000_000_000;
+
+ // Adapted from DuckDB's interval multiplication tests:
+ //
https://github.com/duckdb/duckdb/blob/21aca0424f1faf78b593b1e6fbfdd4846624c987/test/sql/function/interval/test_interval_muldiv.test#L1-L99
+ // DuckDB's cases come from PostgreSQL's interval regression tests:
+ //
https://github.com/postgres/postgres/blob/78758d37306cd89ab060f00cb06f249018d5b8da/src/test/regress/sql/interval.sql#L118-L164
+ let interval = IntervalMonthDayNanoArray::from(vec![
+ IntervalMonthDayNanoType::make_value(41, 12, 360 * HOUR_NANOS),
+ IntervalMonthDayNanoType::make_value(-41, -12, 360 * HOUR_NANOS),
+ IntervalMonthDayNanoType::make_value(1, 1, 0),
+ IntervalMonthDayNanoType::make_value(0, 0, 1),
+ IntervalMonthDayNanoType::make_value(0, 0, 3),
+ IntervalMonthDayNanoType::make_value(0, 0, -1),
+ IntervalMonthDayNanoType::make_value(0, 0, -3),
+ ]);
+ let factor = Float64Array::from(vec![0.3, 0.3, 1.5, 0.5, 0.5, 0.5,
0.5]);
+ let expected = IntervalMonthDayNanoArray::from(vec![
+ IntervalMonthDayNanoType::make_value(12, 12, 122 * HOUR_NANOS + 24
* MINUTE_NANOS),
+ IntervalMonthDayNanoType::make_value(-12, -12, 93 * HOUR_NANOS +
36 * MINUTE_NANOS),
+ IntervalMonthDayNanoType::make_value(1, 16, 12 * HOUR_NANOS),
+ IntervalMonthDayNanoType::make_value(0, 0, 0),
+ IntervalMonthDayNanoType::make_value(0, 0, 2),
+ IntervalMonthDayNanoType::make_value(0, 0, 0),
+ IntervalMonthDayNanoType::make_value(0, 0, -2),
+ ]);
+ assert_eq!(mul(&interval, &factor).unwrap().as_ref(), &expected);
+ assert_eq!(mul(&factor, &interval).unwrap().as_ref(), &expected);
+
+ let interval =
IntervalMonthDayNanoArray::from(vec![IntervalMonthDayNanoType::make_value(
+ 9,
+ -27,
+ 45_296 * NANOSECONDS,
+ )]);
+ let factor = Float64Array::new_scalar(0.3);
+ let expected =
IntervalMonthDayNanoArray::from(vec![IntervalMonthDayNanoType::make_value(
+ 2,
+ 13,
+ 4_948_800_000_000,
+ )]);
+ assert_eq!(mul(&interval, &factor).unwrap().as_ref(), &expected);
+
+ let interval = IntervalMonthDayNanoArray::from(vec![
+ IntervalMonthDayNanoType::make_value(0, 1, 0),
+ IntervalMonthDayNanoType::make_value(4, 0, 0),
+ IntervalMonthDayNanoType::make_value(1, 1, 0),
+ IntervalMonthDayNanoType::make_value(0, 0, (1_i64 << 53) - 1),
+ IntervalMonthDayNanoType::make_value(0, 0, i64::MAX),
+ IntervalMonthDayNanoType::make_value(1, 1, 1),
+ IntervalMonthDayNanoType::make_value(1, 1, 1),
+ IntervalMonthDayNanoType::make_value(1, 0, 0),
+ ]);
+ let factor = Float64Array::from(vec![
+ 3.,
+ 5.,
+ 2.,
+ 0.7,
+ 1.,
+ f64::INFINITY,
+ f64::NEG_INFINITY,
+ -2.,
+ ]);
+ let expected = IntervalMonthDayNanoArray::from(vec![
+ IntervalMonthDayNanoType::make_value(0, 0, 8 * HOUR_NANOS),
+ IntervalMonthDayNanoType::make_value(0, 24, 0),
+ IntervalMonthDayNanoType::make_value(0, 15, 12 * HOUR_NANOS),
+ IntervalMonthDayNanoType::make_value(0, 0, 12_867_427_506_772_844),
+ IntervalMonthDayNanoType::make_value(0, 0, i64::MAX),
+ IntervalMonthDayNanoType::make_value(0, 0, 0),
+ IntervalMonthDayNanoType::make_value(0, 0, 0),
+ IntervalMonthDayNanoType::make_value(0, -15, 0),
+ ]);
+ assert_eq!(div(&interval, &factor).unwrap().as_ref(), &expected);
+
+ let null_factor = Scalar::new(Float64Array::new_null(1));
+ assert_eq!(
+ mul(&interval, &null_factor).unwrap().as_ref(),
+ &IntervalMonthDayNanoArray::new_null(interval.len())
+ );
+ }
+
+ #[test]
+ fn test_interval_mul_div_f64_errors() {
+ let factor = Float64Array::new_scalar(2.);
+ let year_month = IntervalYearMonthArray::new_scalar(1);
+ let day_time =
IntervalDayTimeArray::new_scalar(IntervalDayTime::new(1, 1));
+ for interval in [&year_month as &dyn Datum, &day_time] {
+ assert!(mul(interval, &factor).is_err());
+ assert!(mul(&factor, interval).is_err());
+ assert!(div(interval, &factor).is_err());
+ }
+
+ let interval =
+
IntervalMonthDayNanoArray::new_scalar(IntervalMonthDayNanoType::make_value(1,
1, 1));
+
+ assert!(matches!(
+ add(&interval, &factor),
+ Err(ArrowError::InvalidArgumentError(_))
+ ));
+
+ let zero = Float64Array::new_scalar(-0.);
+ assert!(matches!(
+ div(&interval, &zero),
+ Err(ArrowError::DivideByZero)
+ ));
+
+ assert!(div(&factor, &interval).is_err());
+
+ for factor in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
+ let factor = Float64Array::new_scalar(factor);
+ assert!(matches!(
+ mul(&interval, &factor),
+ Err(ArrowError::ArithmeticOverflow(_))
+ ));
+ }
+
+ let nan = Float64Array::new_scalar(f64::NAN);
+ assert!(matches!(
+ div(&interval, &nan),
+ Err(ArrowError::ArithmeticOverflow(_))
+ ));
+
+ let interval =
IntervalMonthDayNanoArray::new_scalar(IntervalMonthDayNanoType::make_value(
+ i32::MAX,
+ 0,
+ 0,
+ ));
+ assert!(matches!(
+ mul(&interval, &factor),
+ Err(ArrowError::ArithmeticOverflow(_))
+ ));
+
+ let factor = Float64Array::new_scalar(1.5);
+ let interval =
IntervalMonthDayNanoArray::new_scalar(IntervalMonthDayNanoType::make_value(
+ 0,
+ 0,
+ i64::MAX,
+ ));
+ assert!(matches!(
+ mul(&interval, &factor),
+ Err(ArrowError::ArithmeticOverflow(_))
+ ));
+
+ let factor = Float64Array::new_scalar(1.000_000_000_4);
+ let interval =
IntervalMonthDayNanoArray::new_scalar(IntervalMonthDayNanoType::make_value(
+ i32::MIN,
+ 0,
+ 0,
+ ));
+ assert!(matches!(
+ mul(&interval, &factor),
+ Err(ArrowError::ArithmeticOverflow(_))
+ ));
+
+ let factor = Float64Array::new_scalar(0.999_999_999);
+ let interval =
IntervalMonthDayNanoArray::new_scalar(IntervalMonthDayNanoType::make_value(
+ 1,
+ i32::MAX,
+ 0,
+ ));
+ assert!(matches!(
+ mul(&interval, &factor),
+ Err(ArrowError::ArithmeticOverflow(_))
+ ));
+ }
+
#[test]
fn test_interval_mul_i64_overflow() {
let interval = IntervalYearMonthArray::from(vec![i32::MAX]);