@@ -7,11 +7,75 @@ mod time_only_formats {
7
7
pub const TWELVEHOUR : & str = "%r" ;
8
8
}
9
9
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
+
10
73
pub ( crate ) fn parse_time_only ( date : DateTime < Local > , s : & str ) -> Option < DateTime < FixedOffset > > {
11
74
let re =
12
75
Regex :: new ( r"^(?<time>.*?)(?:(?<sign>\+|-)(?<h>[0-9]{1,2}):?(?<m>[0-9]{0,2}))?$" ) . unwrap ( ) ;
13
76
let captures = re. captures ( s) ?;
14
77
78
+ // Parse the sign, hour, and minute to get a `FixedOffset`, if possible.
15
79
let parsed_offset = match captures. name ( "h" ) {
16
80
Some ( hours) if !( hours. as_str ( ) . is_empty ( ) ) => {
17
81
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
27
91
_ => None ,
28
92
} ;
29
93
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) ;
42
121
}
43
122
}
44
123
@@ -64,6 +143,17 @@ mod tests {
64
143
assert_eq ! ( parsed_time, 1709499840 )
65
144
}
66
145
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
+
67
157
#[ test]
68
158
fn test_time_with_offset ( ) {
69
159
env:: set_var ( "TZ" , "UTC" ) ;
0 commit comments