Skip to content

Commit 41ac7e7

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 8ec8e96 commit 41ac7e7

5 files changed

Lines changed: 232 additions & 248 deletions

File tree

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: 2 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, format_system_time_locale_aware},
6060
};
6161

6262
use crate::colors::{StyleManager, color_name};
@@ -620,7 +620,7 @@ 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(out, time, fmt, FormatSystemTimeFallback::Integer)
624624
}
625625

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

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

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -49,41 +49,78 @@ pub enum FormatSystemTimeFallback {
4949
Float, // Just print seconds+nanoseconds since epoch (`stat`)
5050
}
5151

52+
/// Write the seconds-since-epoch fallback used when a `SystemTime` is out of
53+
/// the range representable by `jiff::Zoned`.
54+
fn write_fallback_seconds<W: Write>(
55+
out: &mut W,
56+
time: SystemTime,
57+
mode: FormatSystemTimeFallback,
58+
) -> UResult<()> {
59+
// TODO: The range allowed by jiff is different from what GNU accepts,
60+
// but it still far enough in the future/past to be unlikely to matter:
61+
// jiff: Year between -9999 to 9999 (UTC) [-377705023201..=253402207200]
62+
// GNU: Year fits in signed 32 bits (timezone dependent)
63+
let (mut secs, mut nsecs) = system_time_to_sec(time);
64+
match mode {
65+
FormatSystemTimeFallback::Integer => out.write_all(secs.to_string().as_bytes())?,
66+
FormatSystemTimeFallback::IntegerError => {
67+
let str = secs.to_string();
68+
show_error!("time '{str}' is out of range");
69+
out.write_all(str.as_bytes())?;
70+
}
71+
FormatSystemTimeFallback::Float => {
72+
if secs < 0 && nsecs != 0 {
73+
secs -= 1;
74+
nsecs = 1_000_000_000 - nsecs;
75+
}
76+
out.write_fmt(format_args!("{secs}.{nsecs:09}"))?;
77+
}
78+
}
79+
Ok(())
80+
}
81+
5282
/// Format a `SystemTime` according to given fmt, and append to vector out.
5383
pub fn format_system_time<W: Write>(
5484
out: &mut W,
5585
time: SystemTime,
5686
fmt: &str,
5787
mode: FormatSystemTimeFallback,
5888
) -> UResult<()> {
59-
let zoned: Result<Zoned, _> = time.try_into();
60-
if let Ok(zoned) = zoned {
61-
format_zoned(out, zoned, fmt)
62-
} else {
89+
match time.try_into() {
90+
Ok(zoned) => format_zoned(out, zoned, fmt),
6391
// Assume that if we cannot build a Zoned element, the timestamp is
6492
// 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}"))?;
93+
Err(_) => write_fallback_seconds(out, time, mode),
94+
}
95+
}
96+
97+
/// Like [`format_system_time`], but when built with the `i18n-datetime`
98+
/// feature and a non-C `LC_TIME` locale is active, rewrites locale-dependent
99+
/// strftime directives (`%b`, `%B`, `%a`, `%A`, and `%Y`/`%m`/`%d`/`%e` for
100+
/// non-Gregorian calendars) to their localized values before formatting.
101+
/// For Gregorian locales, `%Y`/`%m`/`%d`/`%e` are unaffected (e.g. `en_US`
102+
/// still renders 2025 as `2025`).
103+
///
104+
/// With the feature disabled or a C/POSIX locale, this is identical to
105+
/// `format_system_time`.
106+
pub fn format_system_time_locale_aware<W: Write>(
107+
out: &mut W,
108+
time: SystemTime,
109+
fmt: &str,
110+
mode: FormatSystemTimeFallback,
111+
) -> UResult<()> {
112+
#[cfg(feature = "i18n-datetime")]
113+
{
114+
use crate::i18n::datetime::{localize_format_string, should_use_icu_locale};
115+
if should_use_icu_locale() {
116+
if let Ok(zoned) = <SystemTime as TryInto<Zoned>>::try_into(time) {
117+
let localized = localize_format_string(fmt, zoned.date());
118+
return format_zoned(out, zoned, &localized);
83119
}
120+
// Out-of-range: fall through to the plain fallback below.
84121
}
85-
Ok(())
86122
}
123+
format_system_time(out, time, fmt, mode)
87124
}
88125

89126
#[cfg(test)]

0 commit comments

Comments
 (0)