use time::{ error::{Parse, TryFromParsed}, format_description::FormatItem, macros::{format_description, offset, time}, Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday, }; const FMT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day] [hour]:[minute]"); const DATE: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); const TIME: &[FormatItem<'_>] = format_description!("[hour]:[minute]"); #[allow(unused)] const OFFSET: &[FormatItem<'_>] = format_description!("[offset_hour sign:mandatory]:[offset_minute]"); const CST: UtcOffset = offset!(-06:00); const CDT: UtcOffset = offset!(-05:00); /// Offset for the united states in Central Time. Accounts for DST /// DST starts the 2nd sunday in March and ends the 1st sunday in November. /// https://www.nist.gov/pml/time-and-frequency-division/popular-links/daylight-saving-time-dst fn us_dst_central_offset(datetime: PrimitiveDateTime) -> UtcOffset { let second_sunday_march = { let mut seen_sunday = false; let mut curr = Date::from_calendar_date(datetime.year(), time::Month::March, 1).unwrap(); loop { if curr.weekday() == Weekday::Sunday { if seen_sunday { break PrimitiveDateTime::new(curr, time!(02:00 AM)); } else { seen_sunday = true; } } curr = curr.next_day().unwrap(); } }; let first_sunday_november = { let mut curr = Date::from_calendar_date(datetime.year(), time::Month::November, 1).unwrap(); loop { if curr.weekday() == Weekday::Sunday { break PrimitiveDateTime::new(curr, time!(02:00 AM)); } else { curr = curr.next_day().unwrap(); } } }; if datetime >= second_sunday_march && datetime < first_sunday_november { CDT } else { CST } } fn parse_offset(raw: &str) -> UtcOffset { match raw.to_ascii_uppercase().as_str() { "CST" => CST, "CDT" => CDT, _ => unimplemented!(), } } fn parse_time(raw: &str) -> Result { Time::parse(raw, TIME) } fn parse_date(raw: &str) -> Result { Date::parse(raw, DATE) } /// Parses an OffsetDateTime from any of the following formats: /// - date only: 2024-04-13 /// - date and time: 2024-04-13 2:56 /// - dat, time, and offset: 2024-04-13 2:56 CDT /// /// If a time is not procided, noon (12:00) is assumed. /// /// If an offset is not provided it will be assumed to be US Central Time and /// it will correct for daylight savings. pub fn parse(raw: &str) -> Result { let mut splits = raw.trim().split(' '); let date = match splits.next() { None => return Err(Parse::TryFromParsed(TryFromParsed::InsufficientInformation)), Some(raw) => parse_date(raw)?, }; let time = match splits.next() { None => time!(12:00:00), Some(raw) => parse_time(raw)?, }; let offset = match splits.next() { None => us_dst_central_offset(PrimitiveDateTime::new(date, time)), Some(raw) => parse_offset(raw), }; Ok(OffsetDateTime::new_in_offset(date, time, offset)) } #[cfg(test)] mod test { use time::{ macros::{datetime, offset}, OffsetDateTime, PrimitiveDateTime, }; use crate::timeparse::{parse_offset, us_dst_central_offset, CDT, CST, FMT}; #[test] fn calculates_cst_cdt_correctly() { fn daylight(raw: &str) { let date = PrimitiveDateTime::parse(raw, FMT).unwrap(); if us_dst_central_offset(date) != CDT { panic!("CDT failed on {raw}") } } fn standard(raw: &str) { let date = PrimitiveDateTime::parse(raw, FMT).unwrap(); if us_dst_central_offset(date) != CST { panic!("CST failed on {raw}") } } // make sure CST and CDT are what we expect assert_eq!(CST, offset!(-06:00)); assert_eq!(CDT, offset!(-05:00)); daylight("2024-04-13 02:56"); daylight("2024-03-10 14:00"); daylight("2024-03-10 02:00"); standard("2024-12-01 00:00"); standard("2024-11-03 14:00"); standard("2024-11-03 02:00"); // other years daylight("2023-03-12 12:00"); // DST start Mar 12, end Nov 5 standard("2023-03-11 23:00"); standard("2023-11-05 12:00"); daylight("2023-11-04 23:00"); } #[test] fn paress_timezone_code() { assert_eq!(parse_offset("CST"), CST); assert_eq!(parse_offset("CDT"), CDT); } fn test_parse(raw: &str, expected: OffsetDateTime) { assert_eq!(super::parse(raw).unwrap(), expected) } #[test] fn parse_date_only() { test_parse("2024-04-13", datetime!(2024-04-13 12:00 -05:00)); test_parse("2024-01-01", datetime!(2024-01-01 12:00 -06:00)); } }