diff --git a/arrow-arith/src/numeric.rs b/arrow-arith/src/numeric.rs index a2dc39166931..2cf8fa43a917 100644 --- a/arrow-arith/src/numeric.rs +++ b/arrow-arith/src/numeric.rs @@ -510,49 +510,122 @@ fn timestamp_op( } /// Arithmetic trait for date arrays -/// -/// Note: these should be fallible (#4456) trait DateOp: ArrowTemporalType { - fn add_year_month(timestamp: Self::Native, delta: i32) -> Self::Native; - fn add_day_time(timestamp: Self::Native, delta: IntervalDayTime) -> Self::Native; - fn add_month_day_nano(timestamp: Self::Native, delta: IntervalMonthDayNano) -> Self::Native; + fn add_year_month(timestamp: Self::Native, delta: i32) -> Result; + fn add_day_time( + timestamp: Self::Native, + delta: IntervalDayTime, + ) -> Result; + fn add_month_day_nano( + timestamp: Self::Native, + delta: IntervalMonthDayNano, + ) -> Result; + + fn sub_year_month(timestamp: Self::Native, delta: i32) -> Result; + fn sub_day_time( + timestamp: Self::Native, + delta: IntervalDayTime, + ) -> Result; + fn sub_month_day_nano( + timestamp: Self::Native, + delta: IntervalMonthDayNano, + ) -> Result; +} + +impl DateOp for Date32Type { + fn add_year_month(left: Self::Native, right: i32) -> Result { + // Date32Type functions don't have _opt variants and should be safe + Ok(Self::add_year_months(left, right)) + } + + fn add_day_time( + left: Self::Native, + right: IntervalDayTime, + ) -> Result { + Ok(Self::add_day_time(left, right)) + } + + fn add_month_day_nano( + left: Self::Native, + right: IntervalMonthDayNano, + ) -> Result { + Ok(Self::add_month_day_nano(left, right)) + } - fn sub_year_month(timestamp: Self::Native, delta: i32) -> Self::Native; - fn sub_day_time(timestamp: Self::Native, delta: IntervalDayTime) -> Self::Native; - fn sub_month_day_nano(timestamp: Self::Native, delta: IntervalMonthDayNano) -> Self::Native; + fn sub_year_month(left: Self::Native, right: i32) -> Result { + Ok(Self::subtract_year_months(left, right)) + } + + fn sub_day_time( + left: Self::Native, + right: IntervalDayTime, + ) -> Result { + Ok(Self::subtract_day_time(left, right)) + } + + fn sub_month_day_nano( + left: Self::Native, + right: IntervalMonthDayNano, + ) -> Result { + Ok(Self::subtract_month_day_nano(left, right)) + } } -macro_rules! date { - ($t:ty) => { - impl DateOp for $t { - fn add_year_month(left: Self::Native, right: i32) -> Self::Native { - Self::add_year_months(left, right) - } +impl DateOp for Date64Type { + fn add_year_month(left: Self::Native, right: i32) -> Result { + Self::add_year_months_opt(left, right).ok_or_else(|| { + ArrowError::ComputeError(format!( + "Date arithmetic overflow: {} + {} months", + left, right + )) + }) + } - fn add_day_time(left: Self::Native, right: IntervalDayTime) -> Self::Native { - Self::add_day_time(left, right) - } + fn add_day_time( + left: Self::Native, + right: IntervalDayTime, + ) -> Result { + Self::add_day_time_opt(left, right).ok_or_else(|| { + ArrowError::ComputeError(format!("Date arithmetic overflow: {} + {:?}", left, right)) + }) + } - fn add_month_day_nano(left: Self::Native, right: IntervalMonthDayNano) -> Self::Native { - Self::add_month_day_nano(left, right) - } + fn add_month_day_nano( + left: Self::Native, + right: IntervalMonthDayNano, + ) -> Result { + Self::add_month_day_nano_opt(left, right).ok_or_else(|| { + ArrowError::ComputeError(format!("Date arithmetic overflow: {} + {:?}", left, right)) + }) + } - fn sub_year_month(left: Self::Native, right: i32) -> Self::Native { - Self::subtract_year_months(left, right) - } + fn sub_year_month(left: Self::Native, right: i32) -> Result { + Self::subtract_year_months_opt(left, right).ok_or_else(|| { + ArrowError::ComputeError(format!( + "Date arithmetic overflow: {} - {} months", + left, right + )) + }) + } - fn sub_day_time(left: Self::Native, right: IntervalDayTime) -> Self::Native { - Self::subtract_day_time(left, right) - } + fn sub_day_time( + left: Self::Native, + right: IntervalDayTime, + ) -> Result { + Self::subtract_day_time_opt(left, right).ok_or_else(|| { + ArrowError::ComputeError(format!("Date arithmetic overflow: {} - {:?}", left, right)) + }) + } - fn sub_month_day_nano(left: Self::Native, right: IntervalMonthDayNano) -> Self::Native { - Self::subtract_month_day_nano(left, right) - } - } - }; + fn sub_month_day_nano( + left: Self::Native, + right: IntervalMonthDayNano, + ) -> Result { + Self::subtract_month_day_nano_opt(left, right).ok_or_else(|| { + ArrowError::ComputeError(format!("Date arithmetic overflow: {} - {:?}", left, right)) + }) + } } -date!(Date32Type); -date!(Date64Type); /// Arithmetic trait for interval arrays trait IntervalOp: ArrowPrimitiveType { @@ -689,29 +762,29 @@ fn date_op( match (op, r_t) { (Op::Add | Op::AddWrapping, Interval(YearMonth)) => { let r = r.as_primitive::(); - Ok(op_ref!(T, l, l_s, r, r_s, T::add_year_month(l, r))) + Ok(try_op_ref!(T, l, l_s, r, r_s, T::add_year_month(l, r))) } (Op::Sub | Op::SubWrapping, Interval(YearMonth)) => { let r = r.as_primitive::(); - Ok(op_ref!(T, l, l_s, r, r_s, T::sub_year_month(l, r))) + Ok(try_op_ref!(T, l, l_s, r, r_s, T::sub_year_month(l, r))) } (Op::Add | Op::AddWrapping, Interval(DayTime)) => { let r = r.as_primitive::(); - Ok(op_ref!(T, l, l_s, r, r_s, T::add_day_time(l, r))) + Ok(try_op_ref!(T, l, l_s, r, r_s, T::add_day_time(l, r))) } (Op::Sub | Op::SubWrapping, Interval(DayTime)) => { let r = r.as_primitive::(); - Ok(op_ref!(T, l, l_s, r, r_s, T::sub_day_time(l, r))) + Ok(try_op_ref!(T, l, l_s, r, r_s, T::sub_day_time(l, r))) } (Op::Add | Op::AddWrapping, Interval(MonthDayNano)) => { let r = r.as_primitive::(); - Ok(op_ref!(T, l, l_s, r, r_s, T::add_month_day_nano(l, r))) + Ok(try_op_ref!(T, l, l_s, r, r_s, T::add_month_day_nano(l, r))) } (Op::Sub | Op::SubWrapping, Interval(MonthDayNano)) => { let r = r.as_primitive::(); - Ok(op_ref!(T, l, l_s, r, r_s, T::sub_month_day_nano(l, r))) + Ok(try_op_ref!(T, l, l_s, r, r_s, T::sub_month_day_nano(l, r))) } _ => Err(ArrowError::InvalidArgumentError(format!( @@ -1533,4 +1606,536 @@ mod tests { "Arithmetic overflow: Overflow happened on: 9223372036854775807 - -1" ); } + + #[test] + fn test_date64_to_naive_date_opt_boundary_values() { + use arrow_array::types::Date64Type; + + // Date64Type::to_naive_date_opt has boundaries determined by NaiveDate's supported range. + // The valid date range is from January 1, -262143 to December 31, 262142 (Gregorian calendar). + + let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); + let ms_per_day = 24 * 60 * 60 * 1000i64; + + // Define the boundary dates using NaiveDate::from_ymd_opt + let max_valid_date = NaiveDate::from_ymd_opt(262142, 12, 31).unwrap(); + let min_valid_date = NaiveDate::from_ymd_opt(-262143, 1, 1).unwrap(); + + // Calculate their millisecond values from epoch + let max_valid_millis = (max_valid_date - epoch).num_milliseconds(); + let min_valid_millis = (min_valid_date - epoch).num_milliseconds(); + + // Verify these match the expected boundaries in milliseconds + assert_eq!( + max_valid_millis, 8210266790400000i64, + "December 31, 262142 should be 8210266790400000 ms from epoch" + ); + assert_eq!( + min_valid_millis, -8334601228800000i64, + "January 1, -262143 should be -8334601228800000 ms from epoch" + ); + + // Test that the boundary dates work + assert!( + Date64Type::to_naive_date_opt(max_valid_millis).is_some(), + "December 31, 262142 should return Some" + ); + assert!( + Date64Type::to_naive_date_opt(min_valid_millis).is_some(), + "January 1, -262143 should return Some" + ); + + // Test that one day beyond the boundaries fails + assert!( + Date64Type::to_naive_date_opt(max_valid_millis + ms_per_day).is_none(), + "January 1, 262143 should return None" + ); + assert!( + Date64Type::to_naive_date_opt(min_valid_millis - ms_per_day).is_none(), + "December 31, -262144 should return None" + ); + + // Test some values well within the valid range + assert!( + Date64Type::to_naive_date_opt(0).is_some(), + "Epoch (1970-01-01) should return Some" + ); + let year_2000 = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(); + let year_2000_millis = (year_2000 - epoch).num_milliseconds(); + assert!( + Date64Type::to_naive_date_opt(year_2000_millis).is_some(), + "Year 2000 should return Some" + ); + + // Test extreme values that definitely fail due to Duration constraints + assert!( + Date64Type::to_naive_date_opt(i64::MAX).is_none(), + "i64::MAX should return None" + ); + assert!( + Date64Type::to_naive_date_opt(i64::MIN).is_none(), + "i64::MIN should return None" + ); + } + + #[test] + fn test_date64_add_year_months_opt_boundary_values() { + use arrow_array::types::Date64Type; + + let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); + + // Test normal case within valid range + let year_2000 = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(); + let year_2000_millis = (year_2000 - epoch).num_milliseconds(); + assert!( + Date64Type::add_year_months_opt(year_2000_millis, 120).is_some(), + "Adding 10 years to year 2000 should succeed" + ); + + // Test with moderate years that are within chrono's safe range + let large_year = NaiveDate::from_ymd_opt(5000, 1, 1).unwrap(); + let large_year_millis = (large_year - epoch).num_milliseconds(); + assert!( + Date64Type::add_year_months_opt(large_year_millis, 12).is_some(), + "Adding 12 months to year 5000 should succeed" + ); + + let neg_year = NaiveDate::from_ymd_opt(-5000, 12, 31).unwrap(); + let neg_year_millis = (neg_year - epoch).num_milliseconds(); + assert!( + Date64Type::add_year_months_opt(neg_year_millis, -12).is_some(), + "Subtracting 12 months from year -5000 should succeed" + ); + + // Test with extreme input values that would cause overflow + assert!( + Date64Type::add_year_months_opt(i64::MAX, 1).is_none(), + "Adding months to i64::MAX should fail" + ); + assert!( + Date64Type::add_year_months_opt(i64::MIN, -1).is_none(), + "Subtracting months from i64::MIN should fail" + ); + + // Test edge case: adding zero should always work for valid dates + assert!( + Date64Type::add_year_months_opt(year_2000_millis, 0).is_some(), + "Adding zero months should always succeed for valid dates" + ); + } + + #[test] + fn test_date64_add_day_time_opt_boundary_values() { + use arrow_array::types::Date64Type; + use arrow_buffer::IntervalDayTime; + + let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); + + // Test with a date far from the boundary but still testing the function + let near_max_date = NaiveDate::from_ymd_opt(200000, 12, 1).unwrap(); + let near_max_millis = (near_max_date - epoch).num_milliseconds(); + + // Adding 30 days should succeed + let interval_30_days = IntervalDayTime::new(30, 0); + assert!( + Date64Type::add_day_time_opt(near_max_millis, interval_30_days).is_some(), + "Adding 30 days to large year should succeed" + ); + + // Adding a very large number of days should fail + let interval_large_days = IntervalDayTime::new(100000000, 0); + assert!( + Date64Type::add_day_time_opt(near_max_millis, interval_large_days).is_none(), + "Adding 100M days to large year should fail" + ); + + // Test with a date far from the boundary in the negative direction + let near_min_date = NaiveDate::from_ymd_opt(-200000, 2, 1).unwrap(); + let near_min_millis = (near_min_date - epoch).num_milliseconds(); + + // Subtracting 30 days should succeed + let interval_minus_30_days = IntervalDayTime::new(-30, 0); + assert!( + Date64Type::add_day_time_opt(near_min_millis, interval_minus_30_days).is_some(), + "Subtracting 30 days from large negative year should succeed" + ); + + // Subtracting a very large number of days should fail + let interval_minus_large_days = IntervalDayTime::new(-100000000, 0); + assert!( + Date64Type::add_day_time_opt(near_min_millis, interval_minus_large_days).is_none(), + "Subtracting 100M days from large negative year should fail" + ); + + // Test normal case within valid range + let year_2000 = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(); + let year_2000_millis = (year_2000 - epoch).num_milliseconds(); + let interval_1000_days = IntervalDayTime::new(1000, 12345); + assert!( + Date64Type::add_day_time_opt(year_2000_millis, interval_1000_days).is_some(), + "Adding 1000 days and time to year 2000 should succeed" + ); + + // Test with extreme input values that would cause overflow + let interval_one_day = IntervalDayTime::new(1, 0); + assert!( + Date64Type::add_day_time_opt(i64::MAX, interval_one_day).is_none(), + "Adding interval to i64::MAX should fail" + ); + assert!( + Date64Type::add_day_time_opt(i64::MIN, IntervalDayTime::new(-1, 0)).is_none(), + "Subtracting interval from i64::MIN should fail" + ); + + // Test with extreme interval values + let max_interval = IntervalDayTime::new(i32::MAX, i32::MAX); + assert!( + Date64Type::add_day_time_opt(0, max_interval).is_none(), + "Adding extreme interval should fail" + ); + + let min_interval = IntervalDayTime::new(i32::MIN, i32::MIN); + assert!( + Date64Type::add_day_time_opt(0, min_interval).is_none(), + "Adding extreme negative interval should fail" + ); + + // Test millisecond overflow within a day + let large_ms_interval = IntervalDayTime::new(0, i32::MAX); + assert!( + Date64Type::add_day_time_opt(year_2000_millis, large_ms_interval).is_some(), + "Adding large milliseconds within valid range should succeed" + ); + } + + #[test] + fn test_date64_add_month_day_nano_opt_boundary_values() { + use arrow_array::types::Date64Type; + use arrow_buffer::IntervalMonthDayNano; + + let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); + + // Test with a large year that is still within chrono's safe range + let near_max_date = NaiveDate::from_ymd_opt(5000, 11, 1).unwrap(); + let near_max_millis = (near_max_date - epoch).num_milliseconds(); + + // Adding 1 month and 30 days should succeed + let interval_safe = IntervalMonthDayNano::new(1, 30, 0); + assert!( + Date64Type::add_month_day_nano_opt(near_max_millis, interval_safe).is_some(), + "Adding 1 month 30 days to large year should succeed" + ); + + // Test normal case within valid range + let year_2000 = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(); + let year_2000_millis = (year_2000 - epoch).num_milliseconds(); + + // Test edge case: adding zero should always work for valid dates + let zero_interval = IntervalMonthDayNano::new(0, 0, 0); + assert!( + Date64Type::add_month_day_nano_opt(year_2000_millis, zero_interval).is_some(), + "Adding zero interval should always succeed for valid dates" + ); + + // Test with a negative year that is still within chrono's safe range + let near_min_date = NaiveDate::from_ymd_opt(-5000, 2, 28).unwrap(); + let near_min_millis = (near_min_date - epoch).num_milliseconds(); + + // Subtracting 1 month and 30 days should succeed + let interval_safe_neg = IntervalMonthDayNano::new(-1, -30, 0); + assert!( + Date64Type::add_month_day_nano_opt(near_min_millis, interval_safe_neg).is_some(), + "Subtracting 1 month 30 days from large negative year should succeed" + ); + + // Test with extreme input values that would cause overflow + assert!( + Date64Type::add_month_day_nano_opt(i64::MAX, IntervalMonthDayNano::new(1, 0, 0)) + .is_none(), + "Adding interval to i64::MAX should fail" + ); + + let interval_normal = IntervalMonthDayNano::new(2, 10, 123_456_789_000); + assert!( + Date64Type::add_month_day_nano_opt(year_2000_millis, interval_normal).is_some(), + "Adding 2 months, 10 days, and nanos to year 2000 should succeed" + ); + + // Test with extreme input values that would cause overflow + assert!( + Date64Type::add_month_day_nano_opt(i64::MAX, IntervalMonthDayNano::new(1, 0, 0)) + .is_none(), + "Adding interval to i64::MAX should fail" + ); + assert!( + Date64Type::add_month_day_nano_opt(i64::MIN, IntervalMonthDayNano::new(-1, 0, 0)) + .is_none(), + "Subtracting interval from i64::MIN should fail" + ); + + // Test with invalid timestamp input (the _opt function should handle these gracefully) + + // Test nanosecond precision (should not affect boundary since it's < 1ms) + let nano_interval = IntervalMonthDayNano::new(0, 0, 999_999_999); + assert!( + Date64Type::add_month_day_nano_opt(year_2000_millis, nano_interval).is_some(), + "Adding nanoseconds within valid range should succeed" + ); + + // Test large nanosecond values that convert to milliseconds + let large_nano_interval = IntervalMonthDayNano::new(0, 0, 86_400_000_000_000); // 1 day in nanos + assert!( + Date64Type::add_month_day_nano_opt(year_2000_millis, large_nano_interval).is_some(), + "Adding 1 day worth of nanoseconds should succeed" + ); + } + + #[test] + fn test_date64_subtract_year_months_opt_boundary_values() { + use arrow_array::types::Date64Type; + + let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); + + // Test with a negative year that is still within chrono's safe range + let near_min_date = NaiveDate::from_ymd_opt(-5000, 12, 31).unwrap(); + let near_min_millis = (near_min_date - epoch).num_milliseconds(); + + // Subtracting 12 months should succeed + assert!( + Date64Type::subtract_year_months_opt(near_min_millis, 12).is_some(), + "Subtracting 12 months from year -5000 should succeed" + ); + + // Test normal case within valid range + let year_2000 = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(); + let year_2000_millis = (year_2000 - epoch).num_milliseconds(); + + // Test edge case: subtracting zero should always work for valid dates + assert!( + Date64Type::subtract_year_months_opt(year_2000_millis, 0).is_some(), + "Subtracting zero months should always succeed for valid dates" + ); + + // Test with a large year that is still within chrono's safe range + let near_max_date = NaiveDate::from_ymd_opt(5000, 1, 1).unwrap(); + let near_max_millis = (near_max_date - epoch).num_milliseconds(); + + // Adding 12 months (subtracting negative) should succeed + assert!( + Date64Type::subtract_year_months_opt(near_max_millis, -12).is_some(), + "Adding 12 months to year 5000 should succeed" + ); + + // Test with extreme input values that would cause overflow + assert!( + Date64Type::subtract_year_months_opt(i64::MAX, -1).is_none(), + "Adding months to i64::MAX should fail" + ); + + assert!( + Date64Type::subtract_year_months_opt(year_2000_millis, 12).is_some(), + "Subtracting 1 year from year 2000 should succeed" + ); + + // Test with extreme input values that would cause overflow + assert!( + Date64Type::subtract_year_months_opt(i64::MAX, -1).is_none(), + "Adding months to i64::MAX should fail" + ); + assert!( + Date64Type::subtract_year_months_opt(i64::MIN, 1).is_none(), + "Subtracting months from i64::MIN should fail" + ); + + // Test edge case: subtracting zero should always work for valid dates + let valid_date = NaiveDate::from_ymd_opt(2020, 6, 15).unwrap(); + let valid_millis = (valid_date - epoch).num_milliseconds(); + assert!( + Date64Type::subtract_year_months_opt(valid_millis, 0).is_some(), + "Subtracting zero months should always succeed for valid dates" + ); + } + + #[test] + fn test_date64_subtract_day_time_opt_boundary_values() { + use arrow_array::types::Date64Type; + use arrow_buffer::IntervalDayTime; + + let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); + + // Test with a date far from the boundary in the negative direction + let near_min_date = NaiveDate::from_ymd_opt(-200000, 2, 1).unwrap(); + let near_min_millis = (near_min_date - epoch).num_milliseconds(); + + // Subtracting 30 days should succeed + let interval_30_days = IntervalDayTime::new(30, 0); + assert!( + Date64Type::subtract_day_time_opt(near_min_millis, interval_30_days).is_some(), + "Subtracting 30 days from large negative year should succeed" + ); + + // Subtracting a very large number of days should fail + let interval_large_days = IntervalDayTime::new(100000000, 0); + assert!( + Date64Type::subtract_day_time_opt(near_min_millis, interval_large_days).is_none(), + "Subtracting 100M days from large negative year should fail" + ); + + // Test with a date far from the boundary but still testing the function + let near_max_date = NaiveDate::from_ymd_opt(200000, 12, 1).unwrap(); + let near_max_millis = (near_max_date - epoch).num_milliseconds(); + + // Adding 30 days (subtracting negative) should succeed + let interval_minus_30_days = IntervalDayTime::new(-30, 0); + assert!( + Date64Type::subtract_day_time_opt(near_max_millis, interval_minus_30_days).is_some(), + "Adding 30 days to large year should succeed" + ); + + // Adding a very large number of days should fail + let interval_minus_large_days = IntervalDayTime::new(-100000000, 0); + assert!( + Date64Type::subtract_day_time_opt(near_max_millis, interval_minus_large_days).is_none(), + "Adding 100M days to large year should fail" + ); + + // Test normal case within valid range + let year_2000 = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(); + let year_2000_millis = (year_2000 - epoch).num_milliseconds(); + let interval_1000_days = IntervalDayTime::new(1000, 12345); + assert!( + Date64Type::subtract_day_time_opt(year_2000_millis, interval_1000_days).is_some(), + "Subtracting 1000 days and time from year 2000 should succeed" + ); + + // Test with extreme input values that would cause overflow + let interval_one_day = IntervalDayTime::new(1, 0); + assert!( + Date64Type::subtract_day_time_opt(i64::MIN, interval_one_day).is_none(), + "Subtracting interval from i64::MIN should fail" + ); + assert!( + Date64Type::subtract_day_time_opt(i64::MAX, IntervalDayTime::new(-1, 0)).is_none(), + "Adding interval to i64::MAX should fail" + ); + + // Test with extreme interval values + let max_interval = IntervalDayTime::new(i32::MAX, i32::MAX); + assert!( + Date64Type::subtract_day_time_opt(0, max_interval).is_none(), + "Subtracting extreme interval should fail" + ); + + let min_interval = IntervalDayTime::new(i32::MIN, i32::MIN); + assert!( + Date64Type::subtract_day_time_opt(0, min_interval).is_none(), + "Subtracting extreme negative interval should fail" + ); + + // Test millisecond precision + let large_ms_interval = IntervalDayTime::new(0, i32::MAX); + assert!( + Date64Type::subtract_day_time_opt(year_2000_millis, large_ms_interval).is_some(), + "Subtracting large milliseconds within valid range should succeed" + ); + + // Test edge case: subtracting zero should always work for valid dates + let zero_interval = IntervalDayTime::new(0, 0); + let valid_date = NaiveDate::from_ymd_opt(2020, 6, 15).unwrap(); + let valid_millis = (valid_date - epoch).num_milliseconds(); + assert!( + Date64Type::subtract_day_time_opt(valid_millis, zero_interval).is_some(), + "Subtracting zero interval should always succeed for valid dates" + ); + } + + #[test] + fn test_date64_subtract_month_day_nano_opt_boundary_values() { + use arrow_array::types::Date64Type; + use arrow_buffer::IntervalMonthDayNano; + + let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); + + // Test with a negative year that is still within chrono's safe range + let near_min_date = NaiveDate::from_ymd_opt(-5000, 2, 28).unwrap(); + let near_min_millis = (near_min_date - epoch).num_milliseconds(); + + // Subtracting 1 month and 30 days should succeed + let interval_safe = IntervalMonthDayNano::new(1, 30, 0); + assert!( + Date64Type::subtract_month_day_nano_opt(near_min_millis, interval_safe).is_some(), + "Subtracting 1 month 30 days from large negative year should succeed" + ); + + // Test normal case within valid range + let year_2000 = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(); + let year_2000_millis = (year_2000 - epoch).num_milliseconds(); + + // Test edge case: subtracting zero should always work for valid dates + let zero_interval = IntervalMonthDayNano::new(0, 0, 0); + assert!( + Date64Type::subtract_month_day_nano_opt(year_2000_millis, zero_interval).is_some(), + "Subtracting zero interval should always succeed for valid dates" + ); + + // Test with a large year that is still within chrono's safe range + let near_max_date = NaiveDate::from_ymd_opt(5000, 11, 1).unwrap(); + let near_max_millis = (near_max_date - epoch).num_milliseconds(); + + // Adding 1 month and 30 days (subtracting negative) should succeed + let interval_safe_neg = IntervalMonthDayNano::new(-1, -30, 0); + assert!( + Date64Type::subtract_month_day_nano_opt(near_max_millis, interval_safe_neg).is_some(), + "Adding 1 month 30 days to large year should succeed" + ); + + // Test with extreme input values that would cause overflow + assert!( + Date64Type::subtract_month_day_nano_opt(i64::MIN, IntervalMonthDayNano::new(1, 0, 0)) + .is_none(), + "Subtracting interval from i64::MIN should fail" + ); + + let interval_normal = IntervalMonthDayNano::new(2, 10, 123_456_789_000); + assert!( + Date64Type::subtract_month_day_nano_opt(year_2000_millis, interval_normal).is_some(), + "Subtracting 2 months, 10 days, and nanos from year 2000 should succeed" + ); + + // Test with extreme input values that would cause overflow + assert!( + Date64Type::subtract_month_day_nano_opt(i64::MIN, IntervalMonthDayNano::new(1, 0, 0)) + .is_none(), + "Subtracting interval from i64::MIN should fail" + ); + assert!( + Date64Type::subtract_month_day_nano_opt(i64::MAX, IntervalMonthDayNano::new(-1, 0, 0)) + .is_none(), + "Adding interval to i64::MAX should fail" + ); + + // Test nanosecond precision (should not affect boundary since it's < 1ms) + let nano_interval = IntervalMonthDayNano::new(0, 0, 999_999_999); + assert!( + Date64Type::subtract_month_day_nano_opt(year_2000_millis, nano_interval).is_some(), + "Subtracting nanoseconds within valid range should succeed" + ); + + // Test large nanosecond values that convert to milliseconds + let large_nano_interval = IntervalMonthDayNano::new(0, 0, 86_400_000_000_000); // 1 day in nanos + assert!( + Date64Type::subtract_month_day_nano_opt(year_2000_millis, large_nano_interval) + .is_some(), + "Subtracting 1 day worth of nanoseconds should succeed" + ); + + // Test edge case: subtracting zero should always work for valid dates + let zero_interval = IntervalMonthDayNano::new(0, 0, 0); + let valid_date = NaiveDate::from_ymd_opt(2020, 6, 15).unwrap(); + let valid_millis = (valid_date - epoch).num_milliseconds(); + assert!( + Date64Type::subtract_month_day_nano_opt(valid_millis, zero_interval).is_some(), + "Subtracting zero interval should always succeed for valid dates" + ); + } } diff --git a/arrow-array/src/types.rs b/arrow-array/src/types.rs index 3d8cfcdb112b..d7d60cfdc92d 100644 --- a/arrow-array/src/types.rs +++ b/arrow-array/src/types.rs @@ -1031,11 +1031,27 @@ impl Date64Type { /// # Arguments /// /// * `i` - The Date64Type to convert + #[deprecated] pub fn to_naive_date(i: ::Native) -> NaiveDate { let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); epoch.add(Duration::try_milliseconds(i).unwrap()) } + /// Converts an arrow Date64Type into a chrono::NaiveDateTime if it fits in the range that chrono::NaiveDateTime can represent. + /// Returns `None` if the calculation would overflow or underflow. + /// + /// This function is able to handle dates ranging between 1677-09-21 (-9,223,372,800,000) and 2262-04-11 (9,223,286,400,000). + /// + /// # Arguments + /// + /// * `i` - The Date64Type to convert + /// + /// Returns `Some(NaiveDateTime)` if it fits, `None` otherwise. + pub fn to_naive_date_opt(i: ::Native) -> Option { + let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); + Duration::try_milliseconds(i).and_then(|d| epoch.checked_add_signed(d)) + } + /// Converts a chrono::NaiveDate into an arrow Date64Type /// /// # Arguments @@ -1052,14 +1068,38 @@ impl Date64Type { /// /// * `date` - The date on which to perform the operation /// * `delta` - The interval to add + #[deprecated( + since = "56.0.0", + note = "Use `add_year_months_opt` instead, which returns an Option to handle overflow." + )] pub fn add_year_months( date: ::Native, delta: ::Native, ) -> ::Native { - let prior = Date64Type::to_naive_date(date); + Self::add_year_months_opt(date, delta).unwrap_or_else(|| { + panic!( + "Date64Type::add_year_months overflowed for date: {}, delta: {}", + date, delta + ) + }) + } + + /// Adds the given IntervalYearMonthType to an arrow Date64Type + /// + /// # Arguments + /// + /// * `date` - The date on which to perform the operation + /// * `delta` - The interval to add + /// + /// Returns `Some(Date64Type)` if it fits, `None` otherwise. + pub fn add_year_months_opt( + date: ::Native, + delta: ::Native, + ) -> Option<::Native> { + let prior = Date64Type::to_naive_date_opt(date)?; let months = IntervalYearMonthType::to_months(delta); let posterior = shift_months(prior, months); - Date64Type::from_naive_date(posterior) + Some(Date64Type::from_naive_date(posterior)) } /// Adds the given IntervalDayTimeType to an arrow Date64Type @@ -1068,15 +1108,39 @@ impl Date64Type { /// /// * `date` - The date on which to perform the operation /// * `delta` - The interval to add + #[deprecated( + since = "56.0.0", + note = "Use `add_day_time_opt` instead, which returns an Option to handle overflow." + )] pub fn add_day_time( date: ::Native, delta: ::Native, ) -> ::Native { + Self::add_day_time_opt(date, delta).unwrap_or_else(|| { + panic!( + "Date64Type::add_day_time overflowed for date: {}, delta: {:?}", + date, delta + ) + }) + } + + /// Adds the given IntervalDayTimeType to an arrow Date64Type + /// + /// # Arguments + /// + /// * `date` - The date on which to perform the operation + /// * `delta` - The interval to add + /// + /// Returns `Some(Date64Type)` if it fits, `None` otherwise. + pub fn add_day_time_opt( + date: ::Native, + delta: ::Native, + ) -> Option<::Native> { let (days, ms) = IntervalDayTimeType::to_parts(delta); - let res = Date64Type::to_naive_date(date); - let res = res.add(Duration::try_days(days as i64).unwrap()); - let res = res.add(Duration::try_milliseconds(ms as i64).unwrap()); - Date64Type::from_naive_date(res) + let res = Date64Type::to_naive_date_opt(date)?; + let res = res.checked_add_signed(Duration::try_days(days as i64)?)?; + let res = res.checked_add_signed(Duration::try_milliseconds(ms as i64)?)?; + Some(Date64Type::from_naive_date(res)) } /// Adds the given IntervalMonthDayNanoType to an arrow Date64Type @@ -1085,16 +1149,40 @@ impl Date64Type { /// /// * `date` - The date on which to perform the operation /// * `delta` - The interval to add + #[deprecated( + since = "56.0.0", + note = "Use `add_month_day_nano_opt` instead, which returns an Option to handle overflow." + )] pub fn add_month_day_nano( date: ::Native, delta: ::Native, ) -> ::Native { + Self::add_month_day_nano_opt(date, delta).unwrap_or_else(|| { + panic!( + "Date64Type::add_month_day_nano overflowed for date: {}, delta: {:?}", + date, delta + ) + }) + } + + /// Adds the given IntervalMonthDayNanoType to an arrow Date64Type + /// + /// # Arguments + /// + /// * `date` - The date on which to perform the operation + /// * `delta` - The interval to add + /// + /// Returns `Some(Date64Type)` if it fits, `None` otherwise. + pub fn add_month_day_nano_opt( + date: ::Native, + delta: ::Native, + ) -> Option<::Native> { let (months, days, nanos) = IntervalMonthDayNanoType::to_parts(delta); - let res = Date64Type::to_naive_date(date); + let res = Date64Type::to_naive_date_opt(date)?; let res = shift_months(res, months); - let res = res.add(Duration::try_days(days as i64).unwrap()); - let res = res.add(Duration::nanoseconds(nanos)); - Date64Type::from_naive_date(res) + let res = res.checked_add_signed(Duration::try_days(days as i64)?)?; + let res = res.checked_add_signed(Duration::nanoseconds(nanos))?; + Some(Date64Type::from_naive_date(res)) } /// Subtract the given IntervalYearMonthType to an arrow Date64Type @@ -1103,14 +1191,38 @@ impl Date64Type { /// /// * `date` - The date on which to perform the operation /// * `delta` - The interval to subtract + #[deprecated( + since = "56.0.0", + note = "Use `subtract_year_months_opt` instead, which returns an Option to handle overflow." + )] pub fn subtract_year_months( date: ::Native, delta: ::Native, ) -> ::Native { - let prior = Date64Type::to_naive_date(date); + Self::subtract_year_months_opt(date, delta).unwrap_or_else(|| { + panic!( + "Date64Type::subtract_year_months overflowed for date: {}, delta: {}", + date, delta + ) + }) + } + + /// Subtract the given IntervalYearMonthType to an arrow Date64Type + /// + /// # Arguments + /// + /// * `date` - The date on which to perform the operation + /// * `delta` - The interval to subtract + /// + /// Returns `Some(Date64Type)` if it fits, `None` otherwise. + pub fn subtract_year_months_opt( + date: ::Native, + delta: ::Native, + ) -> Option<::Native> { + let prior = Date64Type::to_naive_date_opt(date)?; let months = IntervalYearMonthType::to_months(-delta); let posterior = shift_months(prior, months); - Date64Type::from_naive_date(posterior) + Some(Date64Type::from_naive_date(posterior)) } /// Subtract the given IntervalDayTimeType to an arrow Date64Type @@ -1119,15 +1231,39 @@ impl Date64Type { /// /// * `date` - The date on which to perform the operation /// * `delta` - The interval to subtract + #[deprecated( + since = "56.0.0", + note = "Use `subtract_day_time_opt` instead, which returns an Option to handle overflow." + )] pub fn subtract_day_time( date: ::Native, delta: ::Native, ) -> ::Native { + Self::subtract_day_time_opt(date, delta).unwrap_or_else(|| { + panic!( + "Date64Type::subtract_day_time overflowed for date: {}, delta: {:?}", + date, delta + ) + }) + } + + /// Subtract the given IntervalDayTimeType to an arrow Date64Type + /// + /// # Arguments + /// + /// * `date` - The date on which to perform the operation + /// * `delta` - The interval to subtract + /// + /// Returns `Some(Date64Type)` if it fits, `None` otherwise. + pub fn subtract_day_time_opt( + date: ::Native, + delta: ::Native, + ) -> Option<::Native> { let (days, ms) = IntervalDayTimeType::to_parts(delta); - let res = Date64Type::to_naive_date(date); - let res = res.sub(Duration::try_days(days as i64).unwrap()); - let res = res.sub(Duration::try_milliseconds(ms as i64).unwrap()); - Date64Type::from_naive_date(res) + let res = Date64Type::to_naive_date_opt(date)?; + let res = res.checked_sub_signed(Duration::try_days(days as i64)?)?; + let res = res.checked_sub_signed(Duration::try_milliseconds(ms as i64)?)?; + Some(Date64Type::from_naive_date(res)) } /// Subtract the given IntervalMonthDayNanoType to an arrow Date64Type @@ -1136,16 +1272,40 @@ impl Date64Type { /// /// * `date` - The date on which to perform the operation /// * `delta` - The interval to subtract + #[deprecated( + since = "56.0.0", + note = "Use `subtract_month_day_nano_opt` instead, which returns an Option to handle overflow." + )] pub fn subtract_month_day_nano( date: ::Native, delta: ::Native, ) -> ::Native { + Self::subtract_month_day_nano_opt(date, delta).unwrap_or_else(|| { + panic!( + "Date64Type::subtract_month_day_nano overflowed for date: {}, delta: {:?}", + date, delta + ) + }) + } + + /// Subtract the given IntervalMonthDayNanoType to an arrow Date64Type + /// + /// # Arguments + /// + /// * `date` - The date on which to perform the operation + /// * `delta` - The interval to subtract + /// + /// Returns `Some(Date64Type)` if it fits, `None` otherwise. + pub fn subtract_month_day_nano_opt( + date: ::Native, + delta: ::Native, + ) -> Option<::Native> { let (months, days, nanos) = IntervalMonthDayNanoType::to_parts(delta); - let res = Date64Type::to_naive_date(date); + let res = Date64Type::to_naive_date_opt(date)?; let res = shift_months(res, -months); - let res = res.sub(Duration::try_days(days as i64).unwrap()); - let res = res.sub(Duration::nanoseconds(nanos)); - Date64Type::from_naive_date(res) + let res = res.checked_sub_signed(Duration::try_days(days as i64)?)?; + let res = res.checked_sub_signed(Duration::nanoseconds(nanos))?; + Some(Date64Type::from_naive_date(res)) } }