diff --git a/Cargo.lock b/Cargo.lock index fe230f9db57..477796ecef9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,6 +291,16 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "calendrical_calculations" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7" +dependencies = [ + "core_maths", + "displaydoc", +] + [[package]] name = "cc" version = "1.2.27" @@ -532,6 +542,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "coreutils" version = "0.4.0" @@ -1317,6 +1336,30 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_calendar" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "362941891d17750e05cd8fdfca4a89a86552825d625a937020ee1a65580da1f9" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar_data", + "icu_locale", + "icu_locale_core", + "icu_provider", + "ixdtf", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_calendar_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219c8639ab936713a87b571eed2bc2615aa9137e8af6eb221446ee5644acc18" + [[package]] name = "icu_collator" version = "2.0.0" @@ -1356,6 +1399,37 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_datetime" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea7ff7a9d7ce8f419930ec75b9821807cdbde7ec5f0157c322773a6ede6125f" +dependencies = [ + "displaydoc", + "either", + "fixed_decimal", + "icu_calendar", + "icu_datetime_data", + "icu_decimal", + "icu_locale", + "icu_locale_core", + "icu_pattern", + "icu_plurals", + "icu_provider", + "icu_time", + "potential_utf", + "smallvec", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_datetime_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83791ac10bb7b774f130bb81fa89c4059de710dcef53caa0b86e645212d6d54c" + [[package]] name = "icu_decimal" version = "2.0.0" @@ -1435,6 +1509,38 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +[[package]] +name = "icu_pattern" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "983825f401e6bc4a13c45d552ffd9ad6f3f6b6bc0ec03f31d6835a90a46deb1f" +dependencies = [ + "displaydoc", + "either", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_plurals" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd83a65f58b6f28e1f3da8c6ada6b415ee3ad5cb480b75bdb669f34d72dd179" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_locale", + "icu_plurals_data", + "icu_provider", + "zerovec", +] + +[[package]] +name = "icu_plurals_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ec552d761eaf4a1c39ad28936e0af77a41bf01ff756ea54be4f8bfc21c265d7" + [[package]] name = "icu_properties" version = "2.0.1" @@ -1474,6 +1580,32 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_time" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49a81ac920c1a93e3165a5f16d16f5c15d9f19fc5dc1223c00af35f7a7c2bb6" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar", + "icu_locale_core", + "icu_provider", + "icu_time_data", + "ixdtf", + "serde", + "tinystr", + "writeable", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_time_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8472be4410d26a03d7208cae3a76c798dd6766e8226ab977cd8b2d349a6dbf08" + [[package]] name = "indexmap" version = "2.9.0" @@ -1566,6 +1698,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "ixdtf" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8289f7f711a1a51f80e2e368355d023042ca55d8d554fd5e953f01464c15842d" +dependencies = [ + "displaydoc", +] + [[package]] name = "jiff" version = "0.2.16" @@ -1581,6 +1722,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "jiff-icu" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e67c2beaae8b10a82d849b9aabb698a43a682f32b17bcdc035d5ecadb44d646" +dependencies = [ + "icu_calendar", + "icu_time", + "jiff", +] + [[package]] name = "jiff-static" version = "0.2.16" @@ -3192,7 +3344,12 @@ version = "0.4.0" dependencies = [ "clap", "fluent", + "icu_calendar", + "icu_datetime", + "icu_locale", + "icu_provider", "jiff", + "jiff-icu", "libc", "parse_datetime", "uucore", @@ -4662,6 +4819,9 @@ name = "writeable" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +dependencies = [ + "either", +] [[package]] name = "wyz" diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 2d5f53d4b81..f567d187603 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -26,8 +26,13 @@ jiff = { workspace = true, features = [ "tzdb-zoneinfo", "tzdb-concatenated", ] } +jiff-icu = "0.2.2" +icu_calendar = "2.0.0" +icu_datetime = "2.0.0" +icu_locale = { workspace = true } +icu_provider = { workspace = true } parse_datetime = { workspace = true } -uucore = { workspace = true, features = ["parser"] } +uucore = { workspace = true, features = ["parser", "i18n-common"] } [target.'cfg(unix)'.dependencies] libc = { workspace = true } diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 5321256007d..787e901dd98 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -6,14 +6,19 @@ // spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes getres AWST ACST AEST use clap::{Arg, ArgAction, Command}; +use icu_calendar::{AnyCalendar, Date as IcuDate}; +use icu_datetime::{fieldsets, DateTimeFormatter, input::DateTime as IcuDateTime}; +use icu_locale::Locale; use jiff::fmt::strtime; use jiff::tz::{TimeZone, TimeZoneDatabase}; use jiff::{Timestamp, Zoned}; +use jiff_icu::ConvertFrom as _; #[cfg(all(unix, not(target_os = "macos"), not(target_os = "redox")))] use libc::clock_settime; #[cfg(all(unix, not(target_os = "redox")))] use libc::{CLOCK_REALTIME, clock_getres, timespec}; use std::collections::HashMap; +use std::env; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; @@ -27,6 +32,102 @@ use windows_sys::Win32::{Foundation::SYSTEMTIME, System::SystemInformation::SetS use uucore::parser::shortcut_value_parser::ShortcutValueParser; +fn get_time_locale() -> Locale { + let locale_str = env::var("LC_ALL") + .or_else(|_| env::var("LC_TIME")) + .or_else(|_| env::var("LANG")) + .unwrap_or_else(|_| "C".to_string()); + + // Convert POSIX locale format (th_TH.UTF-8) to BCP47 (th-TH) + let bcp47 = locale_str + .split('.') + .next() + .unwrap_or(&locale_str) + .replace('_', "-"); + + bcp47.parse().unwrap_or_else(|_| "en-US".parse().unwrap()) +} + +fn get_calendar_for_locale(locale: &Locale) -> Option<&'static str> { + let lang = locale.id.language.as_str(); + let region = locale.id.region.as_ref().map(|r| r.as_str()); + + match (lang, region) { + ("th", Some("TH")) => Some("buddhist"), + ("fa", _) | (_, Some("IR")) => Some("persian"), + ("am", Some("ET")) => Some("ethiopic"), + _ => None, + } +} + +fn format_with_icu(zdt: &Zoned, format_str: &str) -> Option { + use icu_calendar::AnyCalendarKind; + + let base_locale = get_time_locale(); + let icu_dt = IcuDateTime::convert_from(zdt.datetime()); + + if let Some(cal_name) = get_calendar_for_locale(&base_locale) { + let kind = match cal_name { + "buddhist" => AnyCalendarKind::Buddhist, + "persian" => AnyCalendarKind::Persian, + "ethiopic" => AnyCalendarKind::Ethiopian, + _ => return None, + }; + + // Build locale string with calendar extension + let locale_str = format!("{}-u-ca-{}", base_locale, cal_name); + let locale: Locale = locale_str.parse().ok()?; + + let cal = AnyCalendar::new(kind); + let iso_date = IcuDate::try_new_iso(zdt.year() as i32, zdt.month() as u8, zdt.day() as u8).ok()?; + let any_date = iso_date.to_any().to_calendar(cal); + + match format_str { + "%c" => { + let any_dt = IcuDateTime { date: any_date, time: icu_dt.time }; + Some(DateTimeFormatter::try_new(locale.into(), fieldsets::YMDET::medium()).ok()?.format(&any_dt).to_string()) + } + "%x" => { + Some(DateTimeFormatter::try_new(locale.into(), fieldsets::YMD::medium()).ok()?.format(&any_date).to_string()) + } + "%X" => { + Some(DateTimeFormatter::try_new(locale.into(), fieldsets::ET::medium()).ok()?.format(&icu_dt).to_string()) + } + "%Y" => { + use icu_calendar::types::YearInfo; + let year_num: i32 = match any_date.year() { + YearInfo::Cyclic(y) => y.year as i32, + YearInfo::Era(y) => y.year, + _ => any_date.extended_year(), + }; + Some(year_num.to_string()) + } + "%B" => { + Some(DateTimeFormatter::try_new(locale.into(), fieldsets::M::long()).ok()?.format(&any_date).to_string()) + } + _ => None, + } + } else { + let locale = base_locale; + match format_str { + "%c" => Some(DateTimeFormatter::try_new( + locale.into(), + fieldsets::YMDET::medium(), + ).ok()?.format(&icu_dt).to_string()), + "%x" => Some(DateTimeFormatter::try_new( + locale.into(), + fieldsets::YMD::medium(), + ).ok()?.format(&icu_dt).to_string()), + "%X" => Some(DateTimeFormatter::try_new( + locale.into(), + fieldsets::ET::medium(), + ).ok()?.format(&icu_dt).to_string()), + "%Y" => Some(zdt.year().to_string()), + _ => None, + } + } +} + // Options const DATE: &str = "date"; const HOURS: &str = "hours"; @@ -389,16 +490,22 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Format all the dates for date in dates { match date { - // TODO: Switch to lenient formatting. - Ok(date) => match strtime::format(format_string, &date) { - Ok(s) => println!("{s}"), - Err(e) => { - return Err(USimpleError::new( - 1, - translate!("date-error-invalid-format", "format" => format_string, "error" => e), - )); - } - }, + Ok(date) => { + let output = if let Some(icu_output) = format_with_icu(&date, format_string) { + icu_output + } else { + match strtime::format(format_string, &date) { + Ok(s) => s, + Err(e) => { + return Err(USimpleError::new( + 1, + translate!("date-error-invalid-format", "format" => format_string, "error" => e), + )); + } + } + }; + println!("{}", output); + } Err((input, _err)) => show!(USimpleError::new( 1, translate!("date-error-invalid-date", "date" => input)