use chrono::{DateTime, Datelike, Duration, TimeZone as ChronoTimeZone, Timelike, Utc, Weekday};
use crate::eval::error::EvalResult;
use crate::value::{TimeZone, TimestampValue};
use hamelin_lib::tree::ast::expression::TruncUnit;
pub fn truncate_timestamp(ts: &TimestampValue, unit: &TruncUnit) -> EvalResult<TimestampValue> {
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, tz)
}
TimeZone::FixedOffset(offset) => {
let ts_in_offset = ts.instant().with_timezone(offset);
truncate_generic(&ts_in_offset, unit, offset)
}
TimeZone::Any => unreachable!(), };
Ok(TimestampValue::new(
truncated_instant,
ts.timezone().clone(),
))
}
fn truncate_generic<Tz: ChronoTimeZone>(
ts: &DateTime<Tz>,
unit: &TruncUnit,
tz: &Tz,
) -> DateTime<Utc> {
let truncated = match unit {
TruncUnit::Second => {
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 => {
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 => {
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 => {
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 = 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);
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 result = tz.with_ymd_and_hms(ts.year(), ts.month(), 1, 0, 0, 0);
result.earliest().unwrap_or_else(|| ts.clone())
}
TruncUnit::Quarter => {
let quarter_month = match ts.month() {
1..=3 => 1, 4..=6 => 4, 7..=9 => 7, 10..=12 => 10, _ => 1,
};
let result = tz.with_ymd_and_hms(ts.year(), quarter_month, 1, 0, 0, 0);
result.earliest().unwrap_or_else(|| ts.clone())
}
TruncUnit::Year => {
let result = tz.with_ymd_and_hms(ts.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,
) -> EvalResult<TimestampValue> {
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, tz)
}
TimeZone::FixedOffset(offset) => {
let ts_in_offset = truncated_ts.instant().with_timezone(offset);
next_boundary_generic(&ts_in_offset, unit, 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,
tz: &Tz,
) -> DateTime<Utc> {
let next = match unit {
TruncUnit::Second => truncated_ts.clone() + Duration::seconds(1),
TruncUnit::Minute => truncated_ts.clone() + Duration::minutes(1),
TruncUnit::Hour => truncated_ts.clone() + Duration::hours(1),
TruncUnit::Day => {
let next_date = truncated_ts.date_naive() + chrono::Days::new(1);
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(1))
}
TruncUnit::Week => {
let next_date = truncated_ts.date_naive() + chrono::Days::new(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(1))
}
TruncUnit::Month => {
let year = truncated_ts.year();
let month = truncated_ts.month();
let (next_year, next_month) = if month == 12 {
(year + 1, 1)
} else {
(year, month + 1)
};
tz.with_ymd_and_hms(next_year, next_month, 1, 0, 0, 0)
.earliest()
.unwrap_or_else(|| truncated_ts.clone() + Duration::days(30))
}
TruncUnit::Quarter => {
let year = truncated_ts.year();
let month = truncated_ts.month();
let (next_year, next_month) = match month {
1..=3 => (year, 4), 4..=6 => (year, 7), 7..=9 => (year, 10), 10..=12 => (year + 1, 1), _ => (year, month + 3),
};
tz.with_ymd_and_hms(next_year, next_month, 1, 0, 0, 0)
.earliest()
.unwrap_or_else(|| truncated_ts.clone() + Duration::days(90))
}
TruncUnit::Year => tz
.with_ymd_and_hms(truncated_ts.year() + 1, 1, 1, 0, 0, 0)
.earliest()
.unwrap_or_else(|| truncated_ts.clone() + Duration::days(365)),
};
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).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).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).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).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).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).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)
.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)
.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)
.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)
.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).unwrap();
assert_eq!(truncated.instant(), &expected);
}
#[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).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).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).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)
.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)
.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).unwrap();
assert_eq!(next.instant(), &expected);
}
}