diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index fd269379a42..a6361045b68 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,49 @@ 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 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. + 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 { + // 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); + } + } 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)? + .as_str(), + ); + } + } 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)? + .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 7096e2040ee..4d8e1648bae 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1965,10 +1965,9 @@ 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`. + // GNU renders `%Om` as `06` for June. new_ucmd!() .env("LC_ALL", "C") .env("TZ", "UTC") @@ -1979,6 +1978,51 @@ 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). + // 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("+%EC-%Ey-%Ex") + .succeeds() + .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() {