Skip to content

Commit 977f3d0

Browse files
committed
ls: honor LC_TIME for --time-style=locale via uucore helper
Add format_system_time_locale_aware in uucore::time that runs the format string through i18n::datetime::localize_format_string when a non-C LC_TIME locale is active, and route ls's display_date through it. Fixes month names and alternate-calendar years (Persian, Buddhist, Ethiopian) matching GNU. Also generalize uutests' is_locale_available to derive the expected charmap from the locale suffix so non-UTF-8 locales work, and consolidate the ls locale tests into a single data-driven case.
1 parent 0ea2617 commit 977f3d0

File tree

5 files changed

+254
-248
lines changed

5 files changed

+254
-248
lines changed

src/uu/ls/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ uucore = { workspace = true, features = [
3636
"fs",
3737
"fsext",
3838
"fsxattr",
39+
"i18n-datetime",
3940
"parser-size",
4041
"parser-glob",
4142
"quoting-style",

src/uu/ls/src/display.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ use uucore::{
5656
os_str_as_bytes_lossy,
5757
quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name},
5858
show,
59-
time::{FormatSystemTimeFallback, format_system_time},
59+
time::{FormatSystemTimeFallback, NamePadding, format_system_time_locale_aware},
6060
};
6161

6262
use crate::colors::{StyleManager, color_name};
@@ -620,7 +620,13 @@ fn display_date(
620620
_ => &config.time_format_recent,
621621
};
622622

623-
format_system_time(out, time, fmt, FormatSystemTimeFallback::Integer)
623+
format_system_time_locale_aware(
624+
out,
625+
time,
626+
fmt,
627+
FormatSystemTimeFallback::Integer,
628+
NamePadding::Padded,
629+
)
624630
}
625631

626632
fn display_len_or_rdev(metadata: &Metadata, config: &Config) -> SizeOrDeviceId {

src/uucore/src/lib/features/time.rs

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@ use std::time::{SystemTime, UNIX_EPOCH};
1616
use crate::error::{UResult, USimpleError};
1717
use crate::show_error;
1818

19+
#[cfg(feature = "i18n-datetime")]
20+
pub use crate::i18n::datetime::NamePadding;
21+
22+
/// Controls whether locale name lookups return raw or padded names.
23+
///
24+
/// Without the `i18n-datetime` feature the parameter is accepted but ignored.
25+
#[cfg(not(feature = "i18n-datetime"))]
26+
#[derive(Clone, Copy)]
27+
pub enum NamePadding {
28+
/// Raw names with no trailing padding — for `date` and similar utilities.
29+
Raw,
30+
/// Names padded to uniform display width — for columnar output like `ls`.
31+
Padded,
32+
}
33+
1934
/// Format the given date according to this time format style.
2035
fn format_zoned<W: Write>(out: &mut W, zoned: Zoned, fmt: &str) -> UResult<()> {
2136
let tm = BrokenDownTime::from(&zoned);
@@ -49,41 +64,79 @@ pub enum FormatSystemTimeFallback {
4964
Float, // Just print seconds+nanoseconds since epoch (`stat`)
5065
}
5166

67+
/// Write the seconds-since-epoch fallback used when a `SystemTime` is out of
68+
/// the range representable by `jiff::Zoned`.
69+
fn write_fallback_seconds<W: Write>(
70+
out: &mut W,
71+
time: SystemTime,
72+
mode: FormatSystemTimeFallback,
73+
) -> UResult<()> {
74+
// TODO: The range allowed by jiff is different from what GNU accepts,
75+
// but it still far enough in the future/past to be unlikely to matter:
76+
// jiff: Year between -9999 to 9999 (UTC) [-377705023201..=253402207200]
77+
// GNU: Year fits in signed 32 bits (timezone dependent)
78+
let (mut secs, mut nsecs) = system_time_to_sec(time);
79+
match mode {
80+
FormatSystemTimeFallback::Integer => out.write_all(secs.to_string().as_bytes())?,
81+
FormatSystemTimeFallback::IntegerError => {
82+
let str = secs.to_string();
83+
show_error!("time '{str}' is out of range");
84+
out.write_all(str.as_bytes())?;
85+
}
86+
FormatSystemTimeFallback::Float => {
87+
if secs < 0 && nsecs != 0 {
88+
secs -= 1;
89+
nsecs = 1_000_000_000 - nsecs;
90+
}
91+
out.write_fmt(format_args!("{secs}.{nsecs:09}"))?;
92+
}
93+
}
94+
Ok(())
95+
}
96+
5297
/// Format a `SystemTime` according to given fmt, and append to vector out.
5398
pub fn format_system_time<W: Write>(
5499
out: &mut W,
55100
time: SystemTime,
56101
fmt: &str,
57102
mode: FormatSystemTimeFallback,
58103
) -> UResult<()> {
59-
let zoned: Result<Zoned, _> = time.try_into();
60-
if let Ok(zoned) = zoned {
61-
format_zoned(out, zoned, fmt)
62-
} else {
104+
match time.try_into() {
105+
Ok(zoned) => format_zoned(out, zoned, fmt),
63106
// Assume that if we cannot build a Zoned element, the timestamp is
64107
// out of reasonable range, just print it then.
65-
// TODO: The range allowed by jiff is different from what GNU accepts,
66-
// but it still far enough in the future/past to be unlikely to matter:
67-
// jiff: Year between -9999 to 9999 (UTC) [-377705023201..=253402207200]
68-
// GNU: Year fits in signed 32 bits (timezone dependent)
69-
let (mut secs, mut nsecs) = system_time_to_sec(time);
70-
match mode {
71-
FormatSystemTimeFallback::Integer => out.write_all(secs.to_string().as_bytes())?,
72-
FormatSystemTimeFallback::IntegerError => {
73-
let str = secs.to_string();
74-
show_error!("time '{str}' is out of range");
75-
out.write_all(str.as_bytes())?;
76-
}
77-
FormatSystemTimeFallback::Float => {
78-
if secs < 0 && nsecs != 0 {
79-
secs -= 1;
80-
nsecs = 1_000_000_000 - nsecs;
81-
}
82-
out.write_fmt(format_args!("{secs}.{nsecs:09}"))?;
108+
Err(_) => write_fallback_seconds(out, time, mode),
109+
}
110+
}
111+
112+
/// Like [`format_system_time`], but when built with the `i18n-datetime`
113+
/// feature and a non-C `LC_TIME` locale is active, rewrites locale-dependent
114+
/// strftime directives (`%b`, `%B`, `%a`, `%A`, and `%Y`/`%m`/`%d`/`%e` for
115+
/// non-Gregorian calendars) to their localized values before formatting.
116+
/// For Gregorian locales, `%Y`/`%m`/`%d`/`%e` are unaffected (e.g. `en_US`
117+
/// still renders 2025 as `2025`).
118+
///
119+
/// With the feature disabled or a C/POSIX locale, this is identical to
120+
/// `format_system_time`.
121+
pub fn format_system_time_locale_aware<W: Write>(
122+
out: &mut W,
123+
time: SystemTime,
124+
fmt: &str,
125+
mode: FormatSystemTimeFallback,
126+
padding: NamePadding,
127+
) -> UResult<()> {
128+
#[cfg(feature = "i18n-datetime")]
129+
{
130+
use crate::i18n::datetime::{localize_format_string, should_use_icu_locale};
131+
if should_use_icu_locale() {
132+
if let Ok(zoned) = <SystemTime as TryInto<Zoned>>::try_into(time) {
133+
let localized = localize_format_string(fmt, zoned.date());
134+
return format_zoned(out, zoned, &localized);
83135
}
136+
// Out-of-range: fall through to the plain fallback below.
84137
}
85-
Ok(())
86138
}
139+
format_system_time(out, time, fmt, mode)
87140
}
88141

89142
#[cfg(test)]

0 commit comments

Comments
 (0)