diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 9d25dbd587a..23eaf540047 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -418,8 +418,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // - Semantics: a pure decimal number denotes today's time-of-day (HH or HHMM). // Examples: "0"/"00" => 00:00 today; "7"/"07" => 07:00 today; "0700" => 07:00 today. // For all other forms, fall back to the general parser. - let is_pure_digits = - !input.is_empty() && input.len() <= 4 && input.chars().all(|c| c.is_ascii_digit()); + // + // GNU compatibility (Military timezone 'J' after a time): + // 'J' is local time, so "j"/"J" is the same time-of-day form + // as the bare "" input ("9j" == "9"). Strip it before the digit check. + let time_digits = input.strip_suffix(['j', 'J']).unwrap_or(input); + let is_pure_digits = !time_digits.is_empty() + && time_digits.len() <= 4 + && time_digits.chars().all(|c| c.is_ascii_digit()); let date = if is_empty_or_whitespace || is_military_j { // Treat empty string or 'J' as midnight today (00:00:00) in local time @@ -463,11 +469,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let composed = format!("{date_part} {total_hours:02}:00:00 +00:00"); parse_date(composed, &now, DebugOptions::new(settings.debug, false)) } else if is_pure_digits { - // Derive HH and MM from the input - let (hh_opt, mm_opt) = if input.len() <= 2 { - (input.parse::().ok(), Some(0u32)) + // Derive HH and MM from the digits + let (hh_opt, mm_opt) = if time_digits.len() <= 2 { + (time_digits.parse::().ok(), Some(0u32)) } else { - let (h, m) = input.split_at(input.len() - 2); + let (h, m) = time_digits.split_at(time_digits.len() - 2); (h.parse::().ok(), m.parse::().ok()) }; diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 539867fca18..763e4a844fd 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1263,6 +1263,41 @@ fn test_date_military_timezone_j_variations() { .stdout_contains("UTC"); } +#[test] +fn test_date_military_timezone_j_with_time() { + // 'J' is local time, so "j" is the time-of-day form (HH or HHMM) + // in the local zone, same as the bare "" input. GNU accepts it. + let test_cases = vec![ + ("8j", "08:00:00"), + ("9j", "09:00:00"), + ("9J", "09:00:00"), + ("12j", "12:00:00"), + ("1230j", "12:30:00"), + ("0j", "00:00:00"), + ("00j", "00:00:00"), + ]; + + for (input, expected) in test_cases { + new_ucmd!() + .env("TZ", "UTC") + .arg("-d") + .arg(input) + .arg("+%T") + .succeeds() + .stdout_is(format!("{expected}\n")); + } + + // Out-of-range times are rejected, same as the bare digit form + for input in ["2400j", "2360j", "12345j"] { + new_ucmd!() + .env("TZ", "UTC") + .arg("-d") + .arg(input) + .fails() + .stderr_contains("invalid date"); + } +} + #[test] fn test_date_empty_string() { // Empty string should be treated as midnight today @@ -1973,20 +2008,19 @@ fn test_date_bare_timezone_abbreviation() { } #[test] -#[ignore = "https://github.com/uutils/parse_datetime/issues/279 — GNU date silently ignores unrecognized trailing tokens (e.g. `8j`), but parse_datetime rejects them."] +#[ignore = "https://github.com/uutils/parse_datetime/issues/279. GNU date silently ignores unrecognized trailing tokens (e.g. `8 j`), but parse_datetime rejects them."] fn test_date_ignores_unrecognized_trailing_tokens() { // GNU compatibility: trailing unknown word-tokens after a valid number are ignored. - // GNU parses `8j`, `8 j`, etc. as hour 8; our parse_datetime crate errors out. - for input in ["8j", "8 j"] { - new_ucmd!() - .env("TZ", "UTC") - .arg("-u") - .arg("-d") - .arg(input) - .arg("+%H:%M:%S") - .succeeds() - .stdout_only("08:00:00\n"); - } + // GNU parses `8 j` (number, space, token) as hour 8; our parse_datetime crate errors out. + // The no-space `8j` form is handled directly (see test_date_military_timezone_j_with_time). + new_ucmd!() + .env("TZ", "UTC") + .arg("-u") + .arg("-d") + .arg("8 j") + .arg("+%H:%M:%S") + .succeeds() + .stdout_only("08:00:00\n"); } #[test]