Skip to content

Commit 00dc90d

Browse files
authored
Merge pull request #97 from jfinkels/military-time-zones
Add support for military time zones
2 parents faf363c + 3c67f4b commit 00dc90d

File tree

1 file changed

+102
-12
lines changed

1 file changed

+102
-12
lines changed

src/parse_time_only_str.rs

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,75 @@ mod time_only_formats {
77
pub const TWELVEHOUR: &str = "%r";
88
}
99

10+
/// Convert a military time zone string to a time zone offset.
11+
///
12+
/// Military time zones are the letters A through Z except J. They are
13+
/// described in RFC 5322.
14+
fn to_offset(tz: &str) -> Option<FixedOffset> {
15+
let hour = match tz {
16+
"A" => 1,
17+
"B" => 2,
18+
"C" => 3,
19+
"D" => 4,
20+
"E" => 5,
21+
"F" => 6,
22+
"G" => 7,
23+
"H" => 8,
24+
"I" => 9,
25+
"K" => 10,
26+
"L" => 11,
27+
"M" => 12,
28+
"N" => -1,
29+
"O" => -2,
30+
"P" => -3,
31+
"Q" => -4,
32+
"R" => -5,
33+
"S" => -6,
34+
"T" => -7,
35+
"U" => -8,
36+
"V" => -9,
37+
"W" => -10,
38+
"X" => -11,
39+
"Y" => -12,
40+
"Z" => 0,
41+
_ => return None,
42+
};
43+
let offset_in_sec = hour * 3600;
44+
FixedOffset::east_opt(offset_in_sec)
45+
}
46+
47+
/// Parse a time string without an offset and apply an offset to it.
48+
///
49+
/// Multiple formats are attempted when parsing the string.
50+
fn parse_time_with_offset_multi(
51+
date: DateTime<Local>,
52+
offset: FixedOffset,
53+
s: &str,
54+
) -> Option<DateTime<FixedOffset>> {
55+
for fmt in [
56+
time_only_formats::HH_MM,
57+
time_only_formats::HH_MM_SS,
58+
time_only_formats::TWELVEHOUR,
59+
] {
60+
let parsed = match NaiveTime::parse_from_str(s, fmt) {
61+
Ok(t) => t,
62+
Err(_) => continue,
63+
};
64+
let parsed_dt = date.date_naive().and_time(parsed);
65+
match offset.from_local_datetime(&parsed_dt).single() {
66+
Some(dt) => return Some(dt),
67+
None => continue,
68+
}
69+
}
70+
None
71+
}
72+
1073
pub(crate) fn parse_time_only(date: DateTime<Local>, s: &str) -> Option<DateTime<FixedOffset>> {
1174
let re =
1275
Regex::new(r"^(?<time>.*?)(?:(?<sign>\+|-)(?<h>[0-9]{1,2}):?(?<m>[0-9]{0,2}))?$").unwrap();
1376
let captures = re.captures(s)?;
1477

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

30-
for fmt in [
31-
time_only_formats::HH_MM,
32-
time_only_formats::HH_MM_SS,
33-
time_only_formats::TWELVEHOUR,
34-
] {
35-
if let Ok(parsed) = NaiveTime::parse_from_str(captures["time"].trim(), fmt) {
36-
let parsed_dt = date.date_naive().and_time(parsed);
37-
let offset = match parsed_offset {
38-
Some(offset) => offset,
39-
None => *date.offset(),
40-
};
41-
return offset.from_local_datetime(&parsed_dt).single();
94+
// Parse the time and apply the parsed offset.
95+
let s = captures["time"].trim();
96+
let offset = match parsed_offset {
97+
Some(offset) => offset,
98+
None => *date.offset(),
99+
};
100+
if let Some(result) = parse_time_with_offset_multi(date, offset, s) {
101+
return Some(result);
102+
}
103+
104+
// Military time zones are specified in RFC 5322, Section 4.3
105+
// "Obsolete Date and Time".
106+
// <https://datatracker.ietf.org/doc/html/rfc5322>
107+
//
108+
// We let the parsing above handle "5:00 AM" so at this point we
109+
// should be guaranteed that we don't have an AM/PM suffix. That
110+
// way, we can safely parse "5:00M" here without interference.
111+
let re = Regex::new(r"(?<time>.*?)(?<tz>[A-IKLMN-YZ])").unwrap();
112+
let captures = re.captures(s)?;
113+
if let Some(tz) = captures.name("tz") {
114+
let s = captures["time"].trim();
115+
let offset = match to_offset(tz.as_str()) {
116+
Some(offset) => offset,
117+
None => *date.offset(),
118+
};
119+
if let Some(result) = parse_time_with_offset_multi(date, offset, s) {
120+
return Some(result);
42121
}
43122
}
44123

@@ -64,6 +143,17 @@ mod tests {
64143
assert_eq!(parsed_time, 1709499840)
65144
}
66145

146+
#[test]
147+
fn test_military_time_zones() {
148+
env::set_var("TZ", "UTC");
149+
let date = get_test_date();
150+
let actual = parse_time_only(date, "05:00C").unwrap().timestamp();
151+
// Computed via `date -u -d "2024-03-03 05:00:00C" +%s`, using a
152+
// version of GNU date after v8.32 (earlier versions had a bug).
153+
let expected = 1709431200;
154+
assert_eq!(actual, expected);
155+
}
156+
67157
#[test]
68158
fn test_time_with_offset() {
69159
env::set_var("TZ", "UTC");

0 commit comments

Comments
 (0)