From 0e51ec87222bb7728954bbc0000927ec9eae274c Mon Sep 17 00:00:00 2001 From: abusalah0 <109g83@gmail.com> Date: Fri, 10 Apr 2026 21:21:26 +0300 Subject: [PATCH 1/3] date: treat composite strftime specifiers as atomic --- src/uu/date/src/format_modifiers.rs | 42 ++++++++++++++++++++++++++--- tests/by-util/test_date.rs | 35 +++++++++++++++++++++++- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index 7cdf32b174c..cbb3e0f6203 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -183,13 +183,43 @@ fn is_text_specifier(specifier: &str) -> bool { ) } +/// Returns true if the specifier is a composite strftime format. +/// +/// GNU date applies flags/width to the rendered composite output as a whole, +/// instead of propagating modifiers to inner sub-fields. +fn is_atomic_composite_specifier(specifier: &str) -> bool { + matches!( + specifier.chars().last(), + Some('D' | 'F' | 'T' | 'r' | 'R' | 'c' | 'x' | 'X') + ) +} + /// Returns true if the specifier defaults to space padding. /// This includes text specifiers and numeric specifiers like %e and %k /// that use blank-padding by default in GNU date. fn is_space_padded_specifier(specifier: &str) -> bool { matches!( specifier.chars().last(), - Some('A' | 'a' | 'B' | 'b' | 'h' | 'Z' | 'p' | 'P' | 'e' | 'k' | 'l') + Some( + 'A' | 'a' + | 'B' + | 'b' + | 'h' + | 'Z' + | 'p' + | 'P' + | 'e' + | 'k' + | 'l' + | 'D' + | 'F' + | 'T' + | 'r' + | 'R' + | 'c' + | 'x' + | 'X' + ) ) } @@ -276,6 +306,7 @@ fn apply_modifiers( explicit_width: bool, ) -> Result { let mut result = value.to_string(); + let is_atomic_composite = is_atomic_composite_specifier(specifier); // Determine default pad character based on specifier type // Determine default pad character based on specifier type. @@ -347,6 +378,9 @@ fn apply_modifiers( // If no_pad flag is active, suppress all padding and return if no_pad { + if is_atomic_composite { + return Ok(result); + } return Ok(strip_default_padding(&result)); } @@ -360,12 +394,12 @@ fn apply_modifiers( }; // When the requested width is narrower than the default formatted width, GNU first removes default padding and then reapplies the requested width. - if effective_width > 0 && effective_width < result.len() { + if !is_atomic_composite && effective_width > 0 && effective_width < result.len() { result = strip_default_padding(&result); } // Strip default padding when switching pad characters on numeric fields - if !is_text_specifier(specifier) && result.len() >= 2 { + if !is_atomic_composite && !is_text_specifier(specifier) && result.len() >= 2 { if pad_char == ' ' && result.starts_with('0') { // Switching to space padding: strip leading zeros result = strip_default_padding(&result); @@ -379,7 +413,7 @@ fn apply_modifiers( // GNU behavior: + only adds sign if: // 1. An explicit width is provided, OR // 2. The value exceeds the default width for that specifier (e.g., year > 4 digits) - if force_sign && !result.starts_with('+') && !result.starts_with('-') { + if force_sign && !is_atomic_composite && !result.starts_with('+') && !result.starts_with('-') { if result.chars().next().is_some_and(|c| c.is_ascii_digit()) { let default_w = get_default_width(specifier); // Add sign only if explicit width provided OR result exceeds default width diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 7096e2040ee..c5d3f1cc0d1 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1950,7 +1950,6 @@ fn test_date_strftime_n_width_and_flags() { } #[test] -#[ignore = "https://github.com/uutils/coreutils/issues/11657 — GNU date treats composite strftime specifiers (%D, %F, %T, ...) as atomic; flags like `-` should not propagate to sub-fields."] fn test_date_strftime_flag_on_composite() { // GNU `%-D` keeps `06/15/24` (flag ignored on composite). // uutils applies `-` to inner `%m`, producing `6/15/24`. @@ -1964,6 +1963,40 @@ fn test_date_strftime_flag_on_composite() { .stdout_is("06/15/24\n"); } +#[test] +fn test_date_strftime_composite_modifiers_are_atomic() { + let test_cases = [ + ("+%-D", "06/15/24\n"), + ("+%-F", "2024-06-15\n"), + ("+%-T", "03:04:05\n"), + ("+%-r", "03:04:05 AM\n"), + ("+%-R", "03:04\n"), + ("+%-c", "Sat Jun 15 03:04:05 2024\n"), + ("+%-x", "06/15/24\n"), + ("+%-X", "03:04:05\n"), + ("+%_D", "06/15/24\n"), + ("+%10D", " 06/15/24\n"), + ("+%010D", "0006/15/24\n"), + ("+%-10D", "06/15/24\n"), + ("+%10T", " 03:04:05\n"), + ("+%10R", " 03:04\n"), + ("+%10x", " 06/15/24\n"), + ("+%10X", " 03:04:05\n"), + ("+%+10D", "0006/15/24\n"), + ]; + + for (format, expected) in test_cases { + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15 03:04:05") + .arg(format) + .succeeds() + .stdout_is(expected); + } +} + #[test] #[ignore = "https://github.com/uutils/coreutils/issues/11656 — GNU date strips the `O` strftime modifier in C locale (e.g. `%Om` -> `%m`); uutils leaks it as literal `%om`."] fn test_date_strftime_o_modifier() { From 497a37c9018e21f26db1e0c23e0796ddf2c858ba Mon Sep 17 00:00:00 2001 From: abusalah0 <109g83@gmail.com> Date: Fri, 10 Apr 2026 22:07:10 +0300 Subject: [PATCH 2/3] tests/date: add %+10D composite parity test --- tests/by-util/test_date.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index c5d3f1cc0d1..69f061ab749 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1982,7 +1982,6 @@ fn test_date_strftime_composite_modifiers_are_atomic() { ("+%10R", " 03:04\n"), ("+%10x", " 06/15/24\n"), ("+%10X", " 03:04:05\n"), - ("+%+10D", "0006/15/24\n"), ]; for (format, expected) in test_cases { @@ -1997,6 +1996,20 @@ fn test_date_strftime_composite_modifiers_are_atomic() { } } +#[test] +fn test_date_strftime_plus_width_on_composite() { + // GNU applies %+10D to the full composite output with zero padding, + // and does not inject a leading sign into the composite string. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15 03:04:05") + .arg("+%+10D") + .succeeds() + .stdout_is("0006/15/24\n"); +} + #[test] #[ignore = "https://github.com/uutils/coreutils/issues/11656 — GNU date strips the `O` strftime modifier in C locale (e.g. `%Om` -> `%m`); uutils leaks it as literal `%om`."] fn test_date_strftime_o_modifier() { From 4e23b1cd500d752129d6af5d0b00f258df6dd574 Mon Sep 17 00:00:00 2001 From: abusalah0 <109g83@gmail.com> Date: Mon, 13 Apr 2026 11:31:28 +0300 Subject: [PATCH 3/3] date: address composite modifier review feedback --- src/uu/date/src/format_modifiers.rs | 26 ++++---------------------- tests/by-util/test_date.rs | 3 +++ 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index cbb3e0f6203..20ae8de9e51 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -198,29 +198,11 @@ fn is_atomic_composite_specifier(specifier: &str) -> bool { /// This includes text specifiers and numeric specifiers like %e and %k /// that use blank-padding by default in GNU date. fn is_space_padded_specifier(specifier: &str) -> bool { - matches!( - specifier.chars().last(), - Some( - 'A' | 'a' - | 'B' - | 'b' - | 'h' - | 'Z' - | 'p' - | 'P' - | 'e' - | 'k' - | 'l' - | 'D' - | 'F' - | 'T' - | 'r' - | 'R' - | 'c' - | 'x' - | 'X' + is_atomic_composite_specifier(specifier) + || matches!( + specifier.chars().last(), + Some('A' | 'a' | 'B' | 'b' | 'h' | 'Z' | 'p' | 'P' | 'e' | 'k' | 'l') ) - ) } /// Returns the default width for a specifier. diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 69f061ab749..f02f172bd3f 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1967,11 +1967,14 @@ fn test_date_strftime_flag_on_composite() { fn test_date_strftime_composite_modifiers_are_atomic() { let test_cases = [ ("+%-D", "06/15/24\n"), + ("+%^D", "06/15/24\n"), ("+%-F", "2024-06-15\n"), ("+%-T", "03:04:05\n"), ("+%-r", "03:04:05 AM\n"), ("+%-R", "03:04\n"), ("+%-c", "Sat Jun 15 03:04:05 2024\n"), + ("+%^c", "SAT JUN 15 03:04:05 2024\n"), + ("+%#c", "SAT JUN 15 03:04:05 2024\n"), ("+%-x", "06/15/24\n"), ("+%-X", "03:04:05\n"), ("+%_D", "06/15/24\n"),