Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions src/uu/date/src/format_modifiers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -276,6 +288,7 @@ fn apply_modifiers(
explicit_width: bool,
) -> Result<String, FormatError> {
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.
Expand Down Expand Up @@ -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));
}

Expand All @@ -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);
Expand All @@ -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
Expand Down
51 changes: 50 additions & 1 deletion tests/by-util/test_date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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 = [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no tests for, e.g.:

  • %^c → SAT JUN 15 03:04:05 2024
  • %#c → swap-cased composite
  • %^D → no-op on 06/15/24 (still 06/15/24)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just added tests for these 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() {
Expand Down
Loading