#[cfg(feature = "datetime-intl")]
use mf2_runtime::{
DateFields, DateLength, DateStyle, DateTimeRequest, Sink, TimePrecision, ZoneOption, ZoneStyle,
};
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen(inline_js = r#"
const formatters = new Map();
const zones = new Map();
export function mf2_dtf(locale, options, t) {
const key = locale + '\u0000' + options;
let f = formatters.get(key);
if (f === undefined) {
try { f = new Intl.DateTimeFormat(locale, JSON.parse(options)); } catch (e) { f = null; }
formatters.set(key, f);
}
if (f === null) return undefined;
try { return f.format(t); } catch (e) { return undefined; }
}
export function mf2_zone_offset(zone, t) {
let f = zones.get(zone);
if (f === undefined) {
try {
f = new Intl.DateTimeFormat('en-US', { timeZone: zone, timeZoneName: 'longOffset' });
} catch (e) { f = null; }
zones.set(zone, f);
}
if (f === null) return undefined;
let name;
try {
name = f.formatToParts(t).find((p) => p.type === 'timeZoneName').value;
} catch (e) { return undefined; }
const m = /^GMT(?:([+\u2212-])(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/.exec(name);
if (m === null) return undefined;
if (m[1] === undefined) return 0;
const s = Number(m[2]) * 3600 + Number(m[3]) * 60 + Number(m[4] ?? 0);
return m[1] === '+' ? s : -s;
}
"#)]
extern "C" {
#[cfg(feature = "datetime-intl")]
fn mf2_dtf(locale: &str, options: &str, t: f64) -> Option<alloc::string::String>;
fn mf2_zone_offset(zone: &str, t: f64) -> Option<i32>;
}
#[allow(clippy::cast_precision_loss)]
fn js_ms(ms: i64) -> f64 {
ms as f64
}
pub(crate) fn zone_offset(zone: &str, epoch_ms: i64) -> Option<i32> {
mf2_zone_offset(zone, js_ms(epoch_ms))
}
#[cfg(feature = "datetime-intl")]
struct Json {
buf: [u8; 512],
len: usize,
ok: bool,
fields: u8,
}
#[cfg(feature = "datetime-intl")]
impl Json {
fn new() -> Json {
Json {
buf: [0; 512],
len: 0,
ok: true,
fields: 0,
}
}
fn raw(&mut self, s: &str) {
for &b in s.as_bytes() {
let Some(slot) = self.buf.get_mut(self.len) else {
self.ok = false;
return;
};
*slot = b;
self.len += 1;
}
}
fn key(&mut self, k: &str) {
self.raw(if self.fields == 0 { "{\"" } else { ",\"" });
self.fields = self.fields.saturating_add(1);
self.raw(k);
self.raw("\":");
}
fn text(&mut self, k: &str, v: &str) {
self.key(k);
self.raw("\"");
self.raw(v);
self.raw("\"");
}
fn finish(&mut self) -> Option<&str> {
self.raw(if self.fields == 0 { "{}" } else { "}" });
if !self.ok {
return None;
}
core::str::from_utf8(self.buf.get(..self.len)?).ok()
}
}
#[cfg(feature = "datetime-intl")]
fn offset_id(seconds: i32, out: &mut [u8; 6]) -> &str {
const D: &[u8; 10] = b"0123456789";
let a = seconds.unsigned_abs() / 60;
let (h, m) = (a / 60 % 100, a % 60);
let digit = |n: u32| D.get(n as usize % 10).copied().unwrap_or(b'0');
*out = [
if seconds < 0 { b'-' } else { b'+' },
digit(h / 10),
digit(h),
b':',
digit(m / 10),
digit(m),
];
core::str::from_utf8(out).unwrap_or("+00:00")
}
#[cfg(feature = "datetime-intl")]
fn options(r: &DateTimeRequest<'_>, j: &mut Json) -> bool {
let o = r.options;
let styleable = matches!(
o.date,
None | Some(DateStyle {
fields: DateFields::YearMonthDay,
..
})
) && matches!(
o.time,
None | Some(TimePrecision::Minute | TimePrecision::Second)
) && o.time_zone_style.is_none()
&& !(o.date.is_none() && o.hour12.is_some());
let length = o.date.map_or(DateLength::Medium, |d| d.length);
let len = match length {
DateLength::Long => "long",
DateLength::Medium => "medium",
DateLength::Short => "short",
_ => return false,
};
if styleable {
if o.date.is_some() {
j.text("dateStyle", len);
}
match o.time {
Some(TimePrecision::Minute) => j.text("timeStyle", "short"),
Some(TimePrecision::Second) => j.text("timeStyle", "medium"),
_ => {}
}
} else {
if let Some(d) = o.date {
let (year, month, day, weekday) = match d.fields {
DateFields::Weekday => (false, false, false, true),
DateFields::DayWeekday => (false, false, true, true),
DateFields::MonthDay => (false, true, true, false),
DateFields::MonthDayWeekday => (false, true, true, true),
DateFields::YearMonthDay => (true, true, true, false),
DateFields::YearMonthDayWeekday => (true, true, true, true),
_ => return false,
};
if year {
let v = if length == DateLength::Short {
"2-digit"
} else {
"numeric"
};
j.text("year", v);
}
if month {
let v = match length {
DateLength::Long => "long",
DateLength::Medium => "short",
DateLength::Short => "numeric",
_ => return false,
};
j.text("month", v);
}
if day {
j.text("day", "numeric");
}
if weekday {
let v = if length == DateLength::Long {
"long"
} else {
"short"
};
j.text("weekday", v);
}
}
if let Some(p) = o.time {
j.text("hour", "numeric");
if p != TimePrecision::Hour {
j.text("minute", "2-digit");
}
if p == TimePrecision::Second {
j.text("second", "2-digit");
}
}
if let Some(z) = o.time_zone_style {
let v = match z {
ZoneStyle::Long => "long",
ZoneStyle::Short => "short",
_ => return false,
};
j.text("timeZoneName", v);
}
}
if let Some(h12) = o.hour12 {
j.key("hour12");
j.raw(if h12 { "true" } else { "false" });
}
if let Some(cal) = o.calendar {
j.text("calendar", cal);
}
let mut id = [0u8; 6];
let zone = match r.zone {
ZoneOption::Offset(s) => offset_id(s, &mut id),
ZoneOption::Named(n) => n,
ZoneOption::Utc | ZoneOption::Input => "UTC",
_ => return false,
};
j.text("timeZone", zone);
true
}
#[cfg(feature = "datetime-intl")]
pub(crate) fn format(locale: &str, r: &DateTimeRequest<'_>, out: &mut dyn Sink) -> bool {
let mut j = Json::new();
if !options(r, &mut j) {
return false;
}
let Some(options) = j.finish() else {
return false;
};
match mf2_dtf(locale, options, js_ms(r.epoch_ms)) {
Some(text) => {
out.push_str(&text);
true
}
None => false,
}
}