#[cfg(feature = "datetime-icu")]
use alloc::boxed::Box;
use core::marker::PhantomData;
use icu_calendar::Date as IcuDate;
use icu_datetime::fieldsets::builder::{DateFields as F, FieldSetBuilder, ZoneStyle as Zs};
use icu_datetime::fieldsets::enums::{CompositeDateTimeFieldSet, CompositeFieldSet};
use icu_datetime::options::{Length, TimePrecision as P};
use icu_datetime::unchecked::DateTimeInputUnchecked;
use icu_datetime::{
DateTimeFormatter, DateTimeFormatterPreferences, FixedCalendarDateTimeFormatter,
};
use icu_locale_core::Locale;
use icu_locale_core::extensions::unicode::Value;
use icu_locale_core::preferences::extensions::unicode::keywords::{CalendarAlgorithm, HourCycle};
#[cfg(feature = "datetime-icu")]
use icu_provider::buf::BufferProvider;
use icu_time::zone::iana::IanaParser;
use icu_time::zone::{UtcOffset, ZoneNameTimestamp};
use icu_time::{Time, TimeZone};
use mf2_runtime::{
DateFields, DateLength, DateStyle, DateTimeOptions, Dir, FnContext, FormatError, Sink,
TimePrecision, ZoneOption, ZoneStyle,
};
use writeable::TryWriteable;
use crate::plan::{Backend, Plan};
#[derive(Clone, Copy, Debug)]
pub struct AnyCalendar;
#[derive(Clone, Copy, Debug)]
pub struct GregorianOnly;
#[derive(Clone, Copy, Debug)]
pub struct WithZones;
#[derive(Clone, Copy, Debug)]
pub struct NoZones;
#[cfg(feature = "datetime-icu")]
#[derive(Clone, Copy, Debug)]
pub struct Blob;
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
#[derive(Clone, Copy, Debug)]
pub struct Compiled;
#[doc(hidden)]
pub enum Source<'p> {
#[cfg(feature = "datetime-icu")]
Buffer(&'p dyn BufferProvider),
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
Compiled(PhantomData<&'p ()>),
}
#[doc(hidden)]
pub trait Data: Sync + 'static {
fn with<R>(
cx: &FnContext<'_>,
f: impl FnOnce(Source<'_>) -> Result<R, FormatError>,
) -> Result<R, FormatError>;
}
#[cfg(feature = "datetime-icu")]
impl Data for Blob {
fn with<R>(
cx: &FnContext<'_>,
f: impl FnOnce(Source<'_>) -> Result<R, FormatError>,
) -> Result<R, FormatError> {
let bytes = cx
.catalog()
.locale_entry(mf2_catalog::format::locale_key::ICU_BLOB)
.ok_or(FormatError::UnsupportedOperation)?;
let provider = icu_provider_blob::BlobDataProvider::try_new_from_blob(Box::from(bytes))
.map_err(|_| FormatError::UnsupportedOperation)?;
f(Source::Buffer(&provider))
}
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
impl Data for Compiled {
fn with<R>(
_cx: &FnContext<'_>,
f: impl FnOnce(Source<'_>) -> Result<R, FormatError>,
) -> Result<R, FormatError> {
f(Source::Compiled(PhantomData))
}
}
type Params<C, Z, D> = PhantomData<fn() -> (C, Z, D)>;
#[cfg(feature = "datetime-icu")]
pub struct Icu<C = AnyCalendar, Z = WithZones, D = Blob>(Params<C, Z, D>);
#[cfg(not(feature = "datetime-icu"))]
pub struct Icu<C = AnyCalendar, Z = WithZones, D = Compiled>(Params<C, Z, D>);
impl<C, Z, D> Icu<C, Z, D> {
pub const NEW: Self = Icu(PhantomData);
}
impl<C, Z, D> Clone for Icu<C, Z, D> {
fn clone(&self) -> Self {
*self
}
}
impl<C, Z, D> Copy for Icu<C, Z, D> {}
impl<C, Z, D> core::fmt::Debug for Icu<C, Z, D> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("Icu")
}
}
#[doc(hidden)]
pub trait Variant {
const ZONES: bool;
const GREGORIAN_ONLY: bool;
fn run(
src: &Source<'_>,
locale: &str,
o: &DateTimeOptions<'_>,
run: Option<(&Plan<'_>, Option<&mut dyn Sink>)>,
) -> Result<(), FormatError>;
}
impl<C: 'static, Z: 'static, D: Data> Backend for Icu<C, Z, D>
where
(C, Z): Variant,
{
fn supports(&self, cx: &FnContext<'_>, plan: &Plan<'_>) -> Result<(), FormatError> {
D::with(cx, |src| {
<(C, Z)>::run(&src, cx.locale(), plan.options, Some((plan, None)))
})
}
fn format(&self, cx: &FnContext<'_>, plan: &Plan<'_>, out: &mut dyn Sink) {
let _ = D::with(cx, |src| {
<(C, Z)>::run(&src, cx.locale(), plan.options, Some((plan, Some(out))))
});
}
fn dir(&self, cx: &FnContext<'_>, _plan: &Plan<'_>) -> Dir {
cx.catalog().dir()
}
}
struct SinkWrite<'s>(&'s mut dyn Sink);
impl core::fmt::Write for SinkWrite<'_> {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
self.0.push_str(s);
Ok(())
}
}
fn prefs(
locale: &str,
o: &DateTimeOptions<'_>,
gregorian_only: bool,
) -> Result<DateTimeFormatterPreferences, FormatError> {
let loc = Locale::try_from_str(locale).map_err(|_| FormatError::UnsupportedOperation)?;
let mut p = DateTimeFormatterPreferences::from(&loc);
if let Some(h12) = o.hour12 {
p.hour_cycle = Some(if h12 { HourCycle::H12 } else { HourCycle::H23 });
}
if let Some(cal) = o.calendar {
if gregorian_only && cal != "gregory" {
return Err(FormatError::UnsupportedOperation);
}
let v = Value::try_from_str(cal).map_err(|_| FormatError::UnsupportedOperation)?;
let alg = CalendarAlgorithm::try_from(&v).map_err(|_| FormatError::UnsupportedOperation)?;
p.calendar_algorithm = Some(alg);
}
Ok(p)
}
fn builder(o: &DateTimeOptions<'_>) -> Result<FieldSetBuilder, FormatError> {
let mut b = FieldSetBuilder::new();
if let Some(DateStyle { fields, length }) = o.date {
b.date_fields = Some(match fields {
DateFields::Weekday => F::E,
DateFields::DayWeekday => F::DE,
DateFields::MonthDay => F::MD,
DateFields::MonthDayWeekday => F::MDE,
DateFields::YearMonthDay => F::YMD,
DateFields::YearMonthDayWeekday => F::YMDE,
_ => return Err(FormatError::UnsupportedOperation),
});
b.length = Some(match length {
DateLength::Long => Length::Long,
DateLength::Medium => Length::Medium,
DateLength::Short => Length::Short,
_ => return Err(FormatError::UnsupportedOperation),
});
}
b.time_precision = match o.time {
None => None,
Some(TimePrecision::Hour) => Some(P::Hour),
Some(TimePrecision::Minute) => Some(P::Minute),
Some(TimePrecision::Second) => Some(P::Second),
Some(_) => return Err(FormatError::UnsupportedOperation),
};
Ok(b)
}
fn time(plan: &Plan<'_>) -> Result<Time, FormatError> {
let t = plan.time;
Time::try_new(
t.hour(),
t.minute(),
t.second(),
u32::from(t.millisecond()) * 1_000_000,
)
.map_err(|_| FormatError::UnsupportedOperation)
}
fn iana(src: &Source<'_>) -> Result<IanaParser, FormatError> {
match src {
#[cfg(feature = "datetime-icu")]
Source::Buffer(p) => IanaParser::try_new_with_buffer_provider(*p)
.map_err(|_| FormatError::UnsupportedOperation),
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
Source::Compiled(_) => Ok(IanaParser::new().static_to_owned()),
}
}
fn zone(
src: &Source<'_>,
plan: &Plan<'_>,
input: &mut DateTimeInputUnchecked,
) -> Result<(), FormatError> {
let parser = iana(src)?;
let (id, offset) = match plan.zone {
ZoneOption::Offset(o) => (TimeZone::UNKNOWN, Some(o)),
ZoneOption::Named(n) => (parser.as_borrowed().parse(n), plan.offset),
ZoneOption::Utc | ZoneOption::Input => (parser.as_borrowed().parse("Etc/UTC"), Some(0)),
_ => return Err(FormatError::UnsupportedOperation),
};
input.set_time_zone_id(id);
if let Some(o) = offset {
let o = UtcOffset::try_from_seconds(o).map_err(|_| FormatError::UnsupportedOperation)?;
input.set_time_zone_utc_offset(o);
}
let t = plan.epoch_ms().unwrap_or_else(|| plan.wall_ms());
input.set_time_zone_name_timestamp(ZoneNameTimestamp::from_epoch_seconds(t.div_euclid(1000)));
Ok(())
}
fn write(formatted: &impl TryWriteable, out: Option<&mut dyn Sink>) {
if let Some(out) = out {
let _ = formatted.try_write_to(&mut SinkWrite(out));
}
}
macro_rules! variant {
($cal:ty, $zones:ty, $has_zones:expr, $gregorian:expr, $fmt:ty, $build:ident, $date:expr) => {
impl Variant for ($cal, $zones) {
const ZONES: bool = $has_zones;
const GREGORIAN_ONLY: bool = $gregorian;
fn run(
src: &Source<'_>,
locale: &str,
o: &DateTimeOptions<'_>,
run: Option<(&Plan<'_>, Option<&mut dyn Sink>)>,
) -> Result<(), FormatError> {
let mut b = builder(o)?;
if let Some(style) = o.time_zone_style {
if !$has_zones {
return Err(FormatError::UnsupportedOperation);
}
b.zone_style = Some(match style {
ZoneStyle::Long => Zs::SpecificLong,
ZoneStyle::Short => Zs::SpecificShort,
_ => return Err(FormatError::UnsupportedOperation),
});
}
let fs = b.$build().map_err(|_| FormatError::UnsupportedOperation)?;
let prefs = prefs(locale, o, $gregorian)?;
let f = match src {
#[cfg(feature = "datetime-icu")]
Source::Buffer(p) => <$fmt>::try_new_with_buffer_provider(*p, prefs, fs),
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
Source::Compiled(_) => <$fmt>::try_new(prefs, fs),
}
.map_err(|_| FormatError::UnsupportedOperation)?;
let mut input = DateTimeInputUnchecked::default();
let Some((plan, out)) = run else {
if o.time_zone_style.is_some() {
iana(src)?;
}
return Ok(());
};
let date = $date(&f, plan)?;
input.set_date_fields_unchecked(date);
input.set_time_fields(time(plan)?);
if o.time_zone_style.is_some() {
zone(src, plan, &mut input)?;
}
write(&f.format_unchecked(input), out);
Ok(())
}
}
};
}
fn gregorian<F>(
_f: &F,
plan: &Plan<'_>,
) -> Result<IcuDate<icu_calendar::cal::Gregorian>, FormatError> {
let d = plan.date;
IcuDate::try_new_gregorian(d.year(), d.month(), d.day())
.map_err(|_| FormatError::UnsupportedOperation)
}
fn in_calendar<'f, FSet: icu_datetime::scaffold::DateTimeMarkers>(
f: &'f DateTimeFormatter<FSet>,
plan: &Plan<'_>,
) -> Result<IcuDate<icu_calendar::Ref<'f, icu_calendar::AnyCalendar>>, FormatError> {
let d = plan.date;
let iso = IcuDate::try_new_iso(d.year(), d.month(), d.day())
.map_err(|_| FormatError::UnsupportedOperation)?;
Ok(iso.to_calendar(f.calendar()))
}
variant!(
AnyCalendar,
WithZones,
true,
false,
DateTimeFormatter<CompositeFieldSet>,
build_composite,
in_calendar
);
variant!(
AnyCalendar,
NoZones,
false,
false,
DateTimeFormatter<CompositeDateTimeFieldSet>,
build_composite_datetime,
in_calendar
);
variant!(
GregorianOnly,
WithZones,
true,
true,
FixedCalendarDateTimeFormatter<icu_calendar::cal::Gregorian, CompositeFieldSet>,
build_composite,
gregorian
);
variant!(
GregorianOnly,
NoZones,
false,
true,
FixedCalendarDateTimeFormatter<icu_calendar::cal::Gregorian, CompositeDateTimeFieldSet>,
build_composite_datetime,
gregorian
);
#[cfg(feature = "datetime-icu")]
#[doc(hidden)]
pub fn prime<'c, C: 'static, Z: 'static>(
provider: &dyn BufferProvider,
locale: &str,
shapes: &[DateTimeOptions<'c>],
) -> alloc::vec::Vec<(DateTimeOptions<'c>, FormatError)>
where
(C, Z): Variant,
{
let src = Source::Buffer(provider);
let mut failed = alloc::vec::Vec::new();
for o in shapes {
if (o.time_zone_style.is_some() && !<(C, Z)>::ZONES)
|| (<(C, Z)>::GREGORIAN_ONLY && o.calendar.is_some_and(|c| c != "gregory"))
{
continue;
}
if let Err(e) = <(C, Z)>::run(&src, locale, o, None) {
failed.push((*o, e));
}
}
failed
}