From ab15db6beadf8735da23783cc935411398b402db Mon Sep 17 00:00:00 2001 From: Navaneeth Yadamreddy Date: Sat, 26 Sep 2026 23:39:52 +0530 Subject: [PATCH 1/2] date: match GNU's padding rules for composite strftime specifiers GNU treats %D as atomic, so its pad flags never reach the inner %m/%d fields: `date +%-D` prints 06/15/24, not 6/15/24. Drop the pad flags before a composite specifier and leave the width and the other modifiers alone. The other composites are not atomic, and they need their own handling: %-F keeps the padding of the year, so `%-F` on 0001-01-01 stays 0001-01-01 rather than losing the year padding %T is expanded by jiff with a single-digit hour left unpadded, so put the zero back before any width or flag is applied The `%%` literal and a trailing `%` are left untouched, and an absurd width is still rejected rather than truncated. --- src/uu/date/src/date.rs | 47 ++++++++++++++++++- src/uu/date/src/format_modifiers.rs | 10 ++++ tests/by-util/test_date.rs | 72 ++++++++++++++++++++++++++++- 3 files changed, 126 insertions(+), 3 deletions(-) diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 103b05d6637..e888ae63b97 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -771,6 +771,50 @@ fn substitute_epoch_seconds(fmt: &str, date: &Zoned) -> String { out } +/// Drop the `-` and `_` pad flags before `%D`, which GNU treats as atomic but +/// jiff expands into `%m/%d/%y`, where they do apply. +fn strip_modifiers_on_composite(fmt: &str) -> String { + if !fmt.contains('%') { + return fmt.to_string(); + } + + let mut out = String::with_capacity(fmt.len()); + let mut chars = fmt.chars().peekable(); + while let Some(c) = chars.next() { + if c != '%' { + out.push(c); + continue; + } + // Skip `%%` literally. + if chars.peek() == Some(&'%') { + chars.next(); + out.push_str("%%"); + continue; + } + // Look ahead: optional pad flags (`-`/`_`), other modifiers, then `%D`. + let mut ahead = chars.clone(); + while ahead + .peek() + .is_some_and(|&m| m == '-' || m == '_' || "_0^#+".contains(m) || m.is_ascii_digit()) + { + ahead.next(); + } + if ahead.peek() == Some(&'D') { + out.push('%'); + // Consume only the pad flags (`-`/`_`); keep width and other flags. + while chars.peek().is_some_and(|&m| m == '-' || m == '_') { + chars.next(); + } + if let Some(specifier) = chars.next() { + out.push(specifier); + } + } else { + out.push('%'); + } + } + out +} + /// Remove the `O` strftime modifier from `fmt`. /// /// In the C locale `%O` requests alternative numeric symbols that do not @@ -908,7 +952,8 @@ fn format_date_with_locale_aware_months( // negative infinity (e.g. `@-1.5` → `-2`, not `-1`). Every other field jiff // produces already agrees with GNU, so only `%s` needs correcting; rewrite it // to the floored epoch second before jiff sees the format string. - let fmt_owned = strip_o_modifier(&substitute_epoch_seconds(fmt, date)); + let fmt_owned = + strip_modifiers_on_composite(&strip_o_modifier(&substitute_epoch_seconds(fmt, date))); let fmt = fmt_owned.as_str(); // Check if format string has GNU modifiers (width/flags) and format if present diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index 35e1d8d9982..9d17f1fea74 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -425,6 +425,10 @@ fn apply_modifiers(value: &str, parsed: &ParsedSpec<'_>) -> Result) -> Result Date: Sat, 3 Oct 2026 11:03:44 +0530 Subject: [PATCH 2/2] date: keep the hour padded for %R and %r under pad flags The hour correction was keyed to `%T` with a hard-coded length, so a space-pad flag on another time composite stripped the hour's zero padding and nothing put it back: %_R -> 7:08 (GNU: 07:08) %_10R -> " 7:08" (GNU: " 07:08") %_r -> 7:08:09 AM (GNU: 07:08:09 AM) Unflagged %R was already correct and is now covered too. A single digit followed by ':' can only be an unpadded hour, so match the shape instead of one composite's length. The existing %_10T case is unaffected. --- src/uu/date/src/format_modifiers.rs | 9 +++-- tests/by-util/test_date.rs | 63 +++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index 9d17f1fea74..10f9b1c4833 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -460,9 +460,12 @@ fn apply_modifiers(value: &str, parsed: &ParsedSpec<'_>) -> Result= 2 && bytes[0].is_ascii_digit() && bytes[1] == b':' { result.insert(0, '0'); } diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 500b70771b3..a3295b1bebc 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -2082,6 +2082,69 @@ fn test_date_strftime_flag_on_composite() { .succeeds() .stdout_is(" 07:08:09\n"); + // A space-pad flag switches %R off zero padding and used to leave the hour + // unpadded; GNU keeps the hour's own zero padding and only pads the + // outer width with spaces. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15 07:08:09") + .arg("+%_R") + .succeeds() + .stdout_is("07:08\n"); + + // Same with an explicit width: the spaces pad the outside, the hour stays + // zero padded (this is the `%_10T` behaviour above, applied to %R). + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15 07:08:09") + .arg("+%_10R") + .succeeds() + .stdout_is(" 07:08\n"); + + // %r is the 12-hour composite and had the same problem. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15 07:08:09") + .arg("+%_r") + .succeeds() + .stdout_is("07:08:09 AM\n"); + + // An unflagged %R was already correct; keep it covered. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15 07:08:09") + .arg("+%R") + .succeeds() + .stdout_is("07:08\n"); + + // An hour of ten or more was already padded; leave it alone. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15 10:08:09") + .arg("+%_R") + .succeeds() + .stdout_is("10:08\n"); + + // `%-R` still asks for the padding to be removed. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15 07:08:09") + .arg("+%-R") + .succeeds() + .stdout_is("7:08\n"); + // `%_D` is stripped the same way as `%-D`. new_ucmd!() .env("LC_ALL", "C")