use chrono::{Datelike, NaiveDate};
use super::evaluator::ConditionResult;
pub fn parse_dtm303(value: &str) -> Option<i64> {
let s = value.trim();
if s.len() < 12 {
return None;
}
let year: i32 = s.get(0..4).and_then(|v| v.parse().ok())?;
let month: u32 = s.get(4..6).and_then(|v| v.parse().ok())?;
let day: u32 = s.get(6..8).and_then(|v| v.parse().ok())?;
let hour: u32 = s.get(8..10).and_then(|v| v.parse().ok())?;
let minute: u32 = s.get(10..12).and_then(|v| v.parse().ok())?;
let dt = NaiveDate::from_ymd_opt(year, month, day)?.and_hms_opt(hour, minute, 0)?;
Some(dt.and_utc().timestamp() / 60)
}
pub fn dst_transitions_between(a_utc_min: i64, b_utc_min: i64) -> i32 {
let (lo, hi) = if a_utc_min <= b_utc_min {
(a_utc_min, b_utc_min)
} else {
(b_utc_min, a_utc_min)
};
if lo == hi {
return 0;
}
let lo_year = year_of_epoch_min(lo);
let hi_year = year_of_epoch_min(hi);
let mut delta: i32 = 0;
for y in lo_year..=hi_year {
let spring_forward = dst_instant_min(y, 3);
if lo <= spring_forward && spring_forward < hi {
delta += 60;
}
let fall_back = dst_instant_min(y, 10);
if lo <= fall_back && fall_back < hi {
delta -= 60;
}
}
delta
}
fn dst_instant_min(year: i32, month: u32) -> i64 {
let date = last_sunday_of_month(year, month);
date.and_hms_opt(1, 0, 0).unwrap().and_utc().timestamp() / 60
}
fn year_of_epoch_min(epoch_min: i64) -> i32 {
chrono::DateTime::from_timestamp(epoch_min * 60, 0)
.map(|dt| dt.date_naive().year())
.unwrap_or(1970)
}
pub fn is_mesz_utc(dtm_value: &str) -> ConditionResult {
let s = dtm_value.trim();
if s.len() < 8 {
return ConditionResult::Unknown;
}
let year: i32 = match s[0..4].parse() {
Ok(v) => v,
Err(_) => return ConditionResult::Unknown,
};
let month: u32 = match s[4..6].parse() {
Ok(v) => v,
Err(_) => return ConditionResult::Unknown,
};
let day: u32 = match s[6..8].parse() {
Ok(v) => v,
Err(_) => return ConditionResult::Unknown,
};
let hour: u32 = if s.len() >= 10 {
match s[8..10].parse() {
Ok(v) => v,
Err(_) => return ConditionResult::Unknown,
}
} else {
0
};
let minute: u32 = if s.len() >= 12 {
match s[10..12].parse() {
Ok(v) => v,
Err(_) => return ConditionResult::Unknown,
}
} else {
0
};
let Some(dt) =
NaiveDate::from_ymd_opt(year, month, day).and_then(|d| d.and_hms_opt(hour, minute, 0))
else {
return ConditionResult::Unknown;
};
let mesz_start = last_sunday_of_month(year, 3).and_hms_opt(1, 0, 0).unwrap();
let mesz_end = last_sunday_of_month(year, 10).and_hms_opt(1, 0, 0).unwrap();
ConditionResult::from(dt >= mesz_start && dt < mesz_end)
}
pub fn is_mez_utc(dtm_value: &str) -> ConditionResult {
match is_mesz_utc(dtm_value) {
ConditionResult::True => ConditionResult::False,
ConditionResult::False => ConditionResult::True,
ConditionResult::Unknown => ConditionResult::Unknown,
}
}
fn last_sunday_of_month(year: i32, month: u32) -> NaiveDate {
let last_day = NaiveDate::from_ymd_opt(year, month + 1, 1)
.unwrap_or_else(|| NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap())
.pred_opt()
.unwrap();
let days_since_sunday = last_day.weekday().num_days_from_sunday();
last_day - chrono::Duration::days(days_since_sunday as i64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_known_mesz_date() {
assert_eq!(is_mesz_utc("202607151200"), ConditionResult::True);
}
#[test]
fn test_known_mez_date() {
assert_eq!(is_mesz_utc("202601151200"), ConditionResult::False);
}
#[test]
fn test_march_transition_2026() {
assert_eq!(is_mesz_utc("202603290059"), ConditionResult::False);
assert_eq!(is_mesz_utc("202603290100"), ConditionResult::True);
}
#[test]
fn test_october_transition_2026() {
assert_eq!(is_mesz_utc("202610250059"), ConditionResult::True);
assert_eq!(is_mesz_utc("202610250100"), ConditionResult::False);
}
#[test]
fn test_short_input() {
assert_eq!(is_mesz_utc("2026"), ConditionResult::Unknown);
assert_eq!(is_mesz_utc(""), ConditionResult::Unknown);
assert_eq!(is_mesz_utc("20260715"), ConditionResult::True);
assert_eq!(is_mesz_utc("20260115"), ConditionResult::False);
}
#[test]
fn test_invalid_input_returns_unknown() {
assert_eq!(is_mesz_utc("abcdefghijkl"), ConditionResult::Unknown);
assert_eq!(is_mesz_utc("202613151200"), ConditionResult::Unknown);
assert_eq!(is_mesz_utc("202601321200"), ConditionResult::Unknown);
}
#[test]
fn test_is_mez_complements_is_mesz() {
assert_eq!(is_mez_utc("202607151200"), ConditionResult::False);
assert_eq!(is_mez_utc("202601151200"), ConditionResult::True);
assert_eq!(is_mez_utc("short"), ConditionResult::Unknown);
}
#[test]
fn test_value_with_timezone_suffix() {
assert_eq!(is_mesz_utc("202607151200UTC"), ConditionResult::True);
assert_eq!(is_mesz_utc("202601151200303"), ConditionResult::False);
}
#[test]
fn test_last_sunday_of_march_2026() {
assert_eq!(
last_sunday_of_month(2026, 3),
NaiveDate::from_ymd_opt(2026, 3, 29).unwrap()
);
}
#[test]
fn test_last_sunday_of_october_2026() {
assert_eq!(
last_sunday_of_month(2026, 10),
NaiveDate::from_ymd_opt(2026, 10, 25).unwrap()
);
}
#[test]
fn test_parse_dtm303_valid() {
let m = parse_dtm303("202601151200").unwrap();
let expected = NaiveDate::from_ymd_opt(2026, 1, 15)
.unwrap()
.and_hms_opt(12, 0, 0)
.unwrap()
.and_utc()
.timestamp()
/ 60;
assert_eq!(m, expected);
}
#[test]
fn test_parse_dtm303_ignores_tz_suffix() {
let a = parse_dtm303("202603290100").unwrap();
let b = parse_dtm303("202603290100+00").unwrap();
let c = parse_dtm303("202603290100UTC").unwrap();
assert_eq!(a, b);
assert_eq!(a, c);
}
#[test]
fn test_parse_dtm303_rejects_short() {
assert!(parse_dtm303("20260115").is_none());
assert!(parse_dtm303("").is_none());
}
#[test]
fn test_parse_dtm303_rejects_invalid() {
assert!(parse_dtm303("2026ZZ151200").is_none());
assert!(parse_dtm303("202613151200").is_none());
}
#[test]
fn test_dst_no_transition_same_day() {
let a = parse_dtm303("202601150800").unwrap();
let b = parse_dtm303("202601151200").unwrap();
assert_eq!(dst_transitions_between(a, b), 0);
}
#[test]
fn test_dst_spring_forward_2026() {
let before = parse_dtm303("202603290030").unwrap();
let after = parse_dtm303("202603290130").unwrap();
assert_eq!(dst_transitions_between(before, after), 60);
assert_eq!(dst_transitions_between(after, before), 60);
}
#[test]
fn test_dst_fall_back_2026() {
let before = parse_dtm303("202610250030").unwrap();
let after = parse_dtm303("202610250130").unwrap();
assert_eq!(dst_transitions_between(before, after), -60);
assert_eq!(dst_transitions_between(after, before), -60);
}
#[test]
fn test_dst_transition_at_exact_boundary() {
let at_boundary = parse_dtm303("202603290100").unwrap();
assert_eq!(dst_transitions_between(at_boundary, at_boundary), 0);
let after = parse_dtm303("202603290200").unwrap();
assert_eq!(dst_transitions_between(at_boundary, after), 60);
}
#[test]
fn test_dst_both_transitions_cancel() {
let winter_a = parse_dtm303("202601010000").unwrap();
let winter_b = parse_dtm303("202612310000").unwrap();
assert_eq!(dst_transitions_between(winter_a, winter_b), 0);
}
#[test]
fn test_different_years() {
assert_eq!(
last_sunday_of_month(2025, 3),
NaiveDate::from_ymd_opt(2025, 3, 30).unwrap()
);
assert_eq!(
last_sunday_of_month(2025, 10),
NaiveDate::from_ymd_opt(2025, 10, 26).unwrap()
);
assert_eq!(
last_sunday_of_month(2024, 3),
NaiveDate::from_ymd_opt(2024, 3, 31).unwrap()
);
assert_eq!(
last_sunday_of_month(2024, 10),
NaiveDate::from_ymd_opt(2024, 10, 27).unwrap()
);
}
}