From 284554658d9c1acc7a855ca4327089b33ecbd9a5 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 25 Oct 2025 22:00:02 +0200 Subject: [PATCH] date: add support for the 'J' military timezone --- src/uu/date/src/date.rs | 24 ++++++++++++++++++++++-- tests/by-util/test_date.rs | 27 ++++++++++++++++++++------- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index e4c2b286d..3052d1c5e 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -205,15 +205,35 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Iterate over all dates - whether it's a single date or a file. let dates: Box> = match settings.date_source { DateSource::Human(ref input) => { + // GNU compatibility (Military timezone 'J'): + // 'J' is reserved for local time in military timezones. + // GNU date accepts it and treats it as midnight today (00:00:00). + let is_military_j = input.eq_ignore_ascii_case("j"); + // GNU compatibility (Pure numbers in date strings): // - Manual: https://www.gnu.org/software/coreutils/manual/html_node/Pure-numbers-in-date-strings.html - // - Semantics: a pure decimal number denotes today’s time-of-day (HH or HHMM). + // - 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()); - let date = if is_pure_digits { + let date = if is_military_j { + // Treat 'J' as midnight today (00:00:00) in local time + let date_part = + strtime::format("%F", &now).unwrap_or_else(|_| String::from("1970-01-01")); + let offset = if settings.utc { + String::from("+00:00") + } else { + strtime::format("%:z", &now).unwrap_or_default() + }; + let composed = if offset.is_empty() { + format!("{date_part} 00:00") + } else { + format!("{date_part} 00:00 {offset}") + }; + parse_date(composed) + } 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)) diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index c3e7a132d..94492d765 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -946,17 +946,30 @@ fn test_date_tz_abbreviation_unknown() { } #[test] -#[ignore = "we reject 'J', GNU treats as midnight"] -fn test_date_fuzz_military_timezone_j() { - // J is reserved for local time in military timezones - // GNU date treats it as midnight, we reject it +fn test_date_military_timezone_j_variations() { + // Test multiple variations of 'J' input (case insensitive, with whitespace) + // All should produce midnight (00:00:00) + let test_cases = vec!["J", "j", " J ", " j ", "\tJ\t"]; + + for input in test_cases { + new_ucmd!() + .env("TZ", "UTC") + .arg("-d") + .arg(input) + .arg("+%T") + .succeeds() + .stdout_is("00:00:00\n"); + } + + // Test with -u flag to verify UTC behavior new_ucmd!() - .env("TZ", "UTC+1") + .arg("-u") .arg("-d") .arg("J") - .arg("+%F %T %Z") + .arg("+%T %Z") .succeeds() - .stdout_contains("00:00:00"); + .stdout_contains("00:00:00") + .stdout_contains("UTC"); } #[test]