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