diff options
| author | gennyble <gen@nyble.dev> | 2026-09-20 19:43:42 -0500 |
|---|---|---|
| committer | gennyble <gen@nyble.dev> | 2026-09-20 19:43:42 -0500 |
| commit | 178ca3c066a1ef920b112da741625099b6dc07da (patch) | |
| tree | 4fe1e2822975c1a8a2c42967cb0d061a1408fb46 /timeparse/src/lib.rs | |
| parent | dcadd27bc12e96585cdf96e6c6de087af63c0c6d (diff) | |
move timeparse to it's own crate
Diffstat (limited to 'timeparse/src/lib.rs')
| -rwxr-xr-x | timeparse/src/lib.rs | 211 |
1 files changed, 211 insertions, 0 deletions
diff --git a/timeparse/src/lib.rs b/timeparse/src/lib.rs new file mode 100755 index 0000000..3c8da8e --- /dev/null +++ b/timeparse/src/lib.rs @@ -0,0 +1,211 @@ +use time::{ + Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday, + error::{Parse, TryFromParsed}, + format_description::FormatItem, + macros::{format_description, offset, time}, +}; + +pub mod format; + +#[allow(unused)] +pub(crate) const FMT: &[FormatItem<'_>] = + format_description!("[year]-[month]-[day] [hour]:[minute]"); + +pub(crate) const DATE: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); +pub(crate) const TIME_24HOUR: &[FormatItem<'_>] = + format_description!("[hour padding:zero]:[minute]"); +pub(crate) const TIME_12HOUR: &[FormatItem<'_>] = + format_description!("[hour padding:none]:[minute][period case:lower case_sensitive:false]"); + +#[allow(unused)] +pub(crate) const OFFSET: &[FormatItem<'_>] = + format_description!("[offset_hour sign:mandatory]:[offset_minute]"); + +pub(crate) const CST: UtcOffset = offset!(-06:00); +pub(crate) 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 +pub(crate) 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, + "UTC" => UtcOffset::UTC, + _ => unimplemented!(), + } +} + +fn parse_time(raw: &str) -> Result<Time, time::error::Parse> { + match Time::parse(raw, TIME_24HOUR) { + Err(_e) => Time::parse(raw, TIME_12HOUR), + Ok(t) => Ok(t), + } +} + +fn parse_date(raw: &str) -> Result<Date, time::error::Parse> { + 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<OffsetDateTime, time::error::Parse> { + 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 calculated_offset = us_dst_central_offset(PrimitiveDateTime::new(date, time)); + let offset = match splits.next() { + None => calculated_offset, + Some(raw) => { + let offset = parse_offset(raw); + if offset != calculated_offset { + //FIXME: gen 2024-12; warn here but format it rightly. + () + } + + offset + } + }; + + Ok(OffsetDateTime::new_in_offset(date, time, offset)) +} + +pub fn whichever_now() -> OffsetDateTime { + OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()) +} + +#[cfg(test)] +mod test { + use time::{ + OffsetDateTime, PrimitiveDateTime, + macros::{datetime, offset}, + }; + + use crate::{CDT, CST, FMT, parse_offset, us_dst_central_offset}; + + #[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)); + } +} |
