use std::cell::RefCell;
use std::collections::HashMap;
use icu_calendar::cal::Gregorian;
use icu_datetime::FixedCalendarDateTimeFormatter;
use icu_datetime::fieldsets::enums::CompositeDateTimeFieldSet;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum DateFields {
Year,
YearMonth,
Month,
MonthDay,
YearMonthDay,
HourMinute,
HourMinuteSecond,
}
type Formatter = FixedCalendarDateTimeFormatter<Gregorian, CompositeDateTimeFieldSet>;
day_reactive::tls_slots! {
datetime;
static FORMATTERS: RefCell<HashMap<(String, DateFields), Option<Formatter>>> =
RefCell::new(HashMap::new());
}
fn build(locale: &str, fields: DateFields) -> Option<Formatter> {
use icu_datetime::fieldsets::builder::{DateFields as Icu, FieldSetBuilder};
use icu_datetime::options::{Length, TimePrecision};
let parsed: icu_locale_core::Locale = locale.parse().ok()?;
let mut b = FieldSetBuilder::new();
b.length = Some(Length::Medium);
match fields {
DateFields::Year => b.date_fields = Some(Icu::Y),
DateFields::YearMonth => b.date_fields = Some(Icu::YM),
DateFields::Month => b.date_fields = Some(Icu::M),
DateFields::MonthDay => b.date_fields = Some(Icu::MD),
DateFields::YearMonthDay => b.date_fields = Some(Icu::YMD),
DateFields::HourMinute => b.time_precision = Some(TimePrecision::Minute),
DateFields::HourMinuteSecond => b.time_precision = Some(TimePrecision::Second),
}
let field_set = b.build_composite_datetime().ok()?;
Formatter::try_new((&parsed).into(), field_set).ok()
}
fn iso(fields: DateFields, (y, m, d): (i32, u8, u8), (hh, mm, ss): (u8, u8, u8)) -> String {
match fields {
DateFields::Year => format!("{y:04}"),
DateFields::YearMonth => format!("{y:04}-{m:02}"),
DateFields::Month => format!("{m:02}"),
DateFields::MonthDay => format!("{m:02}-{d:02}"),
DateFields::YearMonthDay => format!("{y:04}-{m:02}-{d:02}"),
DateFields::HourMinute => format!("{hh:02}:{mm:02}"),
DateFields::HourMinuteSecond => format!("{hh:02}:{mm:02}:{ss:02}"),
}
}
pub fn format_date_in(locale: &str, epoch_seconds: f64, fields: DateFields) -> String {
if !epoch_seconds.is_finite() {
return format!("{epoch_seconds}");
}
let (Some(date), Some(time)) = crate::intl::from_epoch_seconds(epoch_seconds.floor() as i64)
else {
return format!("{epoch_seconds}");
};
let (Ok(d), Ok(t)) = (
icu_calendar::Date::try_new_gregorian(date.0, date.1, date.2),
icu_time::Time::try_new(time.0, time.1, time.2, 0),
) else {
return iso(fields, date, time);
};
FORMATTERS.with(|c| {
let mut map = c.borrow_mut();
let formatter = map
.entry((locale.to_string(), fields))
.or_insert_with(|| build(locale, fields));
match formatter {
Some(f) => f
.format(&icu_time::DateTime { date: d, time: t })
.to_string(),
None => iso(fields, date, time),
}
})
}
pub fn format_date(epoch_seconds: f64, fields: DateFields) -> String {
let locale = crate::locale().get();
format_date_in(&locale, epoch_seconds, fields)
}
#[cfg(test)]
mod tests {
use super::*;
const T: f64 = 1_772_724_615.0;
fn f(locale: &str, fields: DateFields) -> String {
format_date_in(locale, T, fields).replace(['\u{a0}', '\u{202f}'], " ")
}
#[test]
fn a_month_and_day_are_written_the_locales_way() {
assert_eq!(f("en", DateFields::MonthDay), "Mar 5");
assert_eq!(f("fr", DateFields::MonthDay), "5 mars");
assert_eq!(f("de", DateFields::MonthDay), "05.03.");
assert_eq!(f("ja", DateFields::MonthDay), "03/05");
}
#[test]
fn a_month_and_year_are_written_the_locales_way() {
assert_eq!(f("en", DateFields::YearMonth), "Mar 2026");
assert_eq!(f("fr", DateFields::YearMonth), "mars 2026");
assert_eq!(f("de", DateFields::YearMonth), "03/2026");
assert_eq!(f("ja", DateFields::YearMonth), "2026/03");
assert_eq!(f("en", DateFields::Year), "2026");
assert_eq!(f("ja", DateFields::Year), "2026年");
}
#[test]
fn a_month_alone_is_the_locales_abbreviated_name() {
assert_eq!(f("en", DateFields::Month), "Mar");
assert_eq!(f("fr", DateFields::Month), "mars");
assert_eq!(f("de", DateFields::Month), "Mär");
assert_eq!(f("ja", DateFields::Month), "3月");
assert_eq!(f("ru", DateFields::Month), "март");
}
#[test]
fn a_full_date_follows_the_locales_order() {
assert_eq!(f("en", DateFields::YearMonthDay), "Mar 5, 2026");
assert_eq!(f("fr", DateFields::YearMonthDay), "5 mars 2026");
assert_eq!(f("de", DateFields::YearMonthDay), "05.03.2026");
}
#[test]
fn the_clock_keeps_the_locales_hour_cycle() {
assert_eq!(f("en", DateFields::HourMinute), "3:30 PM");
assert_eq!(f("fr", DateFields::HourMinute), "15:30");
assert_eq!(f("de", DateFields::HourMinute), "15:30");
assert_eq!(f("en", DateFields::HourMinuteSecond), "3:30:15 PM");
assert_eq!(f("fr", DateFields::HourMinuteSecond), "15:30:15");
}
#[test]
fn an_instant_before_the_epoch_and_a_non_finite_one_degrade_sensibly() {
assert_eq!(
format_date_in("en", -3600.0, DateFields::YearMonthDay),
"Dec 31, 1969"
);
assert_eq!(format_date_in("en", f64::NAN, DateFields::Year), "NaN");
}
#[test]
fn an_unparseable_locale_falls_back_to_iso() {
assert_eq!(
format_date_in("", T, DateFields::YearMonthDay),
"2026-03-05"
);
assert_eq!(format_date_in("", T, DateFields::HourMinute), "15:30");
}
#[test]
fn the_tracked_form_follows_the_current_locale() {
crate::set_locale("fr");
assert_eq!(
format_date(T, DateFields::MonthDay).replace('\u{a0}', " "),
"5 mars"
);
crate::set_locale("en");
assert_eq!(format_date(T, DateFields::MonthDay), "Mar 5");
}
}