Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 102 additions & 12 deletions src/parse_time_only_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,75 @@ mod time_only_formats {
pub const TWELVEHOUR: &str = "%r";
}

/// Convert a military time zone string to a time zone offset.
///
/// Military time zones are the letters A through Z except J. They are
/// described in RFC 5322.
fn to_offset(tz: &str) -> Option<FixedOffset> {
let hour = match tz {
"A" => 1,
"B" => 2,
"C" => 3,
"D" => 4,
"E" => 5,
"F" => 6,
"G" => 7,
"H" => 8,
"I" => 9,
"K" => 10,
"L" => 11,
"M" => 12,
"N" => -1,
"O" => -2,
"P" => -3,
"Q" => -4,
"R" => -5,
"S" => -6,
"T" => -7,
"U" => -8,
"V" => -9,
"W" => -10,
"X" => -11,
"Y" => -12,
"Z" => 0,
_ => return None,
};
let offset_in_sec = hour * 3600;
FixedOffset::east_opt(offset_in_sec)
}

/// Parse a time string without an offset and apply an offset to it.
///
/// Multiple formats are attempted when parsing the string.
fn parse_time_with_offset_multi(
date: DateTime<Local>,
offset: FixedOffset,
s: &str,
) -> Option<DateTime<FixedOffset>> {
for fmt in [
time_only_formats::HH_MM,
time_only_formats::HH_MM_SS,
time_only_formats::TWELVEHOUR,
] {
let parsed = match NaiveTime::parse_from_str(s, fmt) {
Ok(t) => t,
Err(_) => continue,
};
let parsed_dt = date.date_naive().and_time(parsed);
match offset.from_local_datetime(&parsed_dt).single() {
Some(dt) => return Some(dt),
None => continue,
}
}
None
}

pub(crate) fn parse_time_only(date: DateTime<Local>, s: &str) -> Option<DateTime<FixedOffset>> {
let re =
Regex::new(r"^(?<time>.*?)(?:(?<sign>\+|-)(?<h>[0-9]{1,2}):?(?<m>[0-9]{0,2}))?$").unwrap();
let captures = re.captures(s)?;

// Parse the sign, hour, and minute to get a `FixedOffset`, if possible.
let parsed_offset = match captures.name("h") {
Some(hours) if !(hours.as_str().is_empty()) => {
let mut offset_in_sec = hours.as_str().parse::<i32>().unwrap() * 3600;
Expand All @@ -27,18 +91,33 @@ pub(crate) fn parse_time_only(date: DateTime<Local>, s: &str) -> Option<DateTime
_ => None,
};

for fmt in [
time_only_formats::HH_MM,
time_only_formats::HH_MM_SS,
time_only_formats::TWELVEHOUR,
] {
if let Ok(parsed) = NaiveTime::parse_from_str(captures["time"].trim(), fmt) {
let parsed_dt = date.date_naive().and_time(parsed);
let offset = match parsed_offset {
Some(offset) => offset,
None => *date.offset(),
};
return offset.from_local_datetime(&parsed_dt).single();
// Parse the time and apply the parsed offset.
let s = captures["time"].trim();
let offset = match parsed_offset {
Some(offset) => offset,
None => *date.offset(),
};
if let Some(result) = parse_time_with_offset_multi(date, offset, s) {
return Some(result);
}

// Military time zones are specified in RFC 5322, Section 4.3
// "Obsolete Date and Time".
// <https://datatracker.ietf.org/doc/html/rfc5322>
//
// We let the parsing above handle "5:00 AM" so at this point we
// should be guaranteed that we don't have an AM/PM suffix. That
// way, we can safely parse "5:00M" here without interference.
let re = Regex::new(r"(?<time>.*?)(?<tz>[A-IKLMN-YZ])").unwrap();
let captures = re.captures(s)?;
if let Some(tz) = captures.name("tz") {
let s = captures["time"].trim();
let offset = match to_offset(tz.as_str()) {
Some(offset) => offset,
None => *date.offset(),
};
if let Some(result) = parse_time_with_offset_multi(date, offset, s) {
return Some(result);
}
}

Expand All @@ -64,6 +143,17 @@ mod tests {
assert_eq!(parsed_time, 1709499840)
}

#[test]
fn test_military_time_zones() {
env::set_var("TZ", "UTC");
let date = get_test_date();
let actual = parse_time_only(date, "05:00C").unwrap().timestamp();
// Computed via `date -u -d "2024-03-03 05:00:00C" +%s`, using a
// version of GNU date after v8.32 (earlier versions had a bug).
let expected = 1709431200;
assert_eq!(actual, expected);
}

#[test]
fn test_time_with_offset() {
env::set_var("TZ", "UTC");
Expand Down
Loading