@@ -117,6 +117,57 @@ impl From<&str> for Rfc3339Format {
117117 }
118118}
119119
120+ /// Parse military timezone with optional hour offset.
121+ /// Pattern: single letter (a-z except j) optionally followed by 1-2 digits.
122+ /// Returns Some(total_hours_in_utc) or None if pattern doesn't match.
123+ ///
124+ /// Military timezone mappings:
125+ /// - A-I: UTC+1 to UTC+9 (J is skipped for local time)
126+ /// - K-M: UTC+10 to UTC+12
127+ /// - N-Y: UTC-1 to UTC-12
128+ /// - Z: UTC+0
129+ ///
130+ /// The hour offset from digits is added to the base military timezone offset.
131+ /// Examples: "m" -> 12 (noon UTC), "m9" -> 21 (9pm UTC), "a5" -> 4 (4am UTC next day)
132+ fn parse_military_timezone_with_offset ( s : & str ) -> Option < i32 > {
133+ if s. is_empty ( ) || s. len ( ) > 3 {
134+ return None ;
135+ }
136+
137+ let mut chars = s. chars ( ) ;
138+ let letter = chars. next ( ) ?. to_ascii_lowercase ( ) ;
139+
140+ // Check if first character is a letter (a-z, except j which is handled separately)
141+ if !letter. is_ascii_lowercase ( ) || letter == 'j' {
142+ return None ;
143+ }
144+
145+ // Parse optional digits (1-2 digits for hour offset)
146+ let additional_hours: i32 = if let Some ( rest) = chars. as_str ( ) . chars ( ) . next ( ) {
147+ if !rest. is_ascii_digit ( ) {
148+ return None ;
149+ }
150+ chars. as_str ( ) . parse ( ) . ok ( ) ?
151+ } else {
152+ 0
153+ } ;
154+
155+ // Map military timezone letter to UTC offset
156+ let tz_offset = match letter {
157+ 'a' ..='i' => ( letter as i32 - 'a' as i32 ) + 1 , // A=+1, B=+2, ..., I=+9
158+ 'k' ..='m' => ( letter as i32 - 'k' as i32 ) + 10 , // K=+10, L=+11, M=+12
159+ 'n' ..='y' => -( ( letter as i32 - 'n' as i32 ) + 1 ) , // N=-1, O=-2, ..., Y=-12
160+ 'z' => 0 , // Z=+0
161+ _ => return None ,
162+ } ;
163+
164+ // Calculate total hours: midnight (0) + tz_offset + additional_hours
165+ // Midnight in timezone X converted to UTC
166+ let total_hours = ( 0 - tz_offset + additional_hours) . rem_euclid ( 24 ) ;
167+
168+ Some ( total_hours)
169+ }
170+
120171#[ uucore:: main]
121172#[ allow( clippy:: cognitive_complexity) ]
122173pub fn uumain ( args : impl uucore:: Args ) -> UResult < ( ) > {
@@ -215,6 +266,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
215266 // GNU date accepts it and treats it as midnight today (00:00:00).
216267 let is_military_j = input. eq_ignore_ascii_case ( "j" ) ;
217268
269+ // GNU compatibility (Military timezone with optional hour offset):
270+ // Single letter (a-z except j) optionally followed by 1-2 digits.
271+ // Letter represents midnight in that military timezone (UTC offset).
272+ // Digits represent additional hours to add.
273+ // Examples: "m" -> noon UTC (12:00); "m9" -> 21:00 UTC; "a5" -> 04:00 UTC
274+ let military_tz_with_offset = parse_military_timezone_with_offset ( input) ;
275+
218276 // GNU compatibility (Pure numbers in date strings):
219277 // - Manual: https://www.gnu.org/software/coreutils/manual/html_node/Pure-numbers-in-date-strings.html
220278 // - Semantics: a pure decimal number denotes today's time-of-day (HH or HHMM).
@@ -238,6 +296,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
238296 format ! ( "{date_part} 00:00 {offset}" )
239297 } ;
240298 parse_date ( composed)
299+ } else if let Some ( total_hours) = military_tz_with_offset {
300+ // Military timezone with optional hour offset
301+ // Convert to UTC time: midnight + military_tz_offset + additional_hours
302+ let date_part =
303+ strtime:: format ( "%F" , & now) . unwrap_or_else ( |_| String :: from ( "1970-01-01" ) ) ;
304+ let composed = format ! ( "{date_part} {total_hours:02}:00:00 +00:00" ) ;
305+ parse_date ( composed)
241306 } else if is_pure_digits {
242307 // Derive HH and MM from the input
243308 let ( hh_opt, mm_opt) = if input. len ( ) <= 2 {
@@ -742,3 +807,24 @@ fn set_system_datetime(date: Zoned) -> UResult<()> {
742807 Ok ( ( ) )
743808 }
744809}
810+
811+ #[ cfg( test) ]
812+ mod tests {
813+ use super :: * ;
814+
815+ #[ test]
816+ fn test_parse_military_timezone_with_offset ( ) {
817+ // Valid cases: letter only, letter + digit, uppercase
818+ assert_eq ! ( parse_military_timezone_with_offset( "m" ) , Some ( 12 ) ) ; // UTC+12 -> 12:00 UTC
819+ assert_eq ! ( parse_military_timezone_with_offset( "m9" ) , Some ( 21 ) ) ; // 12 + 9 = 21
820+ assert_eq ! ( parse_military_timezone_with_offset( "a5" ) , Some ( 4 ) ) ; // 23 + 5 = 28 % 24 = 4
821+ assert_eq ! ( parse_military_timezone_with_offset( "z" ) , Some ( 0 ) ) ; // UTC+0 -> 00:00 UTC
822+ assert_eq ! ( parse_military_timezone_with_offset( "M9" ) , Some ( 21 ) ) ; // Uppercase works
823+
824+ // Invalid cases: 'j' reserved, empty, too long, starts with digit
825+ assert_eq ! ( parse_military_timezone_with_offset( "j" ) , None ) ; // Reserved for local time
826+ assert_eq ! ( parse_military_timezone_with_offset( "" ) , None ) ; // Empty
827+ assert_eq ! ( parse_military_timezone_with_offset( "m999" ) , None ) ; // Too long
828+ assert_eq ! ( parse_military_timezone_with_offset( "9m" ) , None ) ; // Starts with digit
829+ }
830+ }
0 commit comments