Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d87a5b8

Browse files
committed
date: support(Military timezone with optional hour offset
1 parent 5cae9ae commit d87a5b8

File tree

2 files changed

+117
-3
lines changed

2 files changed

+117
-3
lines changed

src/uu/date/src/date.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
122173
pub 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+
}

tests/by-util/test_date.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,13 +1011,41 @@ fn test_date_empty_string_variations() {
10111011
}
10121012

10131013
#[test]
1014-
#[ignore = "we produce year 0008, GNU gives today 12:00"]
1015-
fn test_date_fuzz_relative_m9() {
1016-
// Relative date string "m9" should be parsed correctly
1014+
fn test_date_relative_m9() {
1015+
// Military timezone "m9" should be parsed as noon + 9 hours = 21:00 UTC
1016+
// When displayed in TZ=UTC+9 (which is UTC-9), this shows as 12:00 local time
10171017
new_ucmd!()
10181018
.env("TZ", "UTC+9")
10191019
.arg("-d")
10201020
.arg("m9")
10211021
.succeeds()
10221022
.stdout_contains("12:00:00");
10231023
}
1024+
1025+
#[test]
1026+
fn test_date_military_timezone_with_offset_variations() {
1027+
// Test various military timezone + offset combinations
1028+
// Format: single letter (a-z except j) optionally followed by 1-2 digits
1029+
1030+
// Test cases: (input, expected_time_utc)
1031+
let test_cases = vec![
1032+
("a", "23:00:00"), // A = UTC+1, midnight in UTC+1 = 23:00 UTC
1033+
("m", "12:00:00"), // M = UTC+12, midnight in UTC+12 = 12:00 UTC
1034+
("z", "00:00:00"), // Z = UTC+0, midnight in UTC+0 = 00:00 UTC
1035+
("m9", "21:00:00"), // M + 9 hours = 12 + 9 = 21:00 UTC
1036+
("a5", "04:00:00"), // A + 5 hours = 23 + 5 = 04:00 UTC (next day)
1037+
("z3", "03:00:00"), // Z + 3 hours = 00 + 3 = 03:00 UTC
1038+
("M", "12:00:00"), // Uppercase should work too
1039+
("A5", "04:00:00"), // Uppercase with offset
1040+
];
1041+
1042+
for (input, expected) in test_cases {
1043+
new_ucmd!()
1044+
.env("TZ", "UTC")
1045+
.arg("-d")
1046+
.arg(input)
1047+
.arg("+%T")
1048+
.succeeds()
1049+
.stdout_is(format!("{expected}\n"));
1050+
}
1051+
}

0 commit comments

Comments
 (0)