@@ -116,6 +116,20 @@ impl From<&str> for Rfc3339Format {
116116 }
117117}
118118
119+ /// Indicates whether parsing a military timezone causes the date to remain the same, roll back to the previous day, or
120+ /// advance to the next day.
121+ /// This can occur when applying a military timezone with an optional hour offset crosses midnight
122+ /// in either direction.
123+ #[ derive( PartialEq , Debug ) ]
124+ enum DayDelta {
125+ /// The date does not change
126+ Same ,
127+ /// The date rolls back to the previous day.
128+ Previous ,
129+ /// The date advances to the next day.
130+ Next ,
131+ }
132+
119133/// Parse military timezone with optional hour offset.
120134/// Pattern: single letter (a-z except j) optionally followed by 1-2 digits.
121135/// Returns Some(total_hours_in_utc) or None if pattern doesn't match.
@@ -128,7 +142,7 @@ impl From<&str> for Rfc3339Format {
128142///
129143/// The hour offset from digits is added to the base military timezone offset.
130144/// Examples: "m" -> 12 (noon UTC), "m9" -> 21 (9pm UTC), "a5" -> 4 (4am UTC next day)
131- fn parse_military_timezone_with_offset ( s : & str ) -> Option < i32 > {
145+ fn parse_military_timezone_with_offset ( s : & str ) -> Option < ( i32 , DayDelta ) > {
132146 if s. is_empty ( ) || s. len ( ) > 3 {
133147 return None ;
134148 }
@@ -160,11 +174,17 @@ fn parse_military_timezone_with_offset(s: &str) -> Option<i32> {
160174 _ => return None ,
161175 } ;
162176
177+ let day_delta = match additional_hours - tz_offset {
178+ h if h < 0 => DayDelta :: Previous ,
179+ h if h >= 24 => DayDelta :: Next ,
180+ _ => DayDelta :: Same ,
181+ } ;
182+
163183 // Calculate total hours: midnight (0) + tz_offset + additional_hours
164184 // Midnight in timezone X converted to UTC
165- let total_hours = ( 0 - tz_offset + additional_hours) . rem_euclid ( 24 ) ;
185+ let hours_from_midnight = ( 0 - tz_offset + additional_hours) . rem_euclid ( 24 ) ;
166186
167- Some ( total_hours )
187+ Some ( ( hours_from_midnight , day_delta ) )
168188}
169189
170190#[ uucore:: main]
@@ -306,11 +326,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
306326 format ! ( "{date_part} 00:00 {offset}" )
307327 } ;
308328 parse_date ( composed)
309- } else if let Some ( total_hours) = military_tz_with_offset {
329+ } else if let Some ( ( total_hours, day_delta ) ) = military_tz_with_offset {
310330 // Military timezone with optional hour offset
311331 // Convert to UTC time: midnight + military_tz_offset + additional_hours
312- let date_part =
313- strtime:: format ( "%F" , & now) . unwrap_or_else ( |_| String :: from ( "1970-01-01" ) ) ;
332+
333+ // When calculating a military timezone with an optional hour offset, midnight may
334+ // be crossed in either direction. `day_delta` indicates whether the date remains
335+ // the same, moves to the previous day, or advances to the next day.
336+ // Changing day can result in error, this closure will help handle these errors
337+ // gracefully.
338+ let format_date_with_epoch_fallback = |date : Result < Zoned , _ > | -> String {
339+ date. and_then ( |d| strtime:: format ( "%F" , & d) )
340+ . unwrap_or_else ( |_| String :: from ( "1970-01-01" ) )
341+ } ;
342+ let date_part = match day_delta {
343+ DayDelta :: Same => format_date_with_epoch_fallback ( Ok ( now) ) ,
344+ DayDelta :: Next => format_date_with_epoch_fallback ( now. tomorrow ( ) ) ,
345+ DayDelta :: Previous => format_date_with_epoch_fallback ( now. yesterday ( ) ) ,
346+ } ;
314347 let composed = format ! ( "{date_part} {total_hours:02}:00:00 +00:00" ) ;
315348 parse_date ( composed)
316349 } else if is_pure_digits {
@@ -817,11 +850,26 @@ mod tests {
817850 #[ test]
818851 fn test_parse_military_timezone_with_offset ( ) {
819852 // Valid cases: letter only, letter + digit, uppercase
820- assert_eq ! ( parse_military_timezone_with_offset( "m" ) , Some ( 12 ) ) ; // UTC+12 -> 12:00 UTC
821- assert_eq ! ( parse_military_timezone_with_offset( "m9" ) , Some ( 21 ) ) ; // 12 + 9 = 21
822- assert_eq ! ( parse_military_timezone_with_offset( "a5" ) , Some ( 4 ) ) ; // 23 + 5 = 28 % 24 = 4
823- assert_eq ! ( parse_military_timezone_with_offset( "z" ) , Some ( 0 ) ) ; // UTC+0 -> 00:00 UTC
824- assert_eq ! ( parse_military_timezone_with_offset( "M9" ) , Some ( 21 ) ) ; // Uppercase works
853+ assert_eq ! (
854+ parse_military_timezone_with_offset( "m" ) ,
855+ Some ( ( 12 , DayDelta :: Previous ) )
856+ ) ; // UTC+12 -> 12:00 UTC
857+ assert_eq ! (
858+ parse_military_timezone_with_offset( "m9" ) ,
859+ Some ( ( 21 , DayDelta :: Previous ) )
860+ ) ; // 12 + 9 = 21
861+ assert_eq ! (
862+ parse_military_timezone_with_offset( "a5" ) ,
863+ Some ( ( 4 , DayDelta :: Same ) )
864+ ) ; // 23 + 5 = 28 % 24 = 4
865+ assert_eq ! (
866+ parse_military_timezone_with_offset( "z" ) ,
867+ Some ( ( 0 , DayDelta :: Same ) )
868+ ) ; // UTC+0 -> 00:00 UTC
869+ assert_eq ! (
870+ parse_military_timezone_with_offset( "M9" ) ,
871+ Some ( ( 21 , DayDelta :: Previous ) )
872+ ) ; // Uppercase works
825873
826874 // Invalid cases: 'j' reserved, empty, too long, starts with digit
827875 assert_eq ! ( parse_military_timezone_with_offset( "j" ) , None ) ; // Reserved for local time
0 commit comments