use icu_datetime::{
fieldsets::{T, YMD, YMDT},
input::{Date, DateTime, Time},
options::Length,
DateTimeFormatter, DateTimeFormatterPreferences, NoCalendarFormatter,
};
use icu_locale_core::Locale;
use crate::CollateError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DateLength {
Full,
Long,
#[default]
Medium,
Short,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TimeLength {
Full,
Long,
#[default]
Medium,
Short,
None,
}
impl DateLength {
fn to_icu_length(self) -> Length {
match self {
Self::Full | Self::Long => Length::Long,
Self::Medium => Length::Medium,
Self::Short => Length::Short,
}
}
}
impl TimeLength {
fn to_icu_length(self) -> Length {
match self {
Self::Full | Self::Long => Length::Long,
Self::Medium => Length::Medium,
Self::Short => Length::Short,
Self::None => Length::Short,
}
}
}
pub struct IcuDateTimeFormatter {
locale_string: String,
date_formatter: DateTimeFormatter<YMD>,
time_formatter: NoCalendarFormatter<T>,
datetime_formatter: DateTimeFormatter<YMDT>,
}
impl IcuDateTimeFormatter {
pub fn new(locale_id: &str) -> Result<Self, CollateError> {
Self::new_with_lengths(locale_id, DateLength::default(), TimeLength::default())
}
pub fn new_with_lengths(
locale_id: &str,
date_length: DateLength,
time_length: TimeLength,
) -> Result<Self, CollateError> {
let locale: Locale = locale_id
.parse()
.map_err(|e| CollateError::InvalidLocale(format!("{e}")))?;
let dl = date_length.to_icu_length();
let tl = time_length.to_icu_length();
let prefs = DateTimeFormatterPreferences::from(locale);
let date_formatter = DateTimeFormatter::try_new(prefs, YMD::for_length(dl))
.map_err(|e| CollateError::Icu(format!("date formatter: {e}")))?;
let time_formatter = NoCalendarFormatter::try_new(prefs, T::for_length(tl))
.map_err(|e| CollateError::Icu(format!("time formatter: {e}")))?;
let datetime_formatter = DateTimeFormatter::try_new(prefs, YMDT::for_length(dl))
.map_err(|e| CollateError::Icu(format!("datetime formatter: {e}")))?;
Ok(Self {
locale_string: locale_id.to_owned(),
date_formatter,
time_formatter,
datetime_formatter,
})
}
pub fn format_date(&self, year: i32, month: u8, day: u8) -> String {
match Date::try_new_iso(year, month, day) {
Ok(date) => self.date_formatter.format(&date).to_string(),
Err(_) => format!("{year:04}-{month:02}-{day:02}"),
}
}
pub fn format_time(&self, hour: u8, minute: u8, second: u8) -> String {
match Time::try_new(hour, minute, second, 0) {
Ok(time) => self.time_formatter.format(&time).to_string(),
Err(_) => format!("{hour:02}:{minute:02}:{second:02}"),
}
}
pub fn format_datetime(
&self,
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
) -> String {
let date = Date::try_new_iso(year, month, day);
let time = Time::try_new(hour, minute, second, 0);
match (date, time) {
(Ok(d), Ok(t)) => {
let dt = DateTime { date: d, time: t };
self.datetime_formatter.format(&dt).to_string()
}
_ => format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}"),
}
}
pub fn locale_id(&self) -> &str {
&self.locale_string
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_datetime_formatter_basic() {
let fmt = IcuDateTimeFormatter::new("en").expect("en formatter");
let s = fmt.format_date(2025, 5, 25);
assert!(
s.contains("2025") || s.contains("25"),
"unexpected date string: {s}"
);
}
#[test]
fn test_datetime_formatter_japanese() {
let fmt = IcuDateTimeFormatter::new("ja").expect("ja formatter");
let s = fmt.format_date(2025, 1, 7);
assert!(!s.is_empty(), "Japanese date format should not be empty");
}
#[test]
fn locale_id_round_trips() {
let fmt = IcuDateTimeFormatter::new("de").expect("German formatter");
assert_eq!(fmt.locale_id(), "de");
}
#[test]
fn format_time_basic() {
let fmt = IcuDateTimeFormatter::new("en").expect("en formatter");
let s = fmt.format_time(14, 30, 0);
assert!(!s.is_empty(), "time format should not be empty: {s}");
}
#[test]
fn format_datetime_basic() {
let fmt = IcuDateTimeFormatter::new("en").expect("en formatter");
let s = fmt.format_datetime(2025, 5, 25, 14, 30, 0);
assert!(!s.is_empty(), "datetime format should not be empty: {s}");
}
#[test]
fn out_of_range_date_falls_back() {
let fmt = IcuDateTimeFormatter::new("en").expect("en formatter");
let s = fmt.format_date(2025, 0, 1);
assert_eq!(s, "2025-00-01", "fallback format unexpected: {s}");
}
#[test]
fn out_of_range_time_falls_back() {
let fmt = IcuDateTimeFormatter::new("en").expect("en formatter");
let s = fmt.format_time(25, 0, 0);
assert_eq!(s, "25:00:00", "fallback format unexpected: {s}");
}
#[test]
fn new_with_lengths_short() {
let fmt =
IcuDateTimeFormatter::new_with_lengths("en", DateLength::Short, TimeLength::Short)
.expect("short formatter");
let s = fmt.format_date(2025, 5, 25);
assert!(!s.is_empty(), "short date should not be empty: {s}");
}
#[test]
fn new_with_lengths_full_maps_to_long() {
let fmt = IcuDateTimeFormatter::new_with_lengths("en", DateLength::Full, TimeLength::Full)
.expect("full formatter");
let s = fmt.format_date(2025, 5, 25);
assert!(!s.is_empty(), "full->long date should not be empty: {s}");
}
#[test]
fn invalid_locale_returns_error() {
let result = IcuDateTimeFormatter::new("not-a-valid-locale!!!");
assert!(result.is_err(), "invalid locale should return error");
}
}