Skip to content

Commit 0e51ec8

Browse files
committed
date: treat composite strftime specifiers as atomic
1 parent 130de6f commit 0e51ec8

2 files changed

Lines changed: 72 additions & 5 deletions

File tree

src/uu/date/src/format_modifiers.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,43 @@ fn is_text_specifier(specifier: &str) -> bool {
183183
)
184184
}
185185

186+
/// Returns true if the specifier is a composite strftime format.
187+
///
188+
/// GNU date applies flags/width to the rendered composite output as a whole,
189+
/// instead of propagating modifiers to inner sub-fields.
190+
fn is_atomic_composite_specifier(specifier: &str) -> bool {
191+
matches!(
192+
specifier.chars().last(),
193+
Some('D' | 'F' | 'T' | 'r' | 'R' | 'c' | 'x' | 'X')
194+
)
195+
}
196+
186197
/// Returns true if the specifier defaults to space padding.
187198
/// This includes text specifiers and numeric specifiers like %e and %k
188199
/// that use blank-padding by default in GNU date.
189200
fn is_space_padded_specifier(specifier: &str) -> bool {
190201
matches!(
191202
specifier.chars().last(),
192-
Some('A' | 'a' | 'B' | 'b' | 'h' | 'Z' | 'p' | 'P' | 'e' | 'k' | 'l')
203+
Some(
204+
'A' | 'a'
205+
| 'B'
206+
| 'b'
207+
| 'h'
208+
| 'Z'
209+
| 'p'
210+
| 'P'
211+
| 'e'
212+
| 'k'
213+
| 'l'
214+
| 'D'
215+
| 'F'
216+
| 'T'
217+
| 'r'
218+
| 'R'
219+
| 'c'
220+
| 'x'
221+
| 'X'
222+
)
193223
)
194224
}
195225

@@ -276,6 +306,7 @@ fn apply_modifiers(
276306
explicit_width: bool,
277307
) -> Result<String, FormatError> {
278308
let mut result = value.to_string();
309+
let is_atomic_composite = is_atomic_composite_specifier(specifier);
279310

280311
// Determine default pad character based on specifier type
281312
// Determine default pad character based on specifier type.
@@ -347,6 +378,9 @@ fn apply_modifiers(
347378

348379
// If no_pad flag is active, suppress all padding and return
349380
if no_pad {
381+
if is_atomic_composite {
382+
return Ok(result);
383+
}
350384
return Ok(strip_default_padding(&result));
351385
}
352386

@@ -360,12 +394,12 @@ fn apply_modifiers(
360394
};
361395

362396
// When the requested width is narrower than the default formatted width, GNU first removes default padding and then reapplies the requested width.
363-
if effective_width > 0 && effective_width < result.len() {
397+
if !is_atomic_composite && effective_width > 0 && effective_width < result.len() {
364398
result = strip_default_padding(&result);
365399
}
366400

367401
// Strip default padding when switching pad characters on numeric fields
368-
if !is_text_specifier(specifier) && result.len() >= 2 {
402+
if !is_atomic_composite && !is_text_specifier(specifier) && result.len() >= 2 {
369403
if pad_char == ' ' && result.starts_with('0') {
370404
// Switching to space padding: strip leading zeros
371405
result = strip_default_padding(&result);
@@ -379,7 +413,7 @@ fn apply_modifiers(
379413
// GNU behavior: + only adds sign if:
380414
// 1. An explicit width is provided, OR
381415
// 2. The value exceeds the default width for that specifier (e.g., year > 4 digits)
382-
if force_sign && !result.starts_with('+') && !result.starts_with('-') {
416+
if force_sign && !is_atomic_composite && !result.starts_with('+') && !result.starts_with('-') {
383417
if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
384418
let default_w = get_default_width(specifier);
385419
// Add sign only if explicit width provided OR result exceeds default width

tests/by-util/test_date.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1950,7 +1950,6 @@ fn test_date_strftime_n_width_and_flags() {
19501950
}
19511951

19521952
#[test]
1953-
#[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."]
19541953
fn test_date_strftime_flag_on_composite() {
19551954
// GNU `%-D` keeps `06/15/24` (flag ignored on composite).
19561955
// uutils applies `-` to inner `%m`, producing `6/15/24`.
@@ -1964,6 +1963,40 @@ fn test_date_strftime_flag_on_composite() {
19641963
.stdout_is("06/15/24\n");
19651964
}
19661965

1966+
#[test]
1967+
fn test_date_strftime_composite_modifiers_are_atomic() {
1968+
let test_cases = [
1969+
("+%-D", "06/15/24\n"),
1970+
("+%-F", "2024-06-15\n"),
1971+
("+%-T", "03:04:05\n"),
1972+
("+%-r", "03:04:05 AM\n"),
1973+
("+%-R", "03:04\n"),
1974+
("+%-c", "Sat Jun 15 03:04:05 2024\n"),
1975+
("+%-x", "06/15/24\n"),
1976+
("+%-X", "03:04:05\n"),
1977+
("+%_D", "06/15/24\n"),
1978+
("+%10D", " 06/15/24\n"),
1979+
("+%010D", "0006/15/24\n"),
1980+
("+%-10D", "06/15/24\n"),
1981+
("+%10T", " 03:04:05\n"),
1982+
("+%10R", " 03:04\n"),
1983+
("+%10x", " 06/15/24\n"),
1984+
("+%10X", " 03:04:05\n"),
1985+
("+%+10D", "0006/15/24\n"),
1986+
];
1987+
1988+
for (format, expected) in test_cases {
1989+
new_ucmd!()
1990+
.env("LC_ALL", "C")
1991+
.env("TZ", "UTC")
1992+
.arg("-d")
1993+
.arg("2024-06-15 03:04:05")
1994+
.arg(format)
1995+
.succeeds()
1996+
.stdout_is(expected);
1997+
}
1998+
}
1999+
19672000
#[test]
19682001
#[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`."]
19692002
fn test_date_strftime_o_modifier() {

0 commit comments

Comments
 (0)