1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2026-01-18 11:11:10 +00:00

Merge pull request #7894 from drinkcat/jiff-date-ls

date/ls: Switch from chrono to jiff
This commit is contained in:
Daniel Hofstetter 2025-06-02 09:36:27 +02:00 committed by GitHub
commit dfc2e249ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 399 additions and 325 deletions

View file

@ -1,4 +1,4 @@
# spell-checker:ignore datetime
# spell-checker:ignore datetime tzdb zoneinfo
[package]
name = "uu_date"
description = "date ~ (uutils) display or set the current time"
@ -19,9 +19,14 @@ workspace = true
path = "src/date.rs"
[dependencies]
chrono = { workspace = true }
clap = { workspace = true }
uucore = { workspace = true, features = ["custom-tz-fmt", "parser"] }
chrono = { workspace = true } # TODO: Eventually we'll want to remove this
jiff = { workspace = true, features = [
"tzdb-bundle-platform",
"tzdb-zoneinfo",
"tzdb-concatenated",
] }
uucore = { workspace = true, features = ["parser"] }
parse_datetime = { workspace = true }
[target.'cfg(unix)'.dependencies]

View file

@ -3,19 +3,17 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (chrono) Datelike Timelike ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes
// spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes
use chrono::format::{Item, StrftimeItems};
use chrono::{DateTime, FixedOffset, Local, Offset, TimeDelta, Utc};
#[cfg(windows)]
use chrono::{Datelike, Timelike};
use clap::{Arg, ArgAction, Command};
use jiff::fmt::strtime;
use jiff::tz::TimeZone;
use jiff::{SignedDuration, Timestamp, Zoned};
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "redox")))]
use libc::{CLOCK_REALTIME, clock_settime, timespec};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use uucore::custom_tz_fmt::custom_time_format;
use uucore::display::Quotable;
use uucore::error::FromIo;
use uucore::error::{UResult, USimpleError};
@ -75,7 +73,7 @@ struct Settings {
utc: bool,
format: Format,
date_source: DateSource,
set_to: Option<DateTime<FixedOffset>>,
set_to: Option<Zoned>,
}
/// Various ways of displaying the date
@ -93,7 +91,7 @@ enum DateSource {
Custom(String),
File(PathBuf),
Stdin,
Human(TimeDelta),
Human(SignedDuration),
}
enum Iso8601Format {
@ -167,9 +165,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
};
let date_source = if let Some(date) = matches.get_one::<String>(OPT_DATE) {
let ref_time = Local::now();
if let Ok(new_time) = parse_datetime::parse_datetime_at_date(ref_time, date.as_str()) {
let duration = new_time.signed_duration_since(ref_time);
if let Ok(duration) = parse_offset(date.as_str()) {
DateSource::Human(duration)
} else {
DateSource::Custom(date.into())
@ -203,39 +199,37 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
if let Some(date) = settings.set_to {
// All set time functions expect UTC datetimes.
let date: DateTime<Utc> = if settings.utc {
date.with_timezone(&Utc)
let date = if settings.utc {
date.with_time_zone(TimeZone::UTC)
} else {
date.into()
date
};
return set_system_datetime(date);
} else {
// Get the current time, either in the local time zone or UTC.
let now: DateTime<FixedOffset> = if settings.utc {
let now = Utc::now();
now.with_timezone(&now.offset().fix())
let now = if settings.utc {
Timestamp::now().to_zoned(TimeZone::UTC)
} else {
let now = Local::now();
now.with_timezone(now.offset())
Zoned::now()
};
// Iterate over all dates - whether it's a single date or a file.
let dates: Box<dyn Iterator<Item = _>> = match settings.date_source {
DateSource::Custom(ref input) => {
let date = parse_date(input.clone());
let date = parse_date(input);
let iter = std::iter::once(date);
Box::new(iter)
}
DateSource::Human(relative_time) => {
// Double check the result is overflow or not of the current_time + relative_time
// it may cause a panic of chrono::datetime::DateTime add
match now.checked_add_signed(relative_time) {
Some(date) => {
match now.checked_add(relative_time) {
Ok(date) => {
let iter = std::iter::once(Ok(date));
Box::new(iter)
}
None => {
Err(_) => {
return Err(USimpleError::new(
1,
format!("invalid date {relative_time}"),
@ -272,23 +266,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
// Format all the dates
for date in dates {
match date {
Ok(date) => {
let format_string = custom_time_format(format_string);
// Hack to work around panic in chrono,
// TODO - remove when a fix for https://github.com/chronotope/chrono/issues/623 is released
let format_items = StrftimeItems::new(format_string.as_str());
if format_items.clone().any(|i| i == Item::Error) {
// TODO: Switch to lenient formatting.
Ok(date) => match strtime::format(format_string, &date) {
Ok(s) => println!("{s}"),
Err(e) => {
return Err(USimpleError::new(
1,
format!("invalid format {}", format_string.replace("%f", "%N")),
format!("invalid format {} ({e})", format_string),
));
}
let formatted = date
.format_with_items(format_items)
.to_string()
.replace("%f", "%N");
println!("{formatted}");
}
},
Err((input, _err)) => show!(USimpleError::new(
1,
format!("invalid date {}", input.quote())
@ -388,13 +375,13 @@ fn make_format_string(settings: &Settings) -> &str {
Iso8601Format::Hours => "%FT%H%:z",
Iso8601Format::Minutes => "%FT%H:%M%:z",
Iso8601Format::Seconds => "%FT%T%:z",
Iso8601Format::Ns => "%FT%T,%f%:z",
Iso8601Format::Ns => "%FT%T,%N%:z",
},
Format::Rfc5322 => "%a, %d %h %Y %T %z",
Format::Rfc3339(ref fmt) => match *fmt {
Rfc3339Format::Date => "%F",
Rfc3339Format::Seconds => "%F %T%:z",
Rfc3339Format::Ns => "%F %T.%f%:z",
Rfc3339Format::Ns => "%F %T.%N%:z",
},
Format::Custom(ref fmt) => fmt,
Format::Default => "%a %b %e %X %Z %Y",
@ -403,19 +390,43 @@ fn make_format_string(settings: &Settings) -> &str {
/// Parse a `String` into a `DateTime`.
/// If it fails, return a tuple of the `String` along with its `ParseError`.
// TODO: Convert `parse_datetime` to jiff and remove wrapper from chrono to jiff structures.
fn parse_date<S: AsRef<str> + Clone>(
s: S,
) -> Result<DateTime<FixedOffset>, (String, parse_datetime::ParseDateTimeError)> {
parse_datetime::parse_datetime(s.as_ref()).map_err(|e| (s.as_ref().into(), e))
) -> Result<Zoned, (String, parse_datetime::ParseDateTimeError)> {
match parse_datetime::parse_datetime(s.as_ref()) {
Ok(date) => {
let timestamp =
Timestamp::new(date.timestamp(), date.timestamp_subsec_nanos() as i32).unwrap();
Ok(Zoned::new(timestamp, TimeZone::UTC))
}
Err(e) => Err((s.as_ref().into(), e)),
}
}
// TODO: Convert `parse_datetime` to jiff and remove wrapper from chrono to jiff structures.
// Also, consider whether parse_datetime::parse_datetime_at_date can be renamed to something
// like parse_datetime::parse_offset, instead of doing some addition/subtraction.
fn parse_offset(date: &str) -> Result<SignedDuration, ()> {
let ref_time = chrono::Local::now();
if let Ok(new_time) = parse_datetime::parse_datetime_at_date(ref_time, date) {
let duration = new_time.signed_duration_since(ref_time);
Ok(SignedDuration::new(
duration.num_seconds(),
duration.subsec_nanos(),
))
} else {
Err(())
}
}
#[cfg(not(any(unix, windows)))]
fn set_system_datetime(_date: DateTime<Utc>) -> UResult<()> {
fn set_system_datetime(_date: Zoned) -> UResult<()> {
unimplemented!("setting date not implemented (unsupported target)");
}
#[cfg(target_os = "macos")]
fn set_system_datetime(_date: DateTime<Utc>) -> UResult<()> {
fn set_system_datetime(_date: Zoned) -> UResult<()> {
Err(USimpleError::new(
1,
"setting the date is not supported by macOS".to_string(),
@ -423,7 +434,7 @@ fn set_system_datetime(_date: DateTime<Utc>) -> UResult<()> {
}
#[cfg(target_os = "redox")]
fn set_system_datetime(_date: DateTime<Utc>) -> UResult<()> {
fn set_system_datetime(_date: Zoned) -> UResult<()> {
Err(USimpleError::new(
1,
"setting the date is not supported by Redox".to_string(),
@ -436,10 +447,11 @@ fn set_system_datetime(_date: DateTime<Utc>) -> UResult<()> {
/// `<https://doc.rust-lang.org/libc/i686-unknown-linux-gnu/libc/fn.clock_settime.html>`
/// `<https://linux.die.net/man/3/clock_settime>`
/// `<https://www.gnu.org/software/libc/manual/html_node/Time-Types.html>`
fn set_system_datetime(date: DateTime<Utc>) -> UResult<()> {
fn set_system_datetime(date: Zoned) -> UResult<()> {
let ts = date.timestamp();
let timespec = timespec {
tv_sec: date.timestamp() as _,
tv_nsec: date.timestamp_subsec_nanos() as _,
tv_sec: ts.as_second() as _,
tv_nsec: ts.subsec_nanosecond() as _,
};
let result = unsafe { clock_settime(CLOCK_REALTIME, &timespec) };
@ -456,7 +468,7 @@ fn set_system_datetime(date: DateTime<Utc>) -> UResult<()> {
/// See here for more:
/// https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-setsystemtime
/// https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime
fn set_system_datetime(date: DateTime<Utc>) -> UResult<()> {
fn set_system_datetime(date: Zoned) -> UResult<()> {
let system_time = SYSTEMTIME {
wYear: date.year() as u16,
wMonth: date.month() as u16,
@ -467,7 +479,7 @@ fn set_system_datetime(date: DateTime<Utc>) -> UResult<()> {
wMinute: date.minute() as u16,
wSecond: date.second() as u16,
// TODO: be careful of leap seconds - valid range is [0, 999] - how to handle?
wMilliseconds: ((date.nanosecond() / 1_000_000) % 1000) as u16,
wMilliseconds: ((date.subsec_nanosecond() / 1_000_000) % 1000) as u16,
};
let result = unsafe { SetSystemTime(&system_time) };

View file

@ -1,3 +1,5 @@
# spell-checker:ignore tzdb zoneinfo
[package]
name = "uu_ls"
description = "ls ~ (uutils) display directory contents"
@ -23,6 +25,11 @@ chrono = { workspace = true }
clap = { workspace = true, features = ["env"] }
glob = { workspace = true }
hostname = { workspace = true }
jiff = { workspace = true, features = [
"tzdb-bundle-platform",
"tzdb-zoneinfo",
"tzdb-concatenated",
] }
lscolors = { workspace = true }
number_prefix = { workspace = true }
selinux = { workspace = true, optional = true }
@ -30,7 +37,6 @@ terminal_size = { workspace = true }
thiserror = { workspace = true }
uucore = { workspace = true, features = [
"colors",
"custom-tz-fmt",
"entries",
"format",
"fs",

View file

@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly nohash
// spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly nohash strtime
use std::iter;
#[cfg(windows)]
@ -16,24 +16,24 @@ use std::{
fs::{self, DirEntry, FileType, Metadata, ReadDir},
io::{BufWriter, ErrorKind, Stdout, Write, stdout},
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
time::{Duration, SystemTime, UNIX_EPOCH},
};
#[cfg(unix)]
use std::{
collections::HashMap,
os::unix::fs::{FileTypeExt, MetadataExt},
time::Duration,
};
use std::{collections::HashSet, io::IsTerminal};
use ansi_width::ansi_width;
use chrono::format::{Item, StrftimeItems};
use chrono::{DateTime, Local, TimeDelta};
use clap::{
Arg, ArgAction, Command,
builder::{NonEmptyStringValueParser, PossibleValue, ValueParser},
};
use glob::{MatchOptions, Pattern};
use jiff::fmt::StdIoWrite;
use jiff::fmt::strtime::BrokenDownTime;
use jiff::{Timestamp, Zoned};
use lscolors::LsColors;
use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions, SPACES_IN_TAB};
use thiserror::Error;
@ -59,7 +59,6 @@ use uucore::libc::{dev_t, major, minor};
use uucore::line_ending::LineEnding;
use uucore::quoting_style::{self, QuotingStyle, escape_name};
use uucore::{
custom_tz_fmt,
display::Quotable,
error::{UError, UResult, set_exit_code},
format_usage,
@ -274,64 +273,37 @@ enum TimeStyle {
Format(String),
}
/// A struct/impl used to format a file DateTime, precomputing the format for performance reasons.
struct TimeStyler {
// default format, always specified.
default: Vec<Item<'static>>,
// format for a recent time, only specified it is is different from the default
recent: Option<Vec<Item<'static>>>,
// If `recent` is set, cache the threshold time when we switch from recent to default format.
recent_time_threshold: Option<DateTime<Local>>,
/// Whether the given date is considered recent (i.e., in the last 6 months).
fn is_recent(time: Timestamp, state: &mut ListState) -> bool {
// According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average.
time > state.recent_time_threshold
}
impl TimeStyler {
/// Create a TimeStyler based on a TimeStyle specification.
fn new(style: &TimeStyle) -> TimeStyler {
let default: Vec<Item<'static>> = match style {
TimeStyle::FullIso => StrftimeItems::new("%Y-%m-%d %H:%M:%S.%f %z").parse(),
TimeStyle::LongIso => StrftimeItems::new("%Y-%m-%d %H:%M").parse(),
TimeStyle::Iso => StrftimeItems::new("%Y-%m-%d ").parse(),
// In this version of chrono translating can be done
// The function is chrono::datetime::DateTime::format_localized
// However it's currently still hard to get the current pure-rust-locale
// So it's not yet implemented
TimeStyle::Locale => StrftimeItems::new("%b %e %Y").parse(),
TimeStyle::Format(fmt) => {
StrftimeItems::new_lenient(custom_tz_fmt::custom_time_format(fmt).as_str())
.parse_to_owned()
}
}
.unwrap();
let recent = match style {
TimeStyle::Iso => Some(StrftimeItems::new("%m-%d %H:%M")),
// See comment above about locale
TimeStyle::Locale => Some(StrftimeItems::new("%b %e %H:%M")),
_ => None,
}
.map(|x| x.collect());
let recent_time_threshold = if recent.is_some() {
// According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average.
Some(Local::now() - TimeDelta::try_seconds(31_556_952 / 2).unwrap())
} else {
None
impl TimeStyle {
/// Format the given time according to this time format style.
fn format(
&self,
date: Zoned,
out: &mut Vec<u8>,
state: &mut ListState,
) -> Result<(), jiff::Error> {
let recent = is_recent(date.timestamp(), state);
let tm = BrokenDownTime::from(&date);
let mut out = StdIoWrite(out);
let config = jiff::fmt::strtime::Config::new().lenient(true);
let fmt = match (self, recent) {
(Self::FullIso, _) => "%Y-%m-%d %H:%M:%S.%f %z",
(Self::LongIso, _) => "%Y-%m-%d %H:%M",
(Self::Iso, true) => "%m-%d %H:%M",
(Self::Iso, false) => "%Y-%m-%d ",
// TODO: Using correct locale string is not implemented.
(Self::Locale, true) => "%b %e %H:%M",
(Self::Locale, false) => "%b %e %Y",
(Self::Format(fmt), _) => fmt,
};
TimeStyler {
default,
recent,
recent_time_threshold,
}
}
/// Format a DateTime, using `recent` format if available, and the DateTime
/// is recent enough.
fn format(&self, time: DateTime<Local>) -> String {
if self.recent.is_none() || time <= self.recent_time_threshold.unwrap() {
time.format_with_items(self.default.iter())
} else {
time.format_with_items(self.recent.as_ref().unwrap().iter())
}
.to_string()
tm.format_with_config(&config, fmt, &mut out)
}
}
@ -2093,8 +2065,7 @@ struct ListState<'a> {
uid_cache: HashMap<u32, String>,
#[cfg(unix)]
gid_cache: HashMap<u32, String>,
time_styler: TimeStyler,
recent_time_threshold: Timestamp,
}
#[allow(clippy::cognitive_complexity)]
@ -2111,7 +2082,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
uid_cache: HashMap::new(),
#[cfg(unix)]
gid_cache: HashMap::new(),
time_styler: TimeStyler::new(&config.time_style),
recent_time_threshold: Timestamp::now() - Duration::new(31_556_952 / 2, 0),
};
for loc in locs {
@ -2907,7 +2878,7 @@ fn display_item_long(
};
output_display.extend(b" ");
output_display.extend(display_date(md, config, state).as_bytes());
display_date(md, config, state, &mut output_display)?;
output_display.extend(b" ");
let item_name = display_item_name(
@ -3106,15 +3077,27 @@ fn get_system_time(md: &Metadata, config: &Config) -> Option<SystemTime> {
}
}
fn get_time(md: &Metadata, config: &Config) -> Option<DateTime<Local>> {
fn get_time(md: &Metadata, config: &Config) -> Option<Zoned> {
let time = get_system_time(md, config)?;
Some(time.into())
time.try_into().ok()
}
fn display_date(metadata: &Metadata, config: &Config, state: &mut ListState) -> String {
fn display_date(
metadata: &Metadata,
config: &Config,
state: &mut ListState,
out: &mut Vec<u8>,
) -> UResult<()> {
match get_time(metadata, config) {
Some(time) => state.time_styler.format(time),
None => "???".into(),
// TODO: Some fancier error conversion might be nice.
Some(time) => config
.time_style
.format(time, out, state)
.map_err(|x| USimpleError::new(1, x.to_string())),
None => {
out.extend(b"???");
Ok(())
}
}
}

View file

@ -21,7 +21,6 @@ path = "src/lib/lib.rs"
[dependencies]
chrono = { workspace = true, optional = true }
chrono-tz = { workspace = true, optional = true }
clap = { workspace = true }
uucore_procs = { workspace = true }
number_prefix = { workspace = true }
@ -29,7 +28,6 @@ dns-lookup = { workspace = true, optional = true }
dunce = { version = "1.0.4", optional = true }
wild = "2.2.1"
glob = { workspace = true, optional = true }
iana-time-zone = { workspace = true, optional = true }
itertools = { workspace = true, optional = true }
time = { workspace = true, optional = true, features = [
"formatting",
@ -138,6 +136,5 @@ utf8 = []
utmpx = ["time", "time/macros", "libc", "dns-lookup"]
version-cmp = []
wide = []
custom-tz-fmt = ["chrono", "chrono-tz", "iana-time-zone"]
tty = []
uptime = ["chrono", "libc", "windows-sys", "utmpx", "utmp-classic"]

View file

@ -14,8 +14,6 @@ pub mod buf_copy;
pub mod checksum;
#[cfg(feature = "colors")]
pub mod colors;
#[cfg(feature = "custom-tz-fmt")]
pub mod custom_tz_fmt;
#[cfg(feature = "encoding")]
pub mod encoding;
#[cfg(feature = "extendedbigdecimal")]

View file

@ -1,60 +0,0 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use chrono::{TimeZone, Utc};
use chrono_tz::{OffsetName, Tz};
use iana_time_zone::get_timezone;
/// Get the alphabetic abbreviation of the current timezone.
///
/// For example, "UTC" or "CET" or "PDT"
fn timezone_abbreviation() -> String {
let tz = match std::env::var("TZ") {
// TODO Support other time zones...
Ok(s) if s == "UTC0" || s.is_empty() => Tz::Etc__UTC,
_ => match get_timezone() {
Ok(tz_str) => tz_str.parse().unwrap(),
Err(_) => Tz::Etc__UTC,
},
};
let offset = tz.offset_from_utc_date(&Utc::now().date_naive());
offset.abbreviation().unwrap_or("UTC").to_string()
}
/// Adapt the given string to be accepted by the chrono library crate.
///
/// # Arguments
///
/// fmt: the format of the string
///
/// # Return
///
/// A string that can be used as parameter of the chrono functions that use formats
pub fn custom_time_format(fmt: &str) -> String {
// TODO - Revisit when chrono 0.5 is released. https://github.com/chronotope/chrono/issues/970
// chrono crashes on %#z, but it's the same as %z anyway.
// GNU `date` uses `%N` for nano seconds, however the `chrono` crate uses `%f`.
fmt.replace("%#z", "%z")
.replace("%N", "%f")
.replace("%Z", timezone_abbreviation().as_ref())
}
#[cfg(test)]
mod tests {
use super::{custom_time_format, timezone_abbreviation};
#[test]
fn test_custom_time_format() {
assert_eq!(custom_time_format("%Y-%m-%d %H-%M-%S"), "%Y-%m-%d %H-%M-%S");
assert_eq!(custom_time_format("%d-%m-%Y %H-%M-%S"), "%d-%m-%Y %H-%M-%S");
assert_eq!(custom_time_format("%Y-%m-%d %H-%M-%S"), "%Y-%m-%d %H-%M-%S");
assert_eq!(
custom_time_format("%Y-%m-%d %H-%M-%S.%N"),
"%Y-%m-%d %H-%M-%S.%f"
);
assert_eq!(custom_time_format("%Z"), timezone_abbreviation());
}
}

View file

@ -41,8 +41,6 @@ pub use crate::features::buf_copy;
pub use crate::features::checksum;
#[cfg(feature = "colors")]
pub use crate::features::colors;
#[cfg(feature = "custom-tz-fmt")]
pub use crate::features::custom_tz_fmt;
#[cfg(feature = "encoding")]
pub use crate::features::encoding;
#[cfg(feature = "extendedbigdecimal")]