@@ -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+
1073pub ( 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