pub const MICROS_PER_DAY: i64 = 86_400 * 1_000_000;
pub const MICROS_PER_MONTH: i64 = 30 * MICROS_PER_DAY;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub struct DuckInterval {
pub months: i32,
pub days: i32,
pub micros: i64,
}
impl DuckInterval {
#[inline]
#[must_use]
pub const fn zero() -> Self {
Self {
months: 0,
days: 0,
micros: 0,
}
}
#[inline]
#[must_use]
pub fn to_micros(self) -> Option<i64> {
interval_to_micros(self)
}
#[inline]
#[must_use]
pub fn to_micros_saturating(self) -> i64 {
interval_to_micros_saturating(self)
}
}
impl Default for DuckInterval {
#[inline]
fn default() -> Self {
Self::zero()
}
}
#[inline]
pub fn interval_to_micros(iv: DuckInterval) -> Option<i64> {
let months_us = i64::from(iv.months).checked_mul(MICROS_PER_MONTH)?;
let days_us = i64::from(iv.days).checked_mul(MICROS_PER_DAY)?;
months_us.checked_add(days_us)?.checked_add(iv.micros)
}
#[inline]
pub fn interval_to_micros_saturating(iv: DuckInterval) -> i64 {
interval_to_micros(iv).unwrap_or_else(|| {
let months_us = i128::from(iv.months) * i128::from(MICROS_PER_MONTH);
let days_us = i128::from(iv.days) * i128::from(MICROS_PER_DAY);
let total = months_us + days_us + i128::from(iv.micros);
if total >= 0 {
i64::MAX
} else {
i64::MIN
}
})
}
#[inline]
pub const unsafe fn read_interval_at(data: *const u8, idx: usize) -> DuckInterval {
let ptr = unsafe { data.add(idx * 16) };
let months = unsafe { core::ptr::read_unaligned(ptr.cast::<i32>()) };
let days = unsafe { core::ptr::read_unaligned(ptr.add(4).cast::<i32>()) };
let micros = unsafe { core::ptr::read_unaligned(ptr.add(8).cast::<i64>()) };
DuckInterval {
months,
days,
micros,
}
}
const _: () = {
assert!(
core::mem::size_of::<DuckInterval>() == 16,
"DuckInterval must be exactly 16 bytes"
);
assert!(
core::mem::align_of::<DuckInterval>() >= 4,
"DuckInterval must have at least 4-byte alignment"
);
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn size_of_duck_interval() {
assert_eq!(core::mem::size_of::<DuckInterval>(), 16);
}
#[test]
fn zero_interval() {
let iv = DuckInterval::zero();
assert_eq!(interval_to_micros(iv), Some(0));
}
#[test]
fn default_interval() {
let iv = DuckInterval::default();
assert_eq!(iv, DuckInterval::zero());
}
#[test]
fn one_day() {
let iv = DuckInterval {
months: 0,
days: 1,
micros: 0,
};
assert_eq!(interval_to_micros(iv), Some(MICROS_PER_DAY));
}
#[test]
fn one_month() {
let iv = DuckInterval {
months: 1,
days: 0,
micros: 0,
};
assert_eq!(interval_to_micros(iv), Some(MICROS_PER_MONTH));
}
#[test]
fn combined_interval() {
let iv = DuckInterval {
months: 0,
days: 1,
micros: 500_000,
};
let expected = MICROS_PER_DAY + 500_000;
assert_eq!(interval_to_micros(iv), Some(expected));
}
#[test]
fn negative_interval() {
let iv = DuckInterval {
months: -1,
days: 0,
micros: 0,
};
assert_eq!(interval_to_micros(iv), Some(-MICROS_PER_MONTH));
}
#[test]
fn overflow_returns_none() {
let iv = DuckInterval {
months: i32::MAX,
days: i32::MAX,
micros: i64::MAX,
};
assert_eq!(interval_to_micros(iv), None);
}
#[test]
fn saturating_on_overflow() {
let iv = DuckInterval {
months: i32::MAX,
days: i32::MAX,
micros: i64::MAX,
};
assert_eq!(interval_to_micros_saturating(iv), i64::MAX);
}
#[test]
fn saturating_no_overflow() {
let iv = DuckInterval {
months: 0,
days: 0,
micros: 42,
};
assert_eq!(interval_to_micros_saturating(iv), 42);
}
#[test]
fn to_micros_method() {
let iv = DuckInterval {
months: 0,
days: 0,
micros: 12345,
};
assert_eq!(iv.to_micros(), Some(12345));
}
#[test]
fn to_micros_saturating_method() {
let iv = DuckInterval {
months: 0,
days: 0,
micros: 12345,
};
assert_eq!(iv.to_micros_saturating(), 12345);
}
#[test]
fn read_interval_at_basic() {
let data = [
DuckInterval {
months: 2,
days: 15,
micros: 999_000,
},
DuckInterval {
months: -1,
days: 3,
micros: 0,
},
];
let iv0 = unsafe { read_interval_at(data.as_ptr().cast::<u8>(), 0) };
assert_eq!(iv0.months, 2);
assert_eq!(iv0.days, 15);
assert_eq!(iv0.micros, 999_000);
let iv1 = unsafe { read_interval_at(data.as_ptr().cast::<u8>(), 1) };
assert_eq!(iv1.months, -1);
assert_eq!(iv1.days, 3);
assert_eq!(iv1.micros, 0);
}
#[test]
fn exactly_max_i64_micros_no_overflow() {
let iv = DuckInterval {
months: 0,
days: 0,
micros: i64::MAX,
};
assert_eq!(interval_to_micros(iv), Some(i64::MAX));
}
#[test]
fn months_calculation() {
let iv = DuckInterval {
months: 12,
days: 0,
micros: 0,
};
let expected = 12_i64 * MICROS_PER_MONTH;
assert_eq!(interval_to_micros(iv), Some(expected));
}
#[test]
fn saturating_positive_overflow_with_negative_days() {
let iv = DuckInterval {
months: i32::MAX,
days: -1,
micros: 0,
};
assert_eq!(interval_to_micros(iv), None); assert_eq!(interval_to_micros_saturating(iv), i64::MAX);
}
#[test]
fn saturating_negative_overflow_with_positive_days() {
let iv = DuckInterval {
months: i32::MIN,
days: 1,
micros: 0,
};
assert_eq!(interval_to_micros(iv), None);
assert_eq!(interval_to_micros_saturating(iv), i64::MIN);
}
#[test]
fn saturating_positive_overflow_negative_micros() {
let iv = DuckInterval {
months: i32::MAX,
days: 0,
micros: -1_000_000,
};
assert_eq!(interval_to_micros(iv), None);
assert_eq!(interval_to_micros_saturating(iv), i64::MAX);
}
#[test]
fn saturating_negative_overflow_all_negative() {
let iv = DuckInterval {
months: i32::MIN,
days: i32::MIN,
micros: i64::MIN,
};
assert_eq!(interval_to_micros(iv), None);
assert_eq!(interval_to_micros_saturating(iv), i64::MIN);
}
mod proptest_interval {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn micros_only_never_overflows_within_i64(micros: i64) {
let iv = DuckInterval { months: 0, days: 0, micros };
assert_eq!(interval_to_micros(iv), Some(micros));
}
#[test]
fn saturating_never_panics(months: i32, days: i32, micros: i64) {
let iv = DuckInterval { months, days, micros };
let _ = interval_to_micros_saturating(iv);
}
#[test]
fn saturating_direction_matches_i128(months: i32, days: i32, micros: i64) {
let iv = DuckInterval { months, days, micros };
let sat = interval_to_micros_saturating(iv);
if interval_to_micros(iv).is_none() {
let total = i128::from(months) * i128::from(MICROS_PER_MONTH)
+ i128::from(days) * i128::from(MICROS_PER_DAY)
+ i128::from(micros);
if total >= 0 {
prop_assert_eq!(sat, i64::MAX);
} else {
prop_assert_eq!(sat, i64::MIN);
}
}
}
#[test]
fn checked_and_saturating_agree_when_no_overflow(months in -100_i32..=100_i32, days in -100_i32..=100_i32, micros in -1_000_000_i64..=1_000_000_i64) {
let iv = DuckInterval { months, days, micros };
if let Some(checked) = interval_to_micros(iv) {
assert_eq!(interval_to_micros_saturating(iv), checked);
}
}
}
}
}