diff options
author | gennyble <gen@nyble.dev> | 2024-04-13 03:11:49 -0500 |
---|---|---|
committer | gennyble <gen@nyble.dev> | 2024-04-13 03:11:49 -0500 |
commit | eafb6a862e5a525e615148dc0f297f1792d6ab89 (patch) | |
tree | bf63ba3c22bfcfba390708f1b54149ea9622d715 /src/timeparse.rs | |
parent | d716ffb05f086061d9a5302f67f3695457fcff02 (diff) | |
download | awake-eafb6a862e5a525e615148dc0f297f1792d6ab89.tar.gz awake-eafb6a862e5a525e615148dc0f297f1792d6ab89.zip |
Add time parsing :)
Diffstat (limited to 'src/timeparse.rs')
-rw-r--r-- | src/timeparse.rs | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/src/timeparse.rs b/src/timeparse.rs new file mode 100644 index 0000000..f4eaca5 --- /dev/null +++ b/src/timeparse.rs @@ -0,0 +1,168 @@ +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, time::error::Parse> { + Time::parse(raw, TIME) +} + +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 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)); + } +} |