1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-27 19:17:43 +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

@ -653,10 +653,6 @@ jobs:
;;
esac
outputs CARGO_TEST_OPTIONS
# ** pass needed environment into `cross` container (iff `cross` not already configured via "Cross.toml")
if [ "${CARGO_CMD}" = 'cross' ] && [ ! -e "Cross.toml" ] ; then
printf "[build.env]\npassthrough = [\"CI\", \"RUST_BACKTRACE\", \"CARGO_TERM_COLOR\"]\n" > Cross.toml
fi
# * executable for `strip`?
STRIP="strip"
case ${{ matrix.job.target }} in

84
Cargo.lock generated
View file

@ -314,27 +314,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "chrono-tz"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3"
dependencies = [
"chrono",
"chrono-tz-build",
"phf",
]
[[package]]
name = "chrono-tz-build"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7"
dependencies = [
"parse-zoneinfo",
"phf_codegen",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
@ -1337,6 +1316,47 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27e77966151130221b079bcec80f1f34a9e414fa489d99152a201c07fd2182bc"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.59.0",
]
[[package]]
name = "jiff-static"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97265751f8a9a4228476f2fc17874a9e7e70e96b893368e42619880fe143b48a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "jiff-tzdb"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
[[package]]
name = "jiff-tzdb-platform"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
dependencies = [
"jiff-tzdb",
]
[[package]]
name = "js-sys"
version = "0.3.77"
@ -1747,15 +1767,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "parse-zoneinfo"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
dependencies = [
"regex",
]
[[package]]
name = "parse_datetime"
version = "0.9.0"
@ -1839,6 +1850,15 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -2808,6 +2828,7 @@ version = "0.1.0"
dependencies = [
"chrono",
"clap",
"jiff",
"libc",
"parse_datetime",
"uucore",
@ -3079,6 +3100,7 @@ dependencies = [
"clap",
"glob",
"hostname",
"jiff",
"lscolors",
"number_prefix",
"selinux",
@ -3675,7 +3697,6 @@ dependencies = [
"blake2b_simd",
"blake3",
"chrono",
"chrono-tz",
"clap",
"crc32fast",
"data-encoding",
@ -3687,7 +3708,6 @@ dependencies = [
"fluent-bundle",
"glob",
"hex",
"iana-time-zone",
"itertools 0.14.0",
"libc",
"md-5",

View file

@ -282,7 +282,6 @@ chrono = { version = "0.4.41", default-features = false, features = [
"alloc",
"clock",
] }
chrono-tz = "0.10.0"
clap = { version = "4.5", features = ["wrap_help", "cargo"] }
clap_complete = "4.4"
clap_mangen = "0.2"
@ -300,9 +299,13 @@ gcd = "2.3"
glob = "0.3.1"
half = "2.4.1"
hostname = "0.4"
iana-time-zone = "0.1.57"
indicatif = "0.17.8"
itertools = "0.14.0"
jiff = { version = "0.2.10", default-features = false, features = [
"std",
"alloc",
"tz-system",
] }
libc = "0.2.172"
linux-raw-sys = "0.9"
lscolors = { version = "0.20.0", default-features = false, features = [

7
Cross.toml Normal file
View file

@ -0,0 +1,7 @@
# spell-checker:ignore (misc) dpkg noninteractive tzdata
[build]
pre-build = [
"apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install tzdata",
]
[build.env]
passthrough = ["CI", "RUST_BACKTRACE", "CARGO_TERM_COLOR"]

159
fuzz/Cargo.lock generated
View file

@ -219,27 +219,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "chrono-tz"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3"
dependencies = [
"chrono",
"chrono-tz-build",
"phf",
]
[[package]]
name = "chrono-tz-build"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402"
dependencies = [
"parse-zoneinfo",
"phf_codegen",
]
[[package]]
name = "clap"
version = "4.5.38"
@ -644,6 +623,47 @@ dependencies = [
"either",
]
[[package]]
name = "jiff"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27e77966151130221b079bcec80f1f34a9e414fa489d99152a201c07fd2182bc"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys",
]
[[package]]
name = "jiff-static"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97265751f8a9a4228476f2fc17874a9e7e70e96b893368e42619880fe143b48a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "jiff-tzdb"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
[[package]]
name = "jiff-tzdb-platform"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
dependencies = [
"jiff-tzdb",
]
[[package]]
name = "jobserver"
version = "0.1.33"
@ -831,15 +851,6 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "parse-zoneinfo"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
dependencies = [
"regex",
]
[[package]]
name = "parse_datetime"
version = "0.9.0"
@ -851,50 +862,27 @@ dependencies = [
"regex",
]
[[package]]
name = "phf"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"rand 0.8.5",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
]
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@ -928,15 +916,6 @@ version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.1"
@ -944,7 +923,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha",
"rand_core 0.9.3",
"rand_core",
]
[[package]]
@ -954,15 +933,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]]
name = "rand_core"
version = "0.9.3"
@ -1133,12 +1106,6 @@ version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "sm3"
version = "0.4.2"
@ -1316,6 +1283,7 @@ version = "0.1.0"
dependencies = [
"chrono",
"clap",
"jiff",
"libc",
"parse_datetime",
"uucore",
@ -1385,7 +1353,7 @@ dependencies = [
"itertools",
"memchr",
"nix",
"rand 0.9.1",
"rand",
"rayon",
"self_cell",
"tempfile",
@ -1442,8 +1410,6 @@ dependencies = [
"bigdecimal",
"blake2b_simd",
"blake3",
"chrono",
"chrono-tz",
"clap",
"crc32fast",
"data-encoding",
@ -1454,7 +1420,6 @@ dependencies = [
"fluent-bundle",
"glob",
"hex",
"iana-time-zone",
"itertools",
"libc",
"md-5",
@ -1481,7 +1446,7 @@ name = "uucore-fuzz"
version = "0.0.0"
dependencies = [
"libfuzzer-sys",
"rand 0.9.1",
"rand",
"uu_cksum",
"uu_cut",
"uu_date",
@ -1514,7 +1479,7 @@ version = "0.1.0"
dependencies = [
"console",
"libc",
"rand 0.9.1",
"rand",
"similar",
"tempfile",
"uucore",

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")]

View file

@ -2,6 +2,8 @@
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//
// spell-checker: ignore: AEDT AEST EEST NZDT NZST
use chrono::{DateTime, Datelike, Duration, NaiveTime, Utc}; // spell-checker:disable-line
use regex::Regex;
@ -564,11 +566,153 @@ fn test_date_from_stdin() {
);
}
const JAN2: &str = "2024-01-02 12:00:00 +0000";
const JUL2: &str = "2024-07-02 12:00:00 +0000";
#[test]
fn test_date_empty_tz() {
fn test_date_tz() {
fn test_tz(tz: &str, date: &str, output: &str) {
println!("Test with TZ={tz}, date=\"{date}\".");
new_ucmd!()
.env("TZ", tz)
.arg("-d")
.arg(date)
.arg("+%Y-%m-%d %H:%M:%S %Z")
.succeeds()
.stdout_only(output);
}
// Empty TZ, UTC0, invalid timezone.
test_tz("", JAN2, "2024-01-02 12:00:00 UTC\n");
test_tz("UTC0", JAN2, "2024-01-02 12:00:00 UTC\n");
// TODO: We do not handle invalid timezones the same way as GNU coreutils
//test_tz("Invalid/Timezone", JAN2, "2024-01-02 12:00:00 Invalid\n");
// Test various locations, some of them use daylight saving, some don't.
test_tz("America/Vancouver", JAN2, "2024-01-02 04:00:00 PST\n");
test_tz("America/Vancouver", JUL2, "2024-07-02 05:00:00 PDT\n");
test_tz("Europe/Berlin", JAN2, "2024-01-02 13:00:00 CET\n");
test_tz("Europe/Berlin", JUL2, "2024-07-02 14:00:00 CEST\n");
test_tz("Africa/Cairo", JAN2, "2024-01-02 14:00:00 EET\n");
// Egypt restored daylight saving in 2023, so if the database is outdated, this will fail.
//test_tz("Africa/Cairo", JUL2, "2024-07-02 15:00:00 EEST\n");
test_tz("Asia/Tokyo", JAN2, "2024-01-02 21:00:00 JST\n");
test_tz("Asia/Tokyo", JUL2, "2024-07-02 21:00:00 JST\n");
test_tz("Australia/Sydney", JAN2, "2024-01-02 23:00:00 AEDT\n");
test_tz("Australia/Sydney", JUL2, "2024-07-02 22:00:00 AEST\n"); // Shifts the other way.
test_tz("Pacific/Tahiti", JAN2, "2024-01-02 02:00:00 -10\n"); // No abbreviation.
test_tz("Antarctica/South_Pole", JAN2, "2024-01-03 01:00:00 NZDT\n");
test_tz("Antarctica/South_Pole", JUL2, "2024-07-03 00:00:00 NZST\n");
}
#[test]
fn test_date_tz_with_utc_flag() {
new_ucmd!()
.env("TZ", "")
.env("TZ", "Europe/Berlin")
.arg("-u")
.arg("+%Z")
.succeeds()
.stdout_only("UTC\n");
}
#[test]
fn test_date_tz_various_formats() {
fn test_tz(tz: &str, date: &str, output: &str) {
println!("Test with TZ={tz}, date=\"{date}\".");
new_ucmd!()
.env("TZ", tz)
.arg("-d")
.arg(date)
.arg("+%z %:z %::z %:::z %Z")
.succeeds()
.stdout_only(output);
}
test_tz(
"America/Vancouver",
JAN2,
"-0800 -08:00 -08:00:00 -08 PST\n",
);
// Half-hour timezone
test_tz("Asia/Calcutta", JAN2, "+0530 +05:30 +05:30:00 +05:30 IST\n");
test_tz("Europe/Berlin", JAN2, "+0100 +01:00 +01:00:00 +01 CET\n");
test_tz(
"Australia/Sydney",
JAN2,
"+1100 +11:00 +11:00:00 +11 AEDT\n",
);
}
#[test]
fn test_date_tz_with_relative_time() {
new_ucmd!()
.env("TZ", "America/Vancouver")
.arg("-d")
.arg("1 hour ago")
.arg("+%Y-%m-%d %H:%M:%S %Z")
.succeeds()
.stdout_matches(&Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} P[DS]T\n$").unwrap());
}
#[test]
fn test_date_utc_time() {
// Test that -u flag shows correct UTC time
// We get 2 UTC times just in case we're really unlucky and this runs around
// an hour change.
let utc_hour_1: i32 = new_ucmd!()
.env("TZ", "Asia/Taipei")
.arg("-u")
.arg("+%-H")
.succeeds()
.stdout_str()
.trim_end()
.parse()
.unwrap();
let tpe_hour: i32 = new_ucmd!()
.env("TZ", "Asia/Taipei")
.arg("+%-H")
.succeeds()
.stdout_str()
.trim_end()
.parse()
.unwrap();
let utc_hour_2: i32 = new_ucmd!()
.env("TZ", "Asia/Taipei")
.arg("-u")
.arg("+%-H")
.succeeds()
.stdout_str()
.trim_end()
.parse()
.unwrap();
// Taipei is always 8 hours ahead of UTC (no daylight savings)
assert!(
(tpe_hour - utc_hour_1 + 24) % 24 == 8 || (tpe_hour - utc_hour_2 + 24) % 24 == 8,
"TPE: {tpe_hour} UTC: {utc_hour_1}/{utc_hour_2}"
);
// Test that -u flag shows UTC timezone
new_ucmd!()
.arg("-u")
.arg("+%Z")
.succeeds()
.stdout_only("UTC\n");
// Test that -u flag with specific timestamp shows correct UTC time
new_ucmd!()
.arg("-u")
.arg("-d")
.arg("@0")
.succeeds()
.stdout_only("Thu Jan 1 00:00:00 UTC 1970\n");
}
#[test]
fn test_date_empty_tz_time() {
new_ucmd!()
.env("TZ", "")
.arg("-d")
.arg("@0")
.succeeds()
.stdout_only("Thu Jan 1 00:00:00 UTC 1970\n");
}