From 7f6e065b82ab70005f420dd34216630713c4a6d5 Mon Sep 17 00:00:00 2001 From: Toby Wang Date: Wed, 8 Apr 2026 21:56:35 +1000 Subject: [PATCH 1/3] tests/date: add test date strftime O and E modifier --- 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 7096e2040ee..6e29600f83d 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1965,7 +1965,6 @@ fn test_date_strftime_flag_on_composite() { } #[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() { // In C locale the `O` modifier is a no-op (alternative numeric symbols). // GNU renders `%Om` as `06` for June; uutils renders it as the literal `%Om`. @@ -1979,6 +1978,20 @@ fn test_date_strftime_o_modifier() { .stdout_is("06-24-12\n"); } +#[test] +fn test_date_strftime_e_modifier() { + // In C locale the `E` modifier is a no-op (alternative era). + // GNU does not render `%Em`; uutils renders it as the literal `%Em` as `06` for June. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15") + .arg("+%Em-%Ey-%El") + .succeeds() + .stdout_is("06-24-12\n"); +} + #[test] #[ignore = "https://github.com/uutils/parse_datetime/issues/280 — GNU date accepts bare timezone abbreviations (UT, GMT, ...) meaning `now in that TZ`; parse_datetime rejects them."] fn test_date_bare_timezone_abbreviation() { From 2a72c8ea88f3ae8ceeb71667368afa1b53c1dfde Mon Sep 17 00:00:00 2001 From: Toby Wang Date: Wed, 15 Apr 2026 12:29:50 +1000 Subject: [PATCH 2/3] date: add exlcusions to modifier O and E --- src/uu/date/src/format_modifiers.rs | 42 +++++++++++++++++++++++++++-- tests/by-util/test_date.rs | 8 +++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index fd269379a42..dd80e36d86e 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -99,7 +99,12 @@ fn parse_format_spec(s: &str) -> Option> { // Flags: any of [_0^#+-], zero or more. let flags_start = pos; - while pos < bytes.len() && matches!(bytes[pos], b'_' | b'0' | b'^' | b'#' | b'+' | b'-') { + while pos < bytes.len() + && matches!( + bytes[pos], + b'_' | b'0' | b'^' | b'#' | b'+' | b'-' | b'O' | b'E' + ) + { pos += 1; } let flags = &s[flags_start..pos]; @@ -223,7 +228,40 @@ fn format_with_modifiers( base_format.clear(); base_format.push('%'); base_format.push_str(parsed.spec); - let formatted = broken_down.to_string_with_config(config, &base_format)?; + let mut formatted = String::new(); + if parsed.flags == "E" { + // modifier applies to the ‘%c’, ‘%C’, ‘%x’, ‘%X’, ‘%y’ and ‘%Y’ conversion specifiers. + if matches!(parsed.spec, "c" | "C" | "x" | "X" | "y" | "Y") { + formatted.push_str( + broken_down + .to_string_with_config(config, &base_format)? + .as_str(), + ); + } else { + formatted.push('%'); + formatted.push_str(parsed.flags); + formatted.push_str(parsed.spec); + } + } else if parsed.flags == "O" { + // Modifier 'O' applies only to numeric conversion specifiers. + if matches!(parsed.spec, "a" | "A" | "c" | "D" | "F" | "x" | "X") { + formatted.push('%'); + formatted.push_str(parsed.flags); + formatted.push_str(parsed.spec); + } else { + formatted.push_str( + broken_down + .to_string_with_config(config, &base_format)? + .as_str(), + ); + } + } else { + formatted.push_str( + broken_down + .to_string_with_config(config, &base_format)? + .as_str(), + ); + } if !parsed.flags.is_empty() || parsed.width.is_some() { let modified = apply_modifiers(&formatted, &parsed)?; diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 6e29600f83d..187388f1dd1 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1967,7 +1967,7 @@ fn test_date_strftime_flag_on_composite() { #[test] fn test_date_strftime_o_modifier() { // In C locale the `O` modifier is a no-op (alternative numeric symbols). - // GNU renders `%Om` as `06` for June; uutils renders it as the literal `%Om`. + // GNU renders `%Om` as `06` for June. new_ucmd!() .env("LC_ALL", "C") .env("TZ", "UTC") @@ -1981,15 +1981,15 @@ fn test_date_strftime_o_modifier() { #[test] fn test_date_strftime_e_modifier() { // In C locale the `E` modifier is a no-op (alternative era). - // GNU does not render `%Em`; uutils renders it as the literal `%Em` as `06` for June. + // GNU renders `%Ex` as `06/15/24` for 2024-06-15. new_ucmd!() .env("LC_ALL", "C") .env("TZ", "UTC") .arg("-d") .arg("2024-06-15") - .arg("+%Em-%Ey-%El") + .arg("+%EC-%Ey-%Ex") .succeeds() - .stdout_is("06-24-12\n"); + .stdout_is("20-24-06/15/24\n"); } #[test] From 70df0d9ee952bf86267108cffb9ed912ecae74d9 Mon Sep 17 00:00:00 2001 From: Toby Wang Date: Wed, 15 Apr 2026 20:35:15 +1000 Subject: [PATCH 3/3] date: add implementation notes and test for invalid modifiers --- src/uu/date/src/format_modifiers.rs | 11 +++++++++- tests/by-util/test_date.rs | 31 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index dd80e36d86e..a6361045b68 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -229,8 +229,11 @@ fn format_with_modifiers( base_format.push('%'); base_format.push_str(parsed.spec); let mut formatted = String::new(); + // If modifier is 'E' or 'O', + // check if specifier is allowed to work with the modifier. if parsed.flags == "E" { - // modifier applies to the ‘%c’, ‘%C’, ‘%x’, ‘%X’, ‘%y’ and ‘%Y’ conversion specifiers. + // Modifier applies to the ‘%c’, ‘%C’, ‘%x’, ‘%X’, + // ‘%y’ and ‘%Y’ conversion specifiers. if matches!(parsed.spec, "c" | "C" | "x" | "X" | "y" | "Y") { formatted.push_str( broken_down @@ -238,6 +241,8 @@ fn format_with_modifiers( .as_str(), ); } else { + // Use unformatted string to display + // if specifier does not work with modifier 'E'. formatted.push('%'); formatted.push_str(parsed.flags); formatted.push_str(parsed.spec); @@ -245,10 +250,13 @@ fn format_with_modifiers( } else if parsed.flags == "O" { // Modifier 'O' applies only to numeric conversion specifiers. if matches!(parsed.spec, "a" | "A" | "c" | "D" | "F" | "x" | "X") { + // Use unformatted string to display + // if specifier does not work with modifier 'O'. formatted.push('%'); formatted.push_str(parsed.flags); formatted.push_str(parsed.spec); } else { + // All other specifiers work with modifier 'O'. formatted.push_str( broken_down .to_string_with_config(config, &base_format)? @@ -256,6 +264,7 @@ fn format_with_modifiers( ); } } else { + // If modifier is not 'E' either 'O', format the string with config. formatted.push_str( broken_down .to_string_with_config(config, &base_format)? diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 187388f1dd1..4d8e1648bae 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1978,6 +1978,21 @@ fn test_date_strftime_o_modifier() { .stdout_is("06-24-12\n"); } +#[test] +fn test_date_strftime_invalid_o_modifier() { + // In C locale the `O` modifier is a no-op (alternative numeric symbols). + // Modifier 'O' applies only to numeric conversion specifiers. + // `%Oa-%OA-%Oc` should be ignored. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15") + .arg("+%Oa-%OA-%Oc") + .succeeds() + .stdout_is("%Oa-%OA-%Oc\n"); +} + #[test] fn test_date_strftime_e_modifier() { // In C locale the `E` modifier is a no-op (alternative era). @@ -1992,6 +2007,22 @@ fn test_date_strftime_e_modifier() { .stdout_is("20-24-06/15/24\n"); } +#[test] +fn test_date_strftime_invalid_e_modifier() { + // In C locale the `E` modifier is a no-op (alternative era). + // This modifier applies to the + // '%c', '%C', '%x', '%X', '%y' and '%Y' conversion specifiers. + // `%Ea-%Em-%El` should be ignored. + new_ucmd!() + .env("LC_ALL", "C") + .env("TZ", "UTC") + .arg("-d") + .arg("2024-06-15") + .arg("+%Ea-%Em-%El") + .succeeds() + .stdout_is("%Ea-%Em-%El\n"); +} + #[test] #[ignore = "https://github.com/uutils/parse_datetime/issues/280 — GNU date accepts bare timezone abbreviations (UT, GMT, ...) meaning `now in that TZ`; parse_datetime rejects them."] fn test_date_bare_timezone_abbreviation() {