use chrono::{DateTime, Datelike, Duration, TimeZone as ChronoTimeZone, Timelike, Utc, Weekday};
use crate::eval::error::EvalResult;
const EPOCH_DATE: chrono::NaiveDate = match chrono::NaiveDate::from_ymd_opt(1970, 1, 1) {
Some(d) => d,
None => panic!("1970-01-01 is a valid date"),
};
const REFERENCE_MONDAY: chrono::NaiveDate = match chrono::NaiveDate::from_ymd_opt(1970, 1, 5) {
Some(d) => d,
None => panic!("1970-01-05 is a valid date"),
};
use crate::value::{TimeZone, TimestampValue};
use hamelin_lib::tree::ast::expression::TruncUnit;
pub fn truncate_timestamp(
ts: &TimestampValue,
unit: &TruncUnit,
multiplier: u32,
) -> EvalResult<TimestampValue> {
if multiplier == 0 {
return Err(crate::eval::error::EvalError::execution(
"Truncation multiplier must be at least 1".to_string(),
));
}
if ts.timezone().is_any() {
return Err(crate::eval::error::EvalError::execution(
"Cannot truncate timestamp with unconstrained timezone (TimeZone::Any)".to_string(),
));
}
let truncated_instant = match ts.timezone() {
TimeZone::Named(tz) => {
let ts_in_tz = ts.instant().with_timezone(tz);
truncate_generic(&ts_in_tz, unit, multiplier, tz)
}
TimeZone::FixedOffset(offset) => {
let ts_in_offset = ts.instant().with_timezone(offset);
truncate_generic(&ts_in_offset, unit, multiplier, offset)
}
TimeZone::Any => unreachable!(), };
Ok(TimestampValue::new(
truncated_instant,
ts.timezone().clone(),
))
}
fn truncate_generic<Tz: ChronoTimeZone>(
ts: &DateTime<Tz>,
unit: &TruncUnit,
multiplier: u32,
tz: &Tz,
) -> DateTime<Utc> {
let truncated = match unit {
TruncUnit::Second => {
if multiplier > 1 {
let step = multiplier as i64;
let epoch_secs = ts.timestamp();
let snapped = epoch_secs.div_euclid(step) * step;
return DateTime::from_timestamp(snapped, 0)
.unwrap_or_else(|| ts.with_timezone(&Utc));
}
let result = tz.with_ymd_and_hms(
ts.year(),
ts.month(),
ts.day(),
ts.hour(),
ts.minute(),
ts.second(),
);
result.earliest().unwrap_or_else(|| ts.clone())
}
TruncUnit::Minute => {
if multiplier > 1 {
let step = multiplier as i64 * 60;
let epoch_secs = ts.timestamp();
let snapped = epoch_secs.div_euclid(step) * step;
return DateTime::from_timestamp(snapped, 0)
.unwrap_or_else(|| ts.with_timezone(&Utc));
}
let result =
tz.with_ymd_and_hms(ts.year(), ts.month(), ts.day(), ts.hour(), ts.minute(), 0);
result.earliest().unwrap_or_else(|| ts.clone())
}
TruncUnit::Hour => {
if multiplier > 1 {
let step = multiplier as i64 * 3600;
let epoch_secs = ts.timestamp();
let snapped = epoch_secs.div_euclid(step) * step;
return DateTime::from_timestamp(snapped, 0)
.unwrap_or_else(|| ts.with_timezone(&Utc));
}
let result = tz.with_ymd_and_hms(ts.year(), ts.month(), ts.day(), ts.hour(), 0, 0);
result.earliest().unwrap_or_else(|| ts.clone())
}
TruncUnit::Day => {
if multiplier > 1 {
let local_date = ts.date_naive();
let local_day = local_date.signed_duration_since(EPOCH_DATE).num_days();
let snapped_day = local_day.div_euclid(multiplier as i64) * multiplier as i64;
let snapped_date = EPOCH_DATE + Duration::days(snapped_day);
snapped_date
.and_hms_opt(0, 0, 0)
.and_then(|naive| tz.from_local_datetime(&naive).earliest())
.unwrap_or_else(|| ts.clone())
} else {
let date = ts.date_naive();
date.and_hms_opt(0, 0, 0)
.and_then(|naive| tz.from_local_datetime(&naive).earliest())
.unwrap_or_else(|| ts.clone())
}
}
TruncUnit::Week => {
let days_since_monday: i64 = match ts.weekday() {
Weekday::Mon => 0,
Weekday::Tue => 1,
Weekday::Wed => 2,
Weekday::Thu => 3,
Weekday::Fri => 4,
Weekday::Sat => 5,
Weekday::Sun => 6,
};
let ts_monday = ts.clone() - Duration::days(days_since_monday);
if multiplier > 1 {
let reference_monday = REFERENCE_MONDAY;
let monday_date = ts_monday.date_naive();
let days_from_ref = monday_date
.signed_duration_since(reference_monday)
.num_days();
let week_days = multiplier as i64 * 7;
let snapped_offset = days_from_ref.div_euclid(week_days) * week_days;
let snapped_date = reference_monday + Duration::days(snapped_offset);
snapped_date
.and_hms_opt(0, 0, 0)
.and_then(|naive| tz.from_local_datetime(&naive).earliest())
.unwrap_or_else(|| ts.clone())
} else {
let date = ts_monday.date_naive();
date.and_hms_opt(0, 0, 0)
.and_then(|naive| tz.from_local_datetime(&naive).earliest())
.unwrap_or_else(|| ts.clone())
}
}
TruncUnit::Month => {
let snapped_month = ((ts.month() - 1) / multiplier) * multiplier + 1;
let result = tz.with_ymd_and_hms(ts.year(), snapped_month, 1, 0, 0, 0);
result.earliest().unwrap_or_else(|| ts.clone())
}
TruncUnit::Quarter => {
let quarter_months = multiplier * 3;
let snapped_month = ((ts.month() - 1) / quarter_months) * quarter_months + 1;
let result = tz.with_ymd_and_hms(ts.year(), snapped_month, 1, 0, 0, 0);
result.earliest().unwrap_or_else(|| ts.clone())
}
TruncUnit::Year => {
let m = multiplier as i64;
let snapped_year = ((ts.year() as i64).div_euclid(m) * m) as i32;
let result = tz.with_ymd_and_hms(snapped_year, 1, 1, 0, 0, 0);
result.earliest().unwrap_or_else(|| ts.clone())
}
};
truncated.with_timezone(&Utc)
}
pub fn next_truncation_boundary(
truncated_ts: &TimestampValue,
unit: &TruncUnit,
multiplier: u32,
) -> EvalResult<TimestampValue> {
if multiplier == 0 {
return Err(crate::eval::error::EvalError::execution(
"Truncation multiplier must be at least 1".to_string(),
));
}
if truncated_ts.timezone().is_any() {
return Err(crate::eval::error::EvalError::execution(
"Cannot calculate next boundary for timestamp with unconstrained timezone (TimeZone::Any)".to_string(),
));
}
let next_instant = match truncated_ts.timezone() {
TimeZone::Named(tz) => {
let ts_in_tz = truncated_ts.instant().with_timezone(tz);
next_boundary_generic(&ts_in_tz, unit, multiplier, tz)
}
TimeZone::FixedOffset(offset) => {
let ts_in_offset = truncated_ts.instant().with_timezone(offset);
next_boundary_generic(&ts_in_offset, unit, multiplier, offset)
}
TimeZone::Any => unreachable!(),
};
Ok(TimestampValue::new(
next_instant,
truncated_ts.timezone().clone(),
))
}
fn next_boundary_generic<Tz: ChronoTimeZone>(
truncated_ts: &DateTime<Tz>,
unit: &TruncUnit,
multiplier: u32,
tz: &Tz,
) -> DateTime<Utc> {
let m = multiplier as i64;
let next = match unit {
TruncUnit::Second => truncated_ts.clone() + Duration::seconds(m),
TruncUnit::Minute => truncated_ts.clone() + Duration::minutes(m),
TruncUnit::Hour => truncated_ts.clone() + Duration::hours(m),
TruncUnit::Day => {
let next_date = truncated_ts.date_naive() + chrono::Days::new(multiplier as u64);
next_date
.and_hms_opt(0, 0, 0)
.and_then(|naive| tz.from_local_datetime(&naive).earliest())
.unwrap_or_else(|| truncated_ts.clone() + Duration::days(m))
}
TruncUnit::Week => {
let next_date = truncated_ts.date_naive() + chrono::Days::new(multiplier as u64 * 7);
next_date
.and_hms_opt(0, 0, 0)
.and_then(|naive| tz.from_local_datetime(&naive).earliest())
.unwrap_or_else(|| truncated_ts.clone() + Duration::weeks(m))
}
TruncUnit::Month => {
let next_month_candidate = truncated_ts.month() + multiplier;
let (target_year, target_month) = if next_month_candidate <= 12 {
(truncated_ts.year(), next_month_candidate)
} else {
(truncated_ts.year() + 1, 1)
};
tz.with_ymd_and_hms(target_year, target_month, 1, 0, 0, 0)
.earliest()
.unwrap_or_else(|| truncated_ts.clone() + Duration::days(30 * m))
}
TruncUnit::Quarter => {
let step_months = multiplier * 3;
let next_month_candidate = truncated_ts.month() + step_months;
let (target_year, target_month) = if next_month_candidate <= 12 {
(truncated_ts.year(), next_month_candidate)
} else {
(truncated_ts.year() + 1, 1)
};
tz.with_ymd_and_hms(target_year, target_month, 1, 0, 0, 0)
.earliest()
.unwrap_or_else(|| truncated_ts.clone() + Duration::days(90 * m))
}
TruncUnit::Year => {
let next_year = truncated_ts.year() + multiplier as i32;
tz.with_ymd_and_hms(next_year, 1, 1, 0, 0, 0)
.earliest()
.unwrap_or_else(|| truncated_ts.clone() + Duration::days(365 * m))
}
};
next.with_timezone(&Utc)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone as ChronoTimeZoneTrait;
use chrono::Timelike;
#[test]
fn test_truncate_to_second() {
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
let ts_with_nanos = ts.with_nanosecond(123456789).unwrap();
let ts_value = TimestampValue::utc(ts_with_nanos);
let truncated = truncate_timestamp(&ts_value, &TruncUnit::Second, 1).unwrap();
assert_eq!(truncated.instant(), &ts);
assert!(truncated.timezone().is_utc());
}
#[test]
fn test_truncate_to_minute() {
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 0).unwrap();
let ts_value = TimestampValue::utc(ts);
let truncated = truncate_timestamp(&ts_value, &TruncUnit::Minute, 1).unwrap();
assert_eq!(truncated.instant(), &expected);
}
#[test]
fn test_truncate_to_hour() {
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 15, 14, 0, 0).unwrap();
let ts_value = TimestampValue::utc(ts);
let truncated = truncate_timestamp(&ts_value, &TruncUnit::Hour, 1).unwrap();
assert_eq!(truncated.instant(), &expected);
}
#[test]
fn test_truncate_to_day() {
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 15, 0, 0, 0).unwrap();
let ts_value = TimestampValue::utc(ts);
let truncated = truncate_timestamp(&ts_value, &TruncUnit::Day, 1).unwrap();
assert_eq!(truncated.instant(), &expected);
}
#[test]
fn test_truncate_to_week() {
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 11, 0, 0, 0).unwrap();
let ts_value = TimestampValue::utc(ts);
let truncated = truncate_timestamp(&ts_value, &TruncUnit::Week, 1).unwrap();
assert_eq!(truncated.instant(), &expected);
}
#[test]
fn test_truncate_to_month() {
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 1, 0, 0, 0).unwrap();
let ts_value = TimestampValue::utc(ts);
let truncated = truncate_timestamp(&ts_value, &TruncUnit::Month, 1).unwrap();
assert_eq!(truncated.instant(), &expected);
}
#[test]
fn test_truncate_to_quarter() {
let ts_q1 = Utc.with_ymd_and_hms(2024, 2, 15, 14, 30, 45).unwrap();
let expected_q1 = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let ts_value_q1 = TimestampValue::utc(ts_q1);
assert_eq!(
truncate_timestamp(&ts_value_q1, &TruncUnit::Quarter, 1)
.unwrap()
.instant(),
&expected_q1
);
let ts_q2 = Utc.with_ymd_and_hms(2024, 5, 15, 14, 30, 45).unwrap();
let expected_q2 = Utc.with_ymd_and_hms(2024, 4, 1, 0, 0, 0).unwrap();
let ts_value_q2 = TimestampValue::utc(ts_q2);
assert_eq!(
truncate_timestamp(&ts_value_q2, &TruncUnit::Quarter, 1)
.unwrap()
.instant(),
&expected_q2
);
let ts_q3 = Utc.with_ymd_and_hms(2024, 8, 15, 14, 30, 45).unwrap();
let expected_q3 = Utc.with_ymd_and_hms(2024, 7, 1, 0, 0, 0).unwrap();
let ts_value_q3 = TimestampValue::utc(ts_q3);
assert_eq!(
truncate_timestamp(&ts_value_q3, &TruncUnit::Quarter, 1)
.unwrap()
.instant(),
&expected_q3
);
let ts_q4 = Utc.with_ymd_and_hms(2024, 11, 15, 14, 30, 45).unwrap();
let expected_q4 = Utc.with_ymd_and_hms(2024, 10, 1, 0, 0, 0).unwrap();
let ts_value_q4 = TimestampValue::utc(ts_q4);
assert_eq!(
truncate_timestamp(&ts_value_q4, &TruncUnit::Quarter, 1)
.unwrap()
.instant(),
&expected_q4
);
}
#[test]
fn test_truncate_to_year() {
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let ts_value = TimestampValue::utc(ts);
let truncated = truncate_timestamp(&ts_value, &TruncUnit::Year, 1).unwrap();
assert_eq!(truncated.instant(), &expected);
}
#[test]
fn test_truncate_with_multiplier_2h() {
let ts_value = TimestampValue::utc(Utc.with_ymd_and_hms(2024, 3, 15, 15, 30, 0).unwrap());
let truncated = truncate_timestamp(&ts_value, &TruncUnit::Hour, 2).unwrap();
assert_eq!(
truncated.instant(),
&Utc.with_ymd_and_hms(2024, 3, 15, 14, 0, 0).unwrap()
);
let ts_value2 = TimestampValue::utc(Utc.with_ymd_and_hms(2024, 3, 15, 13, 45, 0).unwrap());
let truncated2 = truncate_timestamp(&ts_value2, &TruncUnit::Hour, 2).unwrap();
assert_eq!(
truncated2.instant(),
&Utc.with_ymd_and_hms(2024, 3, 15, 12, 0, 0).unwrap()
);
}
#[test]
fn test_truncate_with_multiplier_15m() {
let ts_value = TimestampValue::utc(Utc.with_ymd_and_hms(2024, 3, 15, 14, 37, 0).unwrap());
let truncated = truncate_timestamp(&ts_value, &TruncUnit::Minute, 15).unwrap();
assert_eq!(
truncated.instant(),
&Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 0).unwrap()
);
}
#[test]
fn test_truncate_with_multiplier_30s() {
let ts_value = TimestampValue::utc(Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap());
let truncated = truncate_timestamp(&ts_value, &TruncUnit::Second, 30).unwrap();
assert_eq!(
truncated.instant(),
&Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 30).unwrap()
);
}
#[test]
fn test_truncate_7h_crosses_midnight() {
let ts_value = TimestampValue::utc(Utc.with_ymd_and_hms(2024, 3, 15, 4, 30, 0).unwrap());
let truncated = truncate_timestamp(&ts_value, &TruncUnit::Hour, 7).unwrap();
assert_eq!(
truncated.instant(),
&Utc.with_ymd_and_hms(2024, 3, 15, 4, 0, 0).unwrap()
);
let ts_value2 = TimestampValue::utc(Utc.with_ymd_and_hms(2024, 3, 15, 1, 0, 0).unwrap());
let truncated2 = truncate_timestamp(&ts_value2, &TruncUnit::Hour, 7).unwrap();
assert_eq!(
truncated2.instant(),
&Utc.with_ymd_and_hms(2024, 3, 14, 21, 0, 0).unwrap()
);
}
#[test]
fn test_truncate_3d_crosses_month() {
let ts_value = TimestampValue::utc(Utc.with_ymd_and_hms(2024, 3, 1, 12, 0, 0).unwrap());
let truncated = truncate_timestamp(&ts_value, &TruncUnit::Day, 3).unwrap();
assert_eq!(
truncated.instant(),
&Utc.with_ymd_and_hms(2024, 2, 29, 0, 0, 0).unwrap()
);
let ts_value2 = TimestampValue::utc(Utc.with_ymd_and_hms(2024, 3, 3, 6, 0, 0).unwrap());
let truncated2 = truncate_timestamp(&ts_value2, &TruncUnit::Day, 3).unwrap();
assert_eq!(
truncated2.instant(),
&Utc.with_ymd_and_hms(2024, 3, 3, 0, 0, 0).unwrap()
);
}
#[test]
fn test_next_boundary_7h() {
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 4, 0, 0).unwrap();
let ts_value = TimestampValue::utc(ts);
let next = next_truncation_boundary(&ts_value, &TruncUnit::Hour, 7).unwrap();
assert_eq!(
next.instant(),
&Utc.with_ymd_and_hms(2024, 3, 15, 11, 0, 0).unwrap()
);
}
#[test]
fn test_next_boundary_3d() {
let ts = Utc.with_ymd_and_hms(2024, 2, 29, 0, 0, 0).unwrap();
let ts_value = TimestampValue::utc(ts);
let next = next_truncation_boundary(&ts_value, &TruncUnit::Day, 3).unwrap();
assert_eq!(
next.instant(),
&Utc.with_ymd_and_hms(2024, 3, 3, 0, 0, 0).unwrap()
);
}
#[test]
fn test_next_boundary_second() {
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 46).unwrap();
let ts_value = TimestampValue::utc(ts);
let next = next_truncation_boundary(&ts_value, &TruncUnit::Second, 1).unwrap();
assert_eq!(next.instant(), &expected);
}
#[test]
fn test_next_boundary_month() {
let ts = Utc.with_ymd_and_hms(2024, 3, 1, 0, 0, 0).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 4, 1, 0, 0, 0).unwrap();
let ts_value = TimestampValue::utc(ts);
let next = next_truncation_boundary(&ts_value, &TruncUnit::Month, 1).unwrap();
assert_eq!(next.instant(), &expected);
let ts_dec = Utc.with_ymd_and_hms(2024, 12, 1, 0, 0, 0).unwrap();
let expected_jan = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let ts_value_dec = TimestampValue::utc(ts_dec);
let next_jan = next_truncation_boundary(&ts_value_dec, &TruncUnit::Month, 1).unwrap();
assert_eq!(next_jan.instant(), &expected_jan);
}
#[test]
fn test_next_boundary_quarter() {
let ts_q1 = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let expected_q2 = Utc.with_ymd_and_hms(2024, 4, 1, 0, 0, 0).unwrap();
let ts_value_q1 = TimestampValue::utc(ts_q1);
assert_eq!(
next_truncation_boundary(&ts_value_q1, &TruncUnit::Quarter, 1)
.unwrap()
.instant(),
&expected_q2
);
let ts_q4 = Utc.with_ymd_and_hms(2024, 10, 1, 0, 0, 0).unwrap();
let expected_q1_next = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let ts_value_q4 = TimestampValue::utc(ts_q4);
assert_eq!(
next_truncation_boundary(&ts_value_q4, &TruncUnit::Quarter, 1)
.unwrap()
.instant(),
&expected_q1_next
);
}
#[test]
fn test_next_boundary_year() {
let ts = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let expected = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let ts_value = TimestampValue::utc(ts);
let next = next_truncation_boundary(&ts_value, &TruncUnit::Year, 1).unwrap();
assert_eq!(next.instant(), &expected);
}
#[test]
fn test_next_boundary_with_multiplier_2h() {
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 0, 0).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 15, 16, 0, 0).unwrap();
let ts_value = TimestampValue::utc(ts);
let next = next_truncation_boundary(&ts_value, &TruncUnit::Hour, 2).unwrap();
assert_eq!(next.instant(), &expected);
}
#[test]
fn test_next_boundary_5mon_short_last_bucket() {
let nov = Utc.with_ymd_and_hms(2024, 11, 1, 0, 0, 0).unwrap();
let ts_value = TimestampValue::utc(nov);
let next = next_truncation_boundary(&ts_value, &TruncUnit::Month, 5).unwrap();
assert_eq!(
next.instant(),
&Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
);
let jan = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let ts_jan = TimestampValue::utc(jan);
let next_jan = next_truncation_boundary(&ts_jan, &TruncUnit::Month, 5).unwrap();
assert_eq!(
next_jan.instant(),
&Utc.with_ymd_and_hms(2024, 6, 1, 0, 0, 0).unwrap()
);
}
#[test]
fn test_next_boundary_3q_short_last_bucket() {
let oct = Utc.with_ymd_and_hms(2024, 10, 1, 0, 0, 0).unwrap();
let ts_value = TimestampValue::utc(oct);
let next = next_truncation_boundary(&ts_value, &TruncUnit::Quarter, 3).unwrap();
assert_eq!(
next.instant(),
&Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
);
}
}