use time::{ error::{Parse, TryFromParsed}, format_description::{well_known, 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); // Ripped from infica with the month of Sol removed. I just want the 3 letter months /// Capitalized months (long, short) and lowercase months (long, short). // it seems useful to have the lowercase here so we don't have to always call // to_lowercase const MONTHS: [[&str; 4]; 12] = [ ["January", "Jan", "january", "jan"], ["February", "Feb", "february", "feb"], ["March", "Mar", "march", "mar"], ["April", "Apr", "april", "apr"], ["May", "May", "may", "may"], ["June", "Jun", "june", "jun"], ["July", "Jul", "july", "jul"], ["August", "Aug", "august", "aug"], ["September", "Sep", "september", "sep"], ["October", "Oct", "october", "oct"], ["November", "Nov", "november", "nov"], ["December", "Dec", "december", "dec"], ]; /// 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)) } pub fn format_long(datetime: &OffsetDateTime) -> String { let year = datetime.year(); let month = MONTHS[datetime.month() as usize][1]; let day = datetime.day(); let weekday = datetime.weekday(); format!("{weekday} {day}, {month} {year}") } pub fn iso8601(datetime: &OffsetDateTime) -> String { datetime .format(&well_known::Iso8601::DATE_TIME_OFFSET) .unwrap() } #[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)); } }