mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
date: use jiff-icu for locale calendar conversions
This commit is contained in:
Generated
+24
-10
@@ -337,18 +337,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.55"
|
||||
version = "4.5.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785"
|
||||
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.55"
|
||||
version = "4.5.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61"
|
||||
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -1008,7 +1008,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1355,6 +1355,7 @@ dependencies = [
|
||||
"icu_locale_core",
|
||||
"icu_provider",
|
||||
"ixdtf",
|
||||
"serde",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
@@ -1720,7 +1721,18 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
"windows-sys 0.59.0",
|
||||
"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]]
|
||||
@@ -2003,7 +2015,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2593,7 +2605,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2903,7 +2915,7 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3378,6 +3390,7 @@ dependencies = [
|
||||
"icu_calendar",
|
||||
"icu_locale",
|
||||
"jiff",
|
||||
"jiff-icu",
|
||||
"nix",
|
||||
"parse_datetime",
|
||||
"tempfile",
|
||||
@@ -4456,6 +4469,7 @@ dependencies = [
|
||||
"icu_provider",
|
||||
"itertools 0.14.0",
|
||||
"jiff",
|
||||
"jiff-icu",
|
||||
"libc",
|
||||
"md-5",
|
||||
"memchr",
|
||||
@@ -4649,7 +4663,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -346,6 +346,7 @@ indicatif = "0.18.0"
|
||||
itertools = "0.14.0"
|
||||
itoa = "1.0.15"
|
||||
jiff = "0.2.18"
|
||||
jiff-icu = "0.2.2"
|
||||
libc = "0.2.172"
|
||||
lscolors = { version = "0.21.0", default-features = false, features = [
|
||||
"gnu_legacy",
|
||||
|
||||
@@ -20,13 +20,14 @@ path = "src/date.rs"
|
||||
|
||||
[features]
|
||||
default = ["i18n-datetime"]
|
||||
i18n-datetime = ["uucore/i18n-datetime", "dep:icu_calendar", "dep:icu_locale"]
|
||||
i18n-datetime = ["uucore/i18n-datetime", "dep:icu_calendar", "dep:icu_locale", "dep:jiff-icu"]
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
fluent = { workspace = true }
|
||||
icu_calendar = { workspace = true, optional = true }
|
||||
icu_locale = { workspace = true, optional = true }
|
||||
jiff-icu = { workspace = true, optional = true }
|
||||
jiff = { workspace = true, features = [
|
||||
"tzdb-bundle-platform",
|
||||
"tzdb-zoneinfo",
|
||||
|
||||
+6
-107
@@ -21,10 +21,7 @@ use uucore::display::Quotable;
|
||||
use uucore::error::FromIo;
|
||||
use uucore::error::{UResult, USimpleError};
|
||||
#[cfg(feature = "i18n-datetime")]
|
||||
use uucore::i18n::datetime::{
|
||||
get_era_year, get_localized_day_name, get_localized_month_name, get_time_locale,
|
||||
should_use_icu_locale,
|
||||
};
|
||||
use uucore::i18n::datetime::{localize_format_string, should_use_icu_locale};
|
||||
use uucore::translate;
|
||||
use uucore::{format_usage, show};
|
||||
#[cfg(windows)]
|
||||
@@ -620,112 +617,14 @@ fn format_date_with_locale_aware_months(
|
||||
format_string: &str,
|
||||
config: &Config<PosixCustom>,
|
||||
) -> Result<String, jiff::Error> {
|
||||
// Only use ICU for non-English locales and when format string contains month, day, or era year specifiers
|
||||
if (format_string.contains("%B")
|
||||
|| format_string.contains("%b")
|
||||
|| format_string.contains("%A")
|
||||
|| format_string.contains("%a")
|
||||
|| format_string.contains("%Y")
|
||||
|| format_string.contains("%Ey"))
|
||||
&& should_use_icu_locale()
|
||||
{
|
||||
let broken_down = BrokenDownTime::from(date);
|
||||
// Get localized month names if needed
|
||||
let (full_month, abbrev_month) =
|
||||
if format_string.contains("%B") || format_string.contains("%b") {
|
||||
if let Some(month_val) = broken_down.month() {
|
||||
let month_u8 = if (1..=12).contains(&month_val) {
|
||||
month_val as u8
|
||||
} else {
|
||||
1 // fallback to January for invalid values
|
||||
};
|
||||
(
|
||||
get_localized_month_name(month_u8, true),
|
||||
get_localized_month_name(month_u8, false),
|
||||
)
|
||||
} else {
|
||||
(String::new(), String::new())
|
||||
}
|
||||
} else {
|
||||
(String::new(), String::new())
|
||||
};
|
||||
let broken_down = BrokenDownTime::from(date);
|
||||
|
||||
// Get localized day names if needed
|
||||
let (full_day, abbrev_day) = if format_string.contains("%A") || format_string.contains("%a")
|
||||
{
|
||||
if let (Some(year), Some(month), Some(day)) =
|
||||
(broken_down.year(), broken_down.month(), broken_down.day())
|
||||
{
|
||||
(
|
||||
get_localized_day_name(year.into(), month as u8, day as u8, true),
|
||||
get_localized_day_name(year.into(), month as u8, day as u8, false),
|
||||
)
|
||||
} else {
|
||||
(String::new(), String::new())
|
||||
}
|
||||
} else {
|
||||
(String::new(), String::new())
|
||||
};
|
||||
|
||||
// Get era year if needed
|
||||
let era_year = if format_string.contains("%Y") || format_string.contains("%Ey") {
|
||||
if let (Some(year), Some(month), Some(day)) =
|
||||
(broken_down.year(), broken_down.month(), broken_down.day())
|
||||
{
|
||||
let (locale, _encoding) = get_time_locale();
|
||||
get_era_year(year.into(), month as u8, day as u8, locale)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Replace format specifiers with NULL-byte placeholders for successful ICU translations only
|
||||
// Use NULL bytes to avoid collision with user format strings
|
||||
let mut temp_format = format_string.to_string();
|
||||
if !full_month.is_empty() {
|
||||
temp_format = temp_format.replace("%B", "\0FULL_MONTH\0");
|
||||
}
|
||||
if !abbrev_month.is_empty() {
|
||||
temp_format = temp_format.replace("%b", "\0ABBREV_MONTH\0");
|
||||
}
|
||||
if !full_day.is_empty() {
|
||||
temp_format = temp_format.replace("%A", "\0FULL_DAY\0");
|
||||
}
|
||||
if !abbrev_day.is_empty() {
|
||||
temp_format = temp_format.replace("%a", "\0ABBREV_DAY\0");
|
||||
}
|
||||
if era_year.is_some() {
|
||||
temp_format = temp_format.replace("%Y", "\0ERA_YEAR\0");
|
||||
}
|
||||
|
||||
// Format with the temporary string
|
||||
let temp_result = broken_down.to_string_with_config(config, &temp_format)?;
|
||||
|
||||
// Replace NULL-byte placeholders with localized names
|
||||
let mut final_result = temp_result;
|
||||
if !full_month.is_empty() {
|
||||
final_result = final_result.replace("\0FULL_MONTH\0", &full_month);
|
||||
}
|
||||
if !abbrev_month.is_empty() {
|
||||
final_result = final_result.replace("\0ABBREV_MONTH\0", &abbrev_month);
|
||||
}
|
||||
if !full_day.is_empty() {
|
||||
final_result = final_result.replace("\0FULL_DAY\0", &full_day);
|
||||
}
|
||||
if !abbrev_day.is_empty() {
|
||||
final_result = final_result.replace("\0ABBREV_DAY\0", &abbrev_day);
|
||||
}
|
||||
if let Some(era_year_val) = era_year {
|
||||
final_result = final_result.replace("\0ERA_YEAR\0", &era_year_val.to_string());
|
||||
}
|
||||
|
||||
return Ok(final_result);
|
||||
if !should_use_icu_locale() {
|
||||
return broken_down.to_string_with_config(config, format_string);
|
||||
}
|
||||
|
||||
// Fallback to regular formatting
|
||||
BrokenDownTime::from(date).to_string_with_config(config, format_string)
|
||||
let fmt = localize_format_string(format_string, &date.date());
|
||||
broken_down.to_string_with_config(config, &fmt)
|
||||
}
|
||||
|
||||
/// Return the appropriate format string for the given settings.
|
||||
|
||||
@@ -82,6 +82,7 @@ icu_decimal = { workspace = true, optional = true, features = [
|
||||
] }
|
||||
icu_locale = { workspace = true, optional = true, features = ["compiled_data"] }
|
||||
icu_provider = { workspace = true, optional = true }
|
||||
jiff-icu = { workspace = true, optional = true }
|
||||
|
||||
# Fluent dependencies (always available for localization)
|
||||
fluent = { workspace = true }
|
||||
@@ -153,7 +154,7 @@ i18n-all = ["i18n-collator", "i18n-decimal", "i18n-datetime"]
|
||||
i18n-common = ["icu_locale"]
|
||||
i18n-collator = ["i18n-common", "icu_collator"]
|
||||
i18n-decimal = ["i18n-common", "icu_decimal", "icu_provider"]
|
||||
i18n-datetime = ["i18n-common", "icu_calendar", "icu_datetime"]
|
||||
i18n-datetime = ["i18n-common", "icu_calendar", "icu_datetime", "jiff-icu", "jiff"]
|
||||
mode = ["libc"]
|
||||
perms = ["entries", "libc", "walkdir"]
|
||||
buf-copy = []
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
//! Locale-aware datetime formatting utilities using ICU
|
||||
// spell-checker:ignore fieldsets janvier
|
||||
// spell-checker:ignore fieldsets prefs
|
||||
|
||||
//! Locale-aware datetime formatting utilities using ICU and jiff-icu
|
||||
|
||||
use icu_calendar::Date;
|
||||
use icu_calendar::cal::{Buddhist, Ethiopian, Iso, Persian};
|
||||
use icu_datetime::DateTimeFormatter;
|
||||
use icu_datetime::fieldsets;
|
||||
use icu_locale::Locale;
|
||||
use jiff::civil::Date as JiffDate;
|
||||
use jiff_icu::ConvertFrom;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::i18n::get_locale_from_env;
|
||||
@@ -34,88 +38,6 @@ pub fn should_use_icu_locale() -> bool {
|
||||
*locale != locale!("und")
|
||||
}
|
||||
|
||||
/// Get a localized month name for the given month number (1-12)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `month` - Month number (1 = January, 2 = February, etc.)
|
||||
/// * `full` - If true, return full month name (e.g., "January"), otherwise abbreviated (e.g., "Jan")
|
||||
///
|
||||
/// # Returns
|
||||
/// Localized month name, or falls back to English if locale is not supported
|
||||
pub fn get_localized_month_name(month: u8, full: bool) -> String {
|
||||
// Get locale from environment
|
||||
let (locale, _encoding) = get_time_locale();
|
||||
|
||||
// Create a date with the specified month (use year 2000, day 1 as arbitrary values)
|
||||
let Ok(date) = Date::try_new_gregorian(2000, month, 1) else {
|
||||
// Invalid month, return empty string to signal failure
|
||||
return String::new();
|
||||
};
|
||||
|
||||
// Configure field set for month formatting
|
||||
// Use Year-Month-Day format to ensure we get textual month names
|
||||
let field_set = if full {
|
||||
fieldsets::YMD::long()
|
||||
} else {
|
||||
fieldsets::YMD::medium()
|
||||
};
|
||||
|
||||
// Create formatter with locale
|
||||
let Ok(formatter) = DateTimeFormatter::try_new(locale.clone().into(), field_set) else {
|
||||
// Failed to create formatter, return empty string to signal failure
|
||||
return String::new();
|
||||
};
|
||||
|
||||
// Format the date to get full date, then extract month
|
||||
let formatted = formatter.format(&date).to_string();
|
||||
// Extract month name from formatted date like "15 janvier 2000" or "2000-01-15"
|
||||
// Look for a word that contains letters (the month name)
|
||||
let words: Vec<&str> = formatted.split_whitespace().collect();
|
||||
|
||||
// Return the month name as extracted from ICU (no further processing needed)
|
||||
// ICU already handles the full vs abbreviated formatting correctly
|
||||
words
|
||||
.iter()
|
||||
.find(|word| word.chars().any(|c| c.is_alphabetic()))
|
||||
.map_or_else(String::new, |s| (*s).to_string())
|
||||
}
|
||||
|
||||
/// Get a localized day name for the given date components
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `year` - The year
|
||||
/// * `month` - The month (1-12)
|
||||
/// * `day` - The day of the month
|
||||
/// * `full` - If true, return full day name (e.g., "Monday"), otherwise abbreviated (e.g., "Mon")
|
||||
///
|
||||
/// # Returns
|
||||
/// Localized day name, or falls back to empty string if locale is not supported
|
||||
pub fn get_localized_day_name(year: i32, month: u8, day: u8, full: bool) -> String {
|
||||
// Create ICU Date from components
|
||||
let Ok(date) = Date::try_new_gregorian(year, month, day) else {
|
||||
return String::new();
|
||||
};
|
||||
|
||||
// Get locale from environment
|
||||
let (locale, _encoding) = get_time_locale();
|
||||
|
||||
// Configure field set for day formatting
|
||||
let field_set = if full {
|
||||
fieldsets::E::long() // Full day name
|
||||
} else {
|
||||
fieldsets::E::short() // Abbreviated day name
|
||||
};
|
||||
|
||||
// Create formatter with locale
|
||||
let Ok(formatter) = DateTimeFormatter::try_new(locale.clone().into(), field_set) else {
|
||||
return String::new();
|
||||
};
|
||||
|
||||
// Format the date to get day name
|
||||
let formatted = formatter.format(&date).to_string();
|
||||
formatted.trim().to_string()
|
||||
}
|
||||
|
||||
/// Determine the appropriate calendar system for a given locale
|
||||
pub fn get_locale_calendar_type(locale: &Locale) -> CalendarType {
|
||||
let locale_str = locale.to_string();
|
||||
@@ -145,120 +67,90 @@ pub enum CalendarType {
|
||||
Ethiopian,
|
||||
}
|
||||
|
||||
/// Convert a Gregorian date to the appropriate calendar system for a locale
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `year` - Gregorian year
|
||||
/// * `month` - Month (1-12)
|
||||
/// * `day` - Day (1-31)
|
||||
/// * `calendar_type` - Target calendar system
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Some((era_year, month, day))` - Date in target calendar system
|
||||
/// * `None` - If conversion fails
|
||||
pub fn convert_date_to_locale_calendar(
|
||||
year: i32,
|
||||
month: u8,
|
||||
day: u8,
|
||||
calendar_type: &CalendarType,
|
||||
) -> Option<(i32, u8, u8)> {
|
||||
match calendar_type {
|
||||
CalendarType::Gregorian => Some((year, month, day)),
|
||||
CalendarType::Buddhist => {
|
||||
// Buddhist calendar: Gregorian year + 543
|
||||
Some((year + 543, month, day))
|
||||
}
|
||||
CalendarType::Persian => {
|
||||
// Persian calendar conversion (Solar Hijri)
|
||||
// March 21 (Nowruz) is roughly the start of the Persian year
|
||||
let persian_year = if month > 3 || (month == 3 && day >= 21) {
|
||||
year - 621 // After March 21
|
||||
} else {
|
||||
year - 622 // Before March 21
|
||||
};
|
||||
Some((persian_year, month, day))
|
||||
}
|
||||
CalendarType::Ethiopian => {
|
||||
// Ethiopian calendar conversion
|
||||
// September 11/12 is roughly the start of the Ethiopian year
|
||||
let ethiopian_year = if month > 9 || (month == 9 && day >= 11) {
|
||||
year - 7 // After September 11
|
||||
} else {
|
||||
year - 8 // Before September 11
|
||||
};
|
||||
Some((ethiopian_year, month, day))
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Transform a strftime format string to use locale-specific calendar values
|
||||
pub fn localize_format_string(format: &str, date: &JiffDate) -> String {
|
||||
let (locale, _) = get_time_locale();
|
||||
let iso_date = Date::<Iso>::convert_from(*date);
|
||||
|
||||
/// Get the era year for a given date and locale
|
||||
pub fn get_era_year(year: i32, month: u8, day: u8, locale: &Locale) -> Option<i32> {
|
||||
// Validate input date
|
||||
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
|
||||
return None;
|
||||
}
|
||||
let mut fmt = format.to_string();
|
||||
|
||||
// For non-Gregorian calendars, replace date components with converted values
|
||||
let calendar_type = get_locale_calendar_type(locale);
|
||||
match calendar_type {
|
||||
CalendarType::Gregorian => None,
|
||||
_ => convert_date_to_locale_calendar(year, month, day, &calendar_type)
|
||||
.map(|(era_year, _, _)| era_year),
|
||||
if calendar_type != CalendarType::Gregorian {
|
||||
let (cal_year, cal_month, cal_day) = match calendar_type {
|
||||
CalendarType::Buddhist => {
|
||||
let d = iso_date.to_calendar(Buddhist);
|
||||
(d.extended_year(), d.month().ordinal, d.day_of_month().0)
|
||||
}
|
||||
CalendarType::Persian => {
|
||||
let d = iso_date.to_calendar(Persian);
|
||||
(d.extended_year(), d.month().ordinal, d.day_of_month().0)
|
||||
}
|
||||
CalendarType::Ethiopian => {
|
||||
let d = iso_date.to_calendar(Ethiopian::new());
|
||||
(d.extended_year(), d.month().ordinal, d.day_of_month().0)
|
||||
}
|
||||
CalendarType::Gregorian => unreachable!(),
|
||||
};
|
||||
fmt = fmt
|
||||
.replace("%Y", &cal_year.to_string())
|
||||
.replace("%m", &format!("{cal_month:02}"))
|
||||
.replace("%d", &format!("{cal_day:02}"))
|
||||
.replace("%e", &format!("{cal_day:2}"));
|
||||
}
|
||||
|
||||
// Format localized names using ICU DateTimeFormatter
|
||||
let locale_prefs = locale.clone().into();
|
||||
|
||||
if fmt.contains("%B") {
|
||||
if let Ok(f) = DateTimeFormatter::try_new(locale_prefs, fieldsets::M::long()) {
|
||||
fmt = fmt.replace("%B", &f.format(&iso_date).to_string());
|
||||
}
|
||||
}
|
||||
if fmt.contains("%b") || fmt.contains("%h") {
|
||||
if let Ok(f) = DateTimeFormatter::try_new(locale_prefs, fieldsets::M::medium()) {
|
||||
let month_abbrev = f.format(&iso_date).to_string();
|
||||
fmt = fmt
|
||||
.replace("%b", &month_abbrev)
|
||||
.replace("%h", &month_abbrev);
|
||||
}
|
||||
}
|
||||
if fmt.contains("%A") {
|
||||
if let Ok(f) = DateTimeFormatter::try_new(locale_prefs, fieldsets::E::long()) {
|
||||
fmt = fmt.replace("%A", &f.format(&iso_date).to_string());
|
||||
}
|
||||
}
|
||||
if fmt.contains("%a") {
|
||||
if let Ok(f) = DateTimeFormatter::try_new(locale_prefs, fieldsets::E::short()) {
|
||||
fmt = fmt.replace("%a", &f.format(&iso_date).to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fmt
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_localized_month_name_fallback() {
|
||||
// This should work even if locale is not available
|
||||
let name = get_localized_month_name(1, true);
|
||||
// The function may return empty string if ICU fails, which is fine
|
||||
// The caller (date.rs) will handle this by falling back to jiff
|
||||
assert!(name.is_empty() || name.len() >= 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calendar_type_detection() {
|
||||
let thai_locale = icu_locale::locale!("th-TH");
|
||||
let persian_locale = icu_locale::locale!("fa-IR");
|
||||
let amharic_locale = icu_locale::locale!("am-ET");
|
||||
let english_locale = icu_locale::locale!("en-US");
|
||||
|
||||
use icu_locale::locale;
|
||||
assert_eq!(
|
||||
get_locale_calendar_type(&thai_locale),
|
||||
get_locale_calendar_type(&locale!("th-TH")),
|
||||
CalendarType::Buddhist
|
||||
);
|
||||
assert_eq!(
|
||||
get_locale_calendar_type(&persian_locale),
|
||||
get_locale_calendar_type(&locale!("fa-IR")),
|
||||
CalendarType::Persian
|
||||
);
|
||||
assert_eq!(
|
||||
get_locale_calendar_type(&amharic_locale),
|
||||
get_locale_calendar_type(&locale!("am-ET")),
|
||||
CalendarType::Ethiopian
|
||||
);
|
||||
assert_eq!(
|
||||
get_locale_calendar_type(&english_locale),
|
||||
get_locale_calendar_type(&locale!("en-US")),
|
||||
CalendarType::Gregorian
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_era_year_conversion() {
|
||||
let thai_locale = icu_locale::locale!("th-TH");
|
||||
let persian_locale = icu_locale::locale!("fa-IR");
|
||||
let amharic_locale = icu_locale::locale!("am-ET");
|
||||
|
||||
// Test Thai Buddhist calendar (2026 + 543 = 2569)
|
||||
assert_eq!(get_era_year(2026, 6, 15, &thai_locale), Some(2569));
|
||||
|
||||
// Test Persian calendar (rough approximation)
|
||||
assert_eq!(get_era_year(2026, 3, 22, &persian_locale), Some(1405));
|
||||
assert_eq!(get_era_year(2026, 3, 19, &persian_locale), Some(1404));
|
||||
|
||||
// Test Ethiopian calendar (rough approximation)
|
||||
assert_eq!(get_era_year(2026, 9, 12, &amharic_locale), Some(2019));
|
||||
assert_eq!(get_era_year(2026, 9, 10, &amharic_locale), Some(2018));
|
||||
}
|
||||
}
|
||||
|
||||
+101
-1
@@ -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: AEDT AEST EEST NZDT NZST Kolkata Iseconds févr février janv janvier mercredi samedi sommes
|
||||
// spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata Iseconds févr février janv janvier mercredi samedi sommes juin décembre Januar Juni Dezember enero junio diciembre gennaio giugno dicembre junho dezembro lundi dimanche Montag Sonntag Samstag sábado
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
@@ -1866,3 +1866,103 @@ fn test_date_thai_locale_solar_calendar() {
|
||||
let rfc_output = rfc_result.stdout_str();
|
||||
assert!(rfc_output.starts_with(¤t_year.to_string()));
|
||||
}
|
||||
|
||||
fn check_date(locale: &str, date: &str, fmt: &str, expected: &str) {
|
||||
let actual = new_ucmd!()
|
||||
.env("LC_ALL", locale)
|
||||
.arg("-d")
|
||||
.arg(date)
|
||||
.arg(fmt)
|
||||
.succeeds()
|
||||
.stdout_str()
|
||||
.trim()
|
||||
.to_string();
|
||||
assert_eq!(actual, expected, "LC_ALL={locale} date -d '{date}' '{fmt}'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_locale_calendar_conversions() {
|
||||
// Persian (Solar Hijri) - Nowruz is March 20/21
|
||||
for (d, e) in [
|
||||
("2026-01-01", "1404-10-11"),
|
||||
("2026-01-26", "1404-11-06"),
|
||||
("2026-03-20", "1404-12-29"),
|
||||
("2026-03-21", "1405-01-01"),
|
||||
("2026-03-22", "1405-01-02"),
|
||||
("2026-06-15", "1405-03-25"),
|
||||
("2026-12-31", "1405-10-10"),
|
||||
("2025-03-20", "1403-12-30"),
|
||||
("2025-03-21", "1404-01-01"),
|
||||
("2024-03-19", "1402-12-29"),
|
||||
("2024-03-20", "1403-01-01"),
|
||||
("2000-03-20", "1379-01-01"),
|
||||
] {
|
||||
check_date("fa_IR.UTF-8", d, "+%Y-%m-%d", e);
|
||||
}
|
||||
|
||||
// Thai Buddhist (year + 543, same month/day)
|
||||
for (d, e) in [
|
||||
("2026-01-01", "2569-01-01"),
|
||||
("2026-01-26", "2569-01-26"),
|
||||
("2026-06-15", "2569-06-15"),
|
||||
("2026-12-31", "2569-12-31"),
|
||||
("2025-01-01", "2568-01-01"),
|
||||
("2024-02-29", "2567-02-29"),
|
||||
("2000-01-01", "2543-01-01"),
|
||||
("1970-01-01", "2513-01-01"),
|
||||
] {
|
||||
check_date("th_TH.UTF-8", d, "+%Y-%m-%d", e);
|
||||
}
|
||||
|
||||
// Ethiopian (13 months, New Year on Sept 11)
|
||||
for (d, e) in [
|
||||
("2026-01-01", "2018-04-23"),
|
||||
("2026-01-26", "2018-05-18"),
|
||||
("2026-09-10", "2018-13-05"),
|
||||
("2026-09-11", "2019-01-01"),
|
||||
("2026-09-12", "2019-01-02"),
|
||||
("2026-12-31", "2019-04-22"),
|
||||
("2025-09-11", "2018-01-01"),
|
||||
("2025-09-10", "2017-13-05"),
|
||||
("2000-09-11", "1993-01-01"),
|
||||
] {
|
||||
check_date("am_ET.UTF-8", d, "+%Y-%m-%d", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_locale_month_names() {
|
||||
// %B full month names: Jan, Jun, Dec for each locale
|
||||
for (loc, jan, jun, dec) in [
|
||||
("fr_FR.UTF-8", "janvier", "juin", "décembre"),
|
||||
("de_DE.UTF-8", "Januar", "Juni", "Dezember"),
|
||||
("es_ES.UTF-8", "enero", "junio", "diciembre"),
|
||||
("it_IT.UTF-8", "gennaio", "giugno", "dicembre"),
|
||||
("pt_BR.UTF-8", "janeiro", "junho", "dezembro"),
|
||||
("ja_JP.UTF-8", "1月", "6月", "12月"),
|
||||
("zh_CN.UTF-8", "一月", "六月", "十二月"),
|
||||
] {
|
||||
check_date(loc, "2026-01-15", "+%B", jan);
|
||||
check_date(loc, "2026-06-15", "+%B", jun);
|
||||
check_date(loc, "2026-12-15", "+%B", dec);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_locale_day_names() {
|
||||
// %A full day names: Mon (26th), Sun (25th), Sat (24th) Jan 2026
|
||||
for (loc, mon, sun, sat) in [
|
||||
("fr_FR.UTF-8", "lundi", "dimanche", "samedi"),
|
||||
("de_DE.UTF-8", "Montag", "Sonntag", "Samstag"),
|
||||
("es_ES.UTF-8", "lunes", "domingo", "sábado"),
|
||||
("ja_JP.UTF-8", "月曜日", "日曜日", "土曜日"),
|
||||
("zh_CN.UTF-8", "星期一", "星期日", "星期六"),
|
||||
] {
|
||||
check_date(loc, "2026-01-26", "+%A", mon);
|
||||
check_date(loc, "2026-01-25", "+%A", sun);
|
||||
check_date(loc, "2026-01-24", "+%A", sat);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user