about summary refs log tree commit diff
path: root/src/timeparse.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/timeparse.rs')
-rw-r--r--src/timeparse.rs168
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));
+	}
+}