diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index 7cdf32b174c..20ae8de9e51 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -183,14 +183,26 @@ 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') - ) + 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. @@ -276,6 +288,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 +360,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 +376,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 +395,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..f02f172bd3f 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,56 @@ 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"), + ("+%^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"), + ("+%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"), + ]; + + 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] +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() {