use super::*;
enum UseGroupingResolved {
Bool(bool),
Str(&'static str),
}
fn is_unicode_type_value(s: &str) -> bool {
!s.is_empty()
&& s.split('-').all(|seg| {
(3..=8).contains(&seg.len()) && seg.bytes().all(|b| b.is_ascii_alphanumeric())
})
}
fn is_well_formed_currency(code: &str) -> bool {
code.len() == 3 && code.bytes().all(|b| b.is_ascii_alphabetic())
}
pub(crate) const SANCTIONED_UNITS: &[&str] = &[
"acre",
"bit",
"byte",
"celsius",
"centimeter",
"day",
"degree",
"fahrenheit",
"fluid-ounce",
"foot",
"gallon",
"gigabit",
"gigabyte",
"gram",
"hectare",
"hour",
"inch",
"kilobit",
"kilobyte",
"kilogram",
"kilometer",
"liter",
"megabit",
"megabyte",
"meter",
"microsecond",
"mile",
"mile-scandinavian",
"milliliter",
"millimeter",
"millisecond",
"minute",
"month",
"nanosecond",
"ounce",
"percent",
"petabyte",
"pound",
"second",
"stone",
"terabit",
"terabyte",
"week",
"yard",
"year",
];
fn is_well_formed_unit(unit: &str) -> bool {
let valid_single = |u: &str| SANCTIONED_UNITS.contains(&u);
match unit.split_once("-per-") {
Some((a, b)) => valid_single(a) && valid_single(b),
None => valid_single(unit),
}
}
pub(crate) fn canonicalize_locale_id(tag: &str) -> Option<String> {
if tag.is_empty() || !tag.is_ascii() || tag.contains('_') {
return None;
}
let parts: Vec<&str> = tag.split('-').collect();
if parts.iter().any(|p| p.is_empty()) {
return None;
}
let is_alpha = |s: &str| s.bytes().all(|b| b.is_ascii_alphabetic());
let is_digit = |s: &str| s.bytes().all(|b| b.is_ascii_digit());
let is_alnum = |s: &str| s.bytes().all(|b| b.is_ascii_alphanumeric());
let mut idx = 0usize;
let n = parts.len();
let lang = parts[idx];
if !((2..=3).contains(&lang.len()) || (5..=8).contains(&lang.len())) || !is_alpha(lang) {
return None;
}
let language = lang.to_ascii_lowercase();
idx += 1;
if idx < n && is_alpha(parts[idx]) && parts[idx].len() == 3 {
return None;
}
let mut script = None;
if idx < n && parts[idx].len() == 4 && is_alpha(parts[idx]) {
let s = parts[idx];
let mut t = String::new();
for (i, c) in s.chars().enumerate() {
if i == 0 {
t.push(c.to_ascii_uppercase());
} else {
t.push(c.to_ascii_lowercase());
}
}
script = Some(t);
idx += 1;
}
let mut region = None;
if idx < n
&& ((parts[idx].len() == 2 && is_alpha(parts[idx]))
|| (parts[idx].len() == 3 && is_digit(parts[idx])))
{
region = Some(parts[idx].to_ascii_uppercase());
idx += 1;
}
let mut variants: Vec<String> = Vec::new();
while idx < n {
let s = parts[idx];
let is_variant = ((5..=8).contains(&s.len()) && is_alnum(s))
|| (s.len() == 4 && s.as_bytes()[0].is_ascii_digit() && is_alnum(s));
if !is_variant {
break;
}
let v = s.to_ascii_lowercase();
if variants.contains(&v) {
return None; }
variants.push(v);
idx += 1;
}
variants.sort();
let mut extensions: Vec<(char, String)> = Vec::new();
let mut seen_singletons: Vec<char> = Vec::new();
while idx < n {
let sing = parts[idx];
if sing.len() != 1 || !sing.as_bytes()[0].is_ascii_alphanumeric() {
return None; }
let singleton = sing.as_bytes()[0].to_ascii_lowercase() as char;
if seen_singletons.contains(&singleton) {
return None; }
seen_singletons.push(singleton);
idx += 1;
let mut subs: Vec<String> = Vec::new();
let private = singleton == 'x';
while idx < n && (private || parts[idx].len() != 1) {
let st = parts[idx];
let min = if private { 1 } else { 2 };
if !((min..=8).contains(&st.len()) && is_alnum(st)) {
return None;
}
subs.push(st.to_ascii_lowercase());
idx += 1;
}
if subs.is_empty() {
return None;
}
let body = canonicalize_extension(singleton, &subs)?;
extensions.push((singleton, body));
}
extensions.sort_by_key(|(s, _)| (*s == 'x', *s));
let mut out = language;
if let Some(s) = script {
out.push('-');
out.push_str(&s);
}
if let Some(r) = region {
out.push('-');
out.push_str(&r);
}
for v in &variants {
out.push('-');
out.push_str(v);
}
for (_, body) in &extensions {
out.push('-');
out.push_str(body);
}
Some(out)
}
fn canonicalize_extension(singleton: char, subs: &[String]) -> Option<String> {
if singleton == 'u' {
let mut attributes: Vec<String> = Vec::new();
let mut i = 0;
let is_key = |s: &str| s.len() == 2 && s.as_bytes()[1].is_ascii_alphabetic();
let is_attr_or_type = |s: &str| (3..=8).contains(&s.len());
while i < subs.len() && !is_key(&subs[i]) {
if !is_attr_or_type(&subs[i]) {
return None;
}
attributes.push(subs[i].clone());
i += 1;
}
attributes.sort();
let mut keywords: Vec<(String, Vec<String>)> = Vec::new();
while i < subs.len() {
let key = subs[i].clone();
i += 1;
let mut vals: Vec<String> = Vec::new();
while i < subs.len() && !is_key(&subs[i]) {
if !is_attr_or_type(&subs[i]) {
return None;
}
vals.push(subs[i].clone());
i += 1;
}
if vals.len() == 1 && vals[0] == "true" {
vals.clear();
}
keywords.push((key, vals));
}
keywords.sort_by(|a, b| a.0.cmp(&b.0));
let mut body = String::from("u");
for a in &attributes {
body.push('-');
body.push_str(a);
}
for (k, vals) in &keywords {
body.push('-');
body.push_str(k);
for v in vals {
body.push('-');
body.push_str(v);
}
}
return Some(body);
}
if singleton == 't' {
let is_tkey = |s: &str| {
s.len() == 2
&& s.as_bytes()[0].is_ascii_alphabetic()
&& s.as_bytes()[1].is_ascii_digit()
};
let mut i = 0;
let mut tlang: Vec<String> = Vec::new();
while i < subs.len() && !is_tkey(&subs[i]) {
tlang.push(subs[i].clone());
i += 1;
}
let tlang_canon = if tlang.is_empty() {
None
} else {
Some(canonicalize_locale_id(&tlang.join("-"))?)
};
let mut fields: Vec<(String, Vec<String>)> = Vec::new();
while i < subs.len() {
let key = subs[i].clone();
i += 1;
let mut vals: Vec<String> = Vec::new();
while i < subs.len() && !is_tkey(&subs[i]) {
vals.push(subs[i].clone());
i += 1;
}
if vals.is_empty() {
return None; }
fields.push((key, vals));
}
fields.sort_by(|a, b| a.0.cmp(&b.0));
let mut body = String::from("t");
if let Some(tl) = &tlang_canon {
body.push('-');
body.push_str(&tl.to_ascii_lowercase());
}
for (k, vals) in &fields {
body.push('-');
body.push_str(k);
for v in vals {
body.push('-');
body.push_str(v);
}
}
return Some(body);
}
let mut body = String::new();
body.push(singleton);
for s in subs {
body.push('-');
body.push_str(s);
}
Some(body)
}
struct IntlService {
ctor_id: u16,
tag: &'static str,
marker: &'static str,
methods: &'static [&'static str],
bound_accessor: Option<(&'static str, &'static str)>,
}
const INTL_SERVICES: &[IntlService] = &[
IntlService {
ctor_id: N_INTL_NUMBER_FORMAT,
tag: "Intl.NumberFormat",
marker: "\u{0}brand_nf",
methods: &["resolvedOptions", "formatToParts"],
bound_accessor: Some(("format", "format")),
},
IntlService {
ctor_id: N_INTL_DATETIME_FORMAT,
tag: "Intl.DateTimeFormat",
marker: "\u{0}brand_dtf",
methods: &["resolvedOptions", "formatToParts"],
bound_accessor: Some(("format", "format")),
},
IntlService {
ctor_id: N_INTL_COLLATOR,
tag: "Intl.Collator",
marker: "\u{0}brand_col",
methods: &["resolvedOptions"],
bound_accessor: Some(("compare", "compare")),
},
IntlService {
ctor_id: N_INTL_PLURAL_RULES,
tag: "Intl.PluralRules",
marker: "\u{0}brand_pr",
methods: &["resolvedOptions", "select", "selectRange"],
bound_accessor: None,
},
IntlService {
ctor_id: N_INTL_LIST_FORMAT,
tag: "Intl.ListFormat",
marker: "\u{0}brand_lf",
methods: &["resolvedOptions", "format", "formatToParts"],
bound_accessor: None,
},
IntlService {
ctor_id: N_INTL_REL_TIME,
tag: "Intl.RelativeTimeFormat",
marker: "\u{0}brand_rtf",
methods: &["resolvedOptions", "format", "formatToParts"],
bound_accessor: None,
},
IntlService {
ctor_id: N_INTL_DISPLAY_NAMES,
tag: "Intl.DisplayNames",
marker: "\u{0}brand_dn",
methods: &["resolvedOptions", "of"],
bound_accessor: None,
},
IntlService {
ctor_id: N_INTL_SEGMENTER,
tag: "Intl.Segmenter",
marker: "\u{0}brand_seg",
methods: &["resolvedOptions", "segment"],
bound_accessor: None,
},
];
const LOCALE_ACCESSORS: &[&str] = &[
"baseName",
"calendar",
"caseFirst",
"collation",
"hourCycle",
"language",
"numberingSystem",
"numeric",
"region",
"script",
];
fn intl_method_arity(ctor_id: u16, name: &str) -> u32 {
match (ctor_id, name) {
(_, "resolvedOptions") => 0,
(N_INTL_REL_TIME, "format" | "formatToParts") => 2,
(N_INTL_PLURAL_RULES, "selectRange") => 2,
_ => 1,
}
}
impl<'a> Interp<'a> {
fn intl_underlying_native(name: &str) -> u16 {
match name {
"format" => N_INTL_FORMAT,
"resolvedOptions" => N_INTL_RESOLVED_OPTIONS,
"formatToParts" | "format_to_parts" => N_INTL_FORMAT_TO_PARTS,
"compare" => N_INTL_COMPARE,
"select" => N_INTL_PLURAL_SELECT,
"selectRange" => N_INTL_PLURAL_SELECT_RANGE,
"of" => N_INTL_DISPLAY_NAMES_OF,
"segment" => N_INTL_SEGMENTER_SEGMENT,
"list_format" => N_INTL_LIST_FORMAT_FORMAT,
"rel_format" => N_INTL_REL_TIME_FORMAT,
_ => 0,
}
}
pub(crate) fn install_intl_prototypes(&mut self) {
for svc in INTL_SERVICES {
self.intl_service_prototype(svc);
}
self.intl_locale_prototype();
self.intl_duration_prototype();
}
fn intl_namespace(&mut self) -> Option<Handle> {
self.current
.get("Intl")
.and_then(|v| v.as_handle())
.map(Handle::from_raw)
}
fn intl_ctor_handle(&mut self, ctor_name: &str) -> Option<Handle> {
let ns = self.intl_namespace()?;
self.realm
.get_property(ns, ctor_name)
.and_then(|v| v.as_handle())
.map(Handle::from_raw)
}
fn intl_service_prototype(&mut self, svc: &IntlService) -> Option<Handle> {
if let Some(p) = self.realm.intl_prototype(svc.ctor_id) {
return Some(p);
}
let ctor_name = match svc.ctor_id {
N_INTL_NUMBER_FORMAT => "NumberFormat",
N_INTL_DATETIME_FORMAT => "DateTimeFormat",
N_INTL_COLLATOR => "Collator",
N_INTL_PLURAL_RULES => "PluralRules",
N_INTL_LIST_FORMAT => "ListFormat",
N_INTL_REL_TIME => "RelativeTimeFormat",
N_INTL_DISPLAY_NAMES => "DisplayNames",
N_INTL_SEGMENTER => "Segmenter",
_ => return None,
};
let ctor = self.intl_ctor_handle(ctor_name)?;
let obj_proto = self.object_prototype();
let proto = self.realm.new_object_with_proto(obj_proto);
for &m in svc.methods {
let selector = match (svc.ctor_id, m) {
(N_INTL_LIST_FORMAT, "format") => "list_format",
(N_INTL_REL_TIME, "format") => "rel_format",
(N_INTL_LIST_FORMAT | N_INTL_REL_TIME, "formatToParts") => "format_to_parts",
_ => m,
};
let arity = intl_method_arity(svc.ctor_id, m);
let f = self.make_intl_proto_method(svc.marker, m, selector, arity);
self.realm
.set_property(proto, m, NanBox::handle(f.to_raw()));
self.realm.mark_hidden(proto, m);
}
if let Some((acc_name, selector)) = svc.bound_accessor {
let label = alloc::format!("get {acc_name}");
let marker_v = self.new_str(svc.marker);
let sel_v = self.new_str(selector);
let pair = self.realm.new_array(alloc::vec![marker_v, sel_v]);
let getter = self.realm.new_bound_native(N_INTL_BOUND_GETTER, pair);
self.install_fn_name_length(getter, &label, 0);
self.realm.define_accessor(
proto,
acc_name,
NanBox::handle(getter.to_raw()),
NanBox::undefined(),
);
self.realm.mark_hidden(proto, acc_name);
}
self.install_to_string_tag(proto, svc.tag);
self.realm
.set_hidden_property(proto, "constructor", NanBox::handle(ctor.to_raw()));
self.link_ctor_prototype(ctor, proto);
self.realm.set_intl_prototype(svc.ctor_id, proto);
Some(proto)
}
fn make_intl_proto_method(
&mut self,
marker: &str,
name: &str,
selector: &str,
arity: u32,
) -> Handle {
let marker_v = self.new_str(marker);
let sel_v = self.new_str(selector);
let pair = self.realm.new_array(alloc::vec![marker_v, sel_v]);
let f = self.realm.new_bound_native(N_INTL_PROTO_METHOD, pair);
self.install_fn_name_length(f, name, arity);
f
}
fn link_ctor_prototype(&mut self, ctor: Handle, proto: Handle) {
self.realm
.set_property(ctor, "prototype", NanBox::handle(proto.to_raw()));
self.realm.mark_hidden(ctor, "prototype");
self.realm.set_readonly_property(ctor, "prototype");
self.realm.set_non_configurable_property(ctor, "prototype");
}
fn brand_intl_instance(&mut self, obj: Handle, ctor_id: u16) {
let svc = INTL_SERVICES.iter().find(|s| s.ctor_id == ctor_id);
if let Some(svc) = svc {
self.realm
.set_hidden_property(obj, svc.marker, NanBox::boolean(true));
if let Some(proto) = self.intl_service_prototype(svc) {
self.realm.set_object_proto(obj, Some(proto));
}
}
}
pub(crate) fn require_intl_slot(
&mut self,
this: NanBox,
marker: &str,
what: &str,
) -> Result<Handle, ExecError> {
if let Some(h) = this.as_handle().map(Handle::from_raw)
&& self.realm.get_property(h, marker).is_some()
{
return Ok(h);
}
Err(self.type_error(&alloc::format!(
"{what} called on an object that is not a valid {what} receiver"
)))
}
pub(crate) fn intl_proto_method_dispatch(
&mut self,
this: NanBox,
target: Handle,
args: &[NanBox],
) -> Result<NanBox, ExecError> {
let pair = self
.realm
.array_elements(target)
.map(<[_]>::to_vec)
.unwrap_or_default();
let marker = pair
.first()
.and_then(|v| v.as_handle())
.map(Handle::from_raw)
.and_then(|h| self.realm.string_value(h))
.unwrap_or_default();
let selector = pair
.get(1)
.and_then(|v| v.as_handle())
.map(Handle::from_raw)
.and_then(|h| self.realm.string_value(h))
.unwrap_or_default();
self.require_intl_slot(this, &marker, "Intl method")?;
let id = Self::intl_underlying_native(&selector);
let saved = core::mem::replace(&mut self.this_val, this);
let r = self.call_native(id, args);
self.this_val = saved;
r
}
pub(crate) fn intl_bound_getter_dispatch(
&mut self,
this: NanBox,
target: Handle,
) -> Result<NanBox, ExecError> {
let pair = self
.realm
.array_elements(target)
.map(<[_]>::to_vec)
.unwrap_or_default();
let marker = pair
.first()
.and_then(|v| v.as_handle())
.map(Handle::from_raw)
.and_then(|h| self.realm.string_value(h))
.unwrap_or_default();
let selector = pair
.get(1)
.and_then(|v| v.as_handle())
.map(Handle::from_raw)
.and_then(|h| self.realm.string_value(h))
.unwrap_or_default();
let inst = self.require_intl_slot(this, &marker, "Intl bound-function getter")?;
let cache_key = alloc::format!("\u{0}bound_{selector}");
if let Some(v) = self.realm.get_property(inst, &cache_key) {
return Ok(v);
}
let inst_v = NanBox::handle(inst.to_raw());
let sel_v = self.new_str(&selector);
let bpair = self.realm.new_array(alloc::vec![inst_v, sel_v]);
let bound = self.realm.new_bound_native(N_INTL_BOUND_CALL, bpair);
self.install_fn_name_length(bound, "", 1);
let boundv = NanBox::handle(bound.to_raw());
self.realm.set_hidden_property(inst, &cache_key, boundv);
Ok(boundv)
}
pub(crate) fn intl_bound_call_dispatch(
&mut self,
target: Handle,
args: &[NanBox],
) -> Result<NanBox, ExecError> {
let pair = self
.realm
.array_elements(target)
.map(<[_]>::to_vec)
.unwrap_or_default();
let inst = pair
.first()
.and_then(|v| v.as_handle())
.map(Handle::from_raw);
let selector = pair
.get(1)
.and_then(|v| v.as_handle())
.map(Handle::from_raw)
.and_then(|h| self.realm.string_value(h))
.unwrap_or_default();
let Some(inst) = inst else {
return Ok(NanBox::undefined());
};
let arg0 = args.first().copied().unwrap_or(NanBox::undefined());
match selector.as_str() {
"compare" => {
let saved = core::mem::replace(&mut self.this_val, NanBox::handle(inst.to_raw()));
let r = self.call_native(N_INTL_COMPARE, args);
self.this_val = saved;
r
}
_ => {
let s = self.intl_format_value(inst, arg0);
Ok(self.new_str(&s))
}
}
}
pub(crate) fn make_intl_formatter(
&mut self,
id: u16,
args: &[NanBox],
) -> Result<NanBox, ExecError> {
let obj = self.realm.new_object();
let kind = if id == N_INTL_NUMBER_FORMAT {
"number"
} else {
"datetime"
};
let marker = self.new_str(kind);
self.realm.set_hidden_property(obj, "\u{0}intl", marker);
self.brand_intl_instance(obj, id);
let requested =
self.canonicalize_locale_list(args.first().copied().unwrap_or(NanBox::undefined()))?;
let locale = requested
.into_iter()
.next()
.unwrap_or_else(|| String::from("en-US"));
let locv = self.new_str(&locale);
self.realm.set_hidden_property(obj, "\u{0}locale", locv);
let opts_arg = args.get(1).copied().unwrap_or(NanBox::undefined());
let opts = if matches!(opts_arg.unpack(), Unpacked::Undefined) {
None
} else {
self.coerce_to_object(opts_arg)
.as_handle()
.map(Handle::from_raw)
};
if id == N_INTL_NUMBER_FORMAT {
self.init_number_format(obj, opts)?;
} else {
self.init_datetime_format(obj, opts)?;
}
Ok(NanBox::handle(obj.to_raw()))
}
pub(crate) fn get_string_option(
&mut self,
opts: Option<Handle>,
prop: &str,
values: &[&str],
default: Option<&str>,
) -> Result<Option<String>, ExecError> {
let raw = match opts {
Some(h) => self.read_member(h, prop)?,
None => NanBox::undefined(),
};
if matches!(raw.unpack(), Unpacked::Undefined) {
return Ok(default.map(String::from));
}
let s = self.coerce_to_string(raw)?;
if !values.is_empty() && !values.iter().any(|v| *v == s) {
let m = self.new_str(&alloc::format!("invalid value '{s}' for option {prop}"));
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
Ok(Some(s))
}
fn get_bool_option(
&mut self,
opts: Option<Handle>,
prop: &str,
default: Option<bool>,
) -> Result<Option<bool>, ExecError> {
let raw = match opts {
Some(h) => self.read_member(h, prop)?,
None => NanBox::undefined(),
};
if matches!(raw.unpack(), Unpacked::Undefined) {
return Ok(default);
}
Ok(Some(self.realm.truthy(raw)))
}
fn get_int_option(
&mut self,
opts: Option<Handle>,
prop: &str,
min: f64,
max: f64,
default: Option<f64>,
) -> Result<Option<f64>, ExecError> {
let raw = match opts {
Some(h) => self.read_member(h, prop)?,
None => NanBox::undefined(),
};
if matches!(raw.unpack(), Unpacked::Undefined) {
return Ok(default);
}
let nv = self.coerce_to_number(raw)?;
let n = self.realm.to_number(nv);
if n.is_nan() || n < min || n > max {
let m = self.new_str(&alloc::format!("value out of range for option {prop}"));
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
Ok(Some(trunc_toward_zero(n)))
}
fn store_str(&mut self, obj: Handle, key: &str, val: &Option<String>) {
if let Some(v) = val {
let sv = self.new_str(v);
self.realm.set_hidden_property(obj, key, sv);
}
}
fn init_number_format(&mut self, obj: Handle, opts: Option<Handle>) -> Result<(), ExecError> {
let _ = self.get_string_option(
opts,
"localeMatcher",
&["lookup", "best fit"],
Some("best fit"),
)?;
let nu = self.get_string_option(opts, "numberingSystem", &[], None)?;
if let Some(ns) = &nu {
if !is_unicode_type_value(ns) {
let m = self.new_str("invalid numberingSystem");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
}
self.store_str(obj, "numberingSystem", &nu);
let style = self
.get_string_option(
opts,
"style",
&["decimal", "percent", "currency", "unit"],
Some("decimal"),
)?
.unwrap();
let currency = self.get_string_option(opts, "currency", &[], None)?;
let currency_display = self.get_string_option(
opts,
"currencyDisplay",
&["code", "symbol", "narrowSymbol", "name"],
Some("symbol"),
)?;
let currency_sign = self.get_string_option(
opts,
"currencySign",
&["standard", "accounting"],
Some("standard"),
)?;
let unit = self.get_string_option(opts, "unit", &[], None)?;
let unit_display = self.get_string_option(
opts,
"unitDisplay",
&["short", "narrow", "long"],
Some("short"),
)?;
match ¤cy {
None if style == "currency" => {
let m = self.new_str("currency code is required with currency style");
return Err(ExecError::Throw(self.make_error(N_TYPE_ERROR, Some(m))));
}
Some(c) if !is_well_formed_currency(c) => {
let m = self.new_str("invalid currency code");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
_ => {}
}
match &unit {
None if style == "unit" => {
let m = self.new_str("unit is required with unit style");
return Err(ExecError::Throw(self.make_error(N_TYPE_ERROR, Some(m))));
}
Some(u) if !is_well_formed_unit(u) => {
let m = self.new_str("invalid unit");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
_ => {}
}
let style_s = Some(style.clone());
self.store_str(obj, "style", &style_s);
if style == "currency" {
let cc = currency.as_ref().map(|c| c.to_ascii_uppercase());
self.store_str(obj, "currency", &cc);
self.store_str(obj, "currencyDisplay", ¤cy_display);
self.store_str(obj, "currencySign", ¤cy_sign);
}
if style == "unit" {
self.store_str(obj, "unit", &unit);
self.store_str(obj, "unitDisplay", &unit_display);
}
let notation = self
.get_string_option(
opts,
"notation",
&["standard", "scientific", "engineering", "compact"],
Some("standard"),
)?
.unwrap();
self.set_number_format_digit_options(obj, opts)?;
let compact_display =
self.get_string_option(opts, "compactDisplay", &["short", "long"], Some("short"))?;
if notation == "compact" {
self.store_str(obj, "compactDisplay", &compact_display);
}
let ug_raw = match opts {
Some(h) => self.read_member(h, "useGrouping")?,
None => NanBox::undefined(),
};
let use_grouping_val = self.normalize_use_grouping(ug_raw)?;
let sign_display = self
.get_string_option(
opts,
"signDisplay",
&["auto", "never", "always", "exceptZero", "negative"],
Some("auto"),
)?
.unwrap();
self.store_str(obj, "notation", &Some(notation));
self.store_str(obj, "signDisplay", &Some(sign_display));
match use_grouping_val {
UseGroupingResolved::Bool(b) => {
self.realm
.set_hidden_property(obj, "useGrouping", NanBox::boolean(b));
}
UseGroupingResolved::Str(s) => {
let sv = self.new_str(s);
self.realm.set_hidden_property(obj, "useGrouping", sv);
}
}
Ok(())
}
fn set_number_format_digit_options(
&mut self,
obj: Handle,
opts: Option<Handle>,
) -> Result<(), ExecError> {
let mnid = self
.get_int_option(opts, "minimumIntegerDigits", 1.0, 21.0, Some(1.0))?
.unwrap();
let mnfd = self.get_int_option(opts, "minimumFractionDigits", 0.0, 100.0, None)?;
let mxfd = self.get_int_option(opts, "maximumFractionDigits", 0.0, 100.0, None)?;
let mnsd = self.get_int_option(opts, "minimumSignificantDigits", 1.0, 21.0, None)?;
let mxsd = self.get_int_option(opts, "maximumSignificantDigits", 1.0, 21.0, None)?;
if let (Some(a), Some(b)) = (mnfd, mxfd)
&& a > b
{
let m = self.new_str("minimumFractionDigits is greater than maximumFractionDigits");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
if let (Some(a), Some(b)) = (mnsd, mxsd)
&& a > b
{
let m =
self.new_str("minimumSignificantDigits is greater than maximumSignificantDigits");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
let rinc = self
.get_int_option(opts, "roundingIncrement", 1.0, 5000.0, Some(1.0))?
.unwrap();
const ALLOWED_INC: [u32; 15] = [
1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000,
];
if !ALLOWED_INC.contains(&(rinc as u32)) {
let m = self.new_str("invalid roundingIncrement");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
let rounding_mode = self
.get_string_option(
opts,
"roundingMode",
&[
"ceil",
"floor",
"expand",
"trunc",
"halfCeil",
"halfFloor",
"halfExpand",
"halfTrunc",
"halfEven",
],
Some("halfExpand"),
)?
.unwrap();
let rounding_priority = self
.get_string_option(
opts,
"roundingPriority",
&["auto", "morePrecision", "lessPrecision"],
Some("auto"),
)?
.unwrap();
let tzd = self
.get_string_option(
opts,
"trailingZeroDisplay",
&["auto", "stripIfInteger"],
Some("auto"),
)?
.unwrap();
self.realm
.set_hidden_property(obj, "minimumIntegerDigits", NanBox::number(mnid));
if let Some(v) = mnfd {
self.realm
.set_hidden_property(obj, "minimumFractionDigits", NanBox::number(v));
}
if let Some(v) = mxfd {
self.realm
.set_hidden_property(obj, "maximumFractionDigits", NanBox::number(v));
}
if let Some(v) = mnsd {
self.realm
.set_hidden_property(obj, "minimumSignificantDigits", NanBox::number(v));
}
if let Some(v) = mxsd {
self.realm
.set_hidden_property(obj, "maximumSignificantDigits", NanBox::number(v));
}
self.realm
.set_hidden_property(obj, "roundingIncrement", NanBox::number(rinc));
self.store_str(obj, "roundingMode", &Some(rounding_mode));
self.store_str(obj, "roundingPriority", &Some(rounding_priority));
self.store_str(obj, "trailingZeroDisplay", &Some(tzd));
Ok(())
}
fn normalize_use_grouping(&mut self, raw: NanBox) -> Result<UseGroupingResolved, ExecError> {
if matches!(raw.unpack(), Unpacked::Undefined) {
return Ok(UseGroupingResolved::Str("auto"));
}
if matches!(raw.unpack(), Unpacked::Bool(true)) {
return Ok(UseGroupingResolved::Str("always"));
}
if !self.realm.truthy(raw) {
return Ok(UseGroupingResolved::Bool(false));
}
let s = self.coerce_to_string(raw)?;
match s.as_str() {
"true" | "false" => Ok(UseGroupingResolved::Str("auto")),
"min2" => Ok(UseGroupingResolved::Str("min2")),
"auto" => Ok(UseGroupingResolved::Str("auto")),
"always" => Ok(UseGroupingResolved::Str("always")),
_ => {
let m = self.new_str(&alloc::format!("invalid useGrouping value '{s}'"));
Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))))
}
}
}
fn init_datetime_format(&mut self, obj: Handle, opts: Option<Handle>) -> Result<(), ExecError> {
let _ = self.get_string_option(
opts,
"localeMatcher",
&["lookup", "best fit"],
Some("best fit"),
)?;
let ca = self.get_string_option(opts, "calendar", &[], None)?;
if let Some(c) = &ca
&& !is_unicode_type_value(c)
{
let m = self.new_str("invalid calendar");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
self.store_str(obj, "calendar", &ca);
let nu = self.get_string_option(opts, "numberingSystem", &[], None)?;
if let Some(n) = &nu
&& !is_unicode_type_value(n)
{
let m = self.new_str("invalid numberingSystem");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
self.store_str(obj, "numberingSystem", &nu);
let hour12 = self.get_bool_option(opts, "hour12", None)?;
if let Some(b) = hour12 {
self.realm
.set_hidden_property(obj, "hour12", NanBox::boolean(b));
}
let hc = self.get_string_option(opts, "hourCycle", &["h11", "h12", "h23", "h24"], None)?;
self.store_str(obj, "hourCycle", &hc);
let _tz = self.get_string_option(opts, "timeZone", &[], None)?;
self.store_str(obj, "timeZone", &_tz);
let nv = ["numeric", "2-digit"];
let nm = ["long", "short", "narrow"];
let weekday = self.get_string_option(opts, "weekday", &nm, None)?;
self.store_str(obj, "weekday", &weekday);
let era = self.get_string_option(opts, "era", &nm, None)?;
self.store_str(obj, "era", &era);
let year = self.get_string_option(opts, "year", &nv, None)?;
self.store_str(obj, "year", &year);
let month = self.get_string_option(
opts,
"month",
&["numeric", "2-digit", "long", "short", "narrow"],
None,
)?;
self.store_str(obj, "month", &month);
let day = self.get_string_option(opts, "day", &nv, None)?;
self.store_str(obj, "day", &day);
let day_period = self.get_string_option(opts, "dayPeriod", &nm, None)?;
self.store_str(obj, "dayPeriod", &day_period);
let hour = self.get_string_option(opts, "hour", &nv, None)?;
self.store_str(obj, "hour", &hour);
let minute = self.get_string_option(opts, "minute", &nv, None)?;
self.store_str(obj, "minute", &minute);
let second = self.get_string_option(opts, "second", &nv, None)?;
self.store_str(obj, "second", &second);
let fsd = self.get_int_option(opts, "fractionalSecondDigits", 1.0, 3.0, None)?;
if let Some(v) = fsd {
self.realm
.set_hidden_property(obj, "fractionalSecondDigits", NanBox::number(v));
}
let tzn = self.get_string_option(
opts,
"timeZoneName",
&[
"long",
"short",
"shortOffset",
"longOffset",
"shortGeneric",
"longGeneric",
],
None,
)?;
self.store_str(obj, "timeZoneName", &tzn);
let _ = self.get_string_option(
opts,
"formatMatcher",
&["basic", "best fit"],
Some("best fit"),
)?;
let date_style = self.get_string_option(
opts,
"dateStyle",
&["full", "long", "medium", "short"],
None,
)?;
self.store_str(obj, "dateStyle", &date_style);
let time_style = self.get_string_option(
opts,
"timeStyle",
&["full", "long", "medium", "short"],
None,
)?;
self.store_str(obj, "timeStyle", &time_style);
if (date_style.is_some() || time_style.is_some())
&& (weekday.is_some()
|| era.is_some()
|| year.is_some()
|| month.is_some()
|| day.is_some()
|| hour.is_some()
|| minute.is_some()
|| second.is_some()
|| day_period.is_some()
|| fsd.is_some()
|| tzn.is_some())
{
let m = self.new_str("dateStyle/timeStyle may not be combined with component options");
return Err(ExecError::Throw(self.make_error(N_TYPE_ERROR, Some(m))));
}
Ok(())
}
pub(crate) fn intl_resolved_options(&mut self, fmt: Option<Handle>) -> NanBox {
let out = self.realm.new_object();
let kind = fmt
.and_then(|h| self.realm.get_property(h, "\u{0}intl"))
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("number"));
let get_str = |this: &Self, key: &str| -> Option<String> {
fmt.and_then(|h| this.realm.get_property(h, key))
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| this.realm.to_display_string(v))
};
let get_num = |this: &Self, key: &str| -> Option<f64> {
fmt.and_then(|h| this.realm.get_property(h, key))
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| this.realm.to_number(v))
};
let locale = get_str(self, "\u{0}locale").unwrap_or_else(|| String::from("en-US"));
let lv = self.new_str(&locale);
self.realm.set_property(out, "locale", lv);
if kind == "list" {
let lt = get_str(self, "type").unwrap_or_else(|| String::from("conjunction"));
let ltv = self.new_str(<);
self.realm.set_property(out, "type", ltv);
let style = get_str(self, "style").unwrap_or_else(|| String::from("long"));
let stv = self.new_str(&style);
self.realm.set_property(out, "style", stv);
} else if kind == "rtf" {
let style = get_str(self, "style").unwrap_or_else(|| String::from("long"));
let stv = self.new_str(&style);
self.realm.set_property(out, "style", stv);
let numeric = get_str(self, "numeric").unwrap_or_else(|| String::from("always"));
let nv = self.new_str(&numeric);
self.realm.set_property(out, "numeric", nv);
let ns = get_str(self, "numberingSystem").unwrap_or_else(|| String::from("latn"));
let nsv = self.new_str(&ns);
self.realm.set_property(out, "numberingSystem", nsv);
} else if kind == "plural" {
let pr_type = get_str(self, "type").unwrap_or_else(|| String::from("cardinal"));
let tv = self.new_str(&pr_type);
self.realm.set_property(out, "type", tv);
let notation = get_str(self, "notation").unwrap_or_else(|| String::from("standard"));
let nv = self.new_str(¬ation);
self.realm.set_property(out, "notation", nv);
if notation == "compact" {
let cd = get_str(self, "compactDisplay").unwrap_or_else(|| String::from("short"));
let cdv = self.new_str(&cd);
self.realm.set_property(out, "compactDisplay", cdv);
}
let mnid = get_num(self, "minimumIntegerDigits").unwrap_or(1.0);
self.realm
.set_property(out, "minimumIntegerDigits", NanBox::number(mnid));
let mnsd = get_num(self, "minimumSignificantDigits");
let mxsd = get_num(self, "maximumSignificantDigits");
let mnfd_opt = get_num(self, "minimumFractionDigits");
let mxfd_opt = get_num(self, "maximumFractionDigits");
let priority =
get_str(self, "roundingPriority").unwrap_or_else(|| String::from("auto"));
let has_sig = mnsd.is_some() || mxsd.is_some();
let report_frac = |this: &mut Self, out: Handle| {
let mnfd = mnfd_opt.unwrap_or(0.0);
let mxfd = mxfd_opt.unwrap_or_else(|| 3.0_f64.max(mnfd));
this.realm
.set_property(out, "minimumFractionDigits", NanBox::number(mnfd));
this.realm
.set_property(out, "maximumFractionDigits", NanBox::number(mxfd));
};
let report_sig = |this: &mut Self, out: Handle| {
let mnsd = mnsd.unwrap_or(1.0);
let mxsd = mxsd.unwrap_or(21.0);
this.realm
.set_property(out, "minimumSignificantDigits", NanBox::number(mnsd));
this.realm
.set_property(out, "maximumSignificantDigits", NanBox::number(mxsd));
};
if priority == "morePrecision" || priority == "lessPrecision" {
report_frac(self, out);
report_sig(self, out);
} else if has_sig {
report_sig(self, out);
} else {
report_frac(self, out);
}
let ordinal = pr_type == "ordinal";
let cats = self.plural_categories(&locale, ordinal);
let cat_vals: Vec<NanBox> = cats.iter().map(|c| self.new_str(c)).collect();
let arr = self.realm.new_array(cat_vals);
self.realm
.set_property(out, "pluralCategories", NanBox::handle(arr.to_raw()));
let rinc = get_num(self, "roundingIncrement").unwrap_or(1.0);
self.realm
.set_property(out, "roundingIncrement", NanBox::number(rinc));
let rm = get_str(self, "roundingMode").unwrap_or_else(|| String::from("halfExpand"));
let rmv = self.new_str(&rm);
self.realm.set_property(out, "roundingMode", rmv);
let rp = self.new_str(&priority);
self.realm.set_property(out, "roundingPriority", rp);
let tzd = get_str(self, "trailingZeroDisplay").unwrap_or_else(|| String::from("auto"));
let tzv = self.new_str(&tzd);
self.realm.set_property(out, "trailingZeroDisplay", tzv);
} else if kind == "number" {
let ns = get_str(self, "numberingSystem").unwrap_or_else(|| String::from("latn"));
let nsv = self.new_str(&ns);
self.realm.set_property(out, "numberingSystem", nsv);
let style = get_str(self, "style").unwrap_or_else(|| String::from("decimal"));
let sv = self.new_str(&style);
self.realm.set_property(out, "style", sv);
if style == "currency" {
if let Some(c) = get_str(self, "currency") {
let cv = self.new_str(&c);
self.realm.set_property(out, "currency", cv);
}
let cd = get_str(self, "currencyDisplay").unwrap_or_else(|| String::from("symbol"));
let cdv = self.new_str(&cd);
self.realm.set_property(out, "currencyDisplay", cdv);
let cs = get_str(self, "currencySign").unwrap_or_else(|| String::from("standard"));
let csv = self.new_str(&cs);
self.realm.set_property(out, "currencySign", csv);
}
if style == "unit" {
if let Some(u) = get_str(self, "unit") {
let uv = self.new_str(&u);
self.realm.set_property(out, "unit", uv);
}
let ud = get_str(self, "unitDisplay").unwrap_or_else(|| String::from("short"));
let udv = self.new_str(&ud);
self.realm.set_property(out, "unitDisplay", udv);
}
let mnid = get_num(self, "minimumIntegerDigits").unwrap_or(1.0);
self.realm
.set_property(out, "minimumIntegerDigits", NanBox::number(mnid));
let mnsd = get_num(self, "minimumSignificantDigits");
let mxsd = get_num(self, "maximumSignificantDigits");
let mnfd_opt = get_num(self, "minimumFractionDigits");
let mxfd_opt = get_num(self, "maximumFractionDigits");
let priority =
get_str(self, "roundingPriority").unwrap_or_else(|| String::from("auto"));
let has_sig = mnsd.is_some() || mxsd.is_some();
let (def_min, def_max): (f64, f64) = match style.as_str() {
"currency" => (2.0, 2.0),
"percent" => (0.0, 0.0),
_ => (0.0, 3.0),
};
let report_frac = |this: &mut Self, out: Handle| {
let mnfd = mnfd_opt.unwrap_or(def_min);
let mxfd = mxfd_opt.unwrap_or_else(|| def_max.max(mnfd));
this.realm
.set_property(out, "minimumFractionDigits", NanBox::number(mnfd));
this.realm
.set_property(out, "maximumFractionDigits", NanBox::number(mxfd));
};
let report_sig = |this: &mut Self, out: Handle| {
let mnsd = mnsd.unwrap_or(1.0);
let mxsd = mxsd.unwrap_or(21.0);
this.realm
.set_property(out, "minimumSignificantDigits", NanBox::number(mnsd));
this.realm
.set_property(out, "maximumSignificantDigits", NanBox::number(mxsd));
};
if priority == "morePrecision" || priority == "lessPrecision" {
report_frac(self, out);
report_sig(self, out);
} else if has_sig {
report_sig(self, out);
} else {
report_frac(self, out);
}
let ug = fmt
.and_then(|h| self.realm.get_property(h, "useGrouping"))
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.unwrap_or_else(|| self.new_str("auto"));
self.realm.set_property(out, "useGrouping", ug);
let notation = get_str(self, "notation").unwrap_or_else(|| String::from("standard"));
let nv = self.new_str(¬ation);
self.realm.set_property(out, "notation", nv);
if notation == "compact" {
let cd = get_str(self, "compactDisplay").unwrap_or_else(|| String::from("short"));
let cdv = self.new_str(&cd);
self.realm.set_property(out, "compactDisplay", cdv);
}
let sd = get_str(self, "signDisplay").unwrap_or_else(|| String::from("auto"));
let sdv = self.new_str(&sd);
self.realm.set_property(out, "signDisplay", sdv);
let rinc = get_num(self, "roundingIncrement").unwrap_or(1.0);
self.realm
.set_property(out, "roundingIncrement", NanBox::number(rinc));
let rm = get_str(self, "roundingMode").unwrap_or_else(|| String::from("halfExpand"));
let rmv = self.new_str(&rm);
self.realm.set_property(out, "roundingMode", rmv);
let rp = self.new_str(&priority);
self.realm.set_property(out, "roundingPriority", rp);
let tzd = get_str(self, "trailingZeroDisplay").unwrap_or_else(|| String::from("auto"));
let tzv = self.new_str(&tzd);
self.realm.set_property(out, "trailingZeroDisplay", tzv);
} else {
let ns = get_str(self, "numberingSystem").unwrap_or_else(|| String::from("latn"));
let nsv = self.new_str(&ns);
self.realm.set_property(out, "numberingSystem", nsv);
let cal = get_str(self, "calendar").unwrap_or_else(|| String::from("gregory"));
let cv = self.new_str(&cal);
self.realm.set_property(out, "calendar", cv);
let tz = get_str(self, "timeZone").unwrap_or_else(|| String::from("UTC"));
let tzv = self.new_str(&tz);
self.realm.set_property(out, "timeZone", tzv);
if let Some(hc) = get_str(self, "hourCycle") {
let v = self.new_str(&hc);
self.realm.set_property(out, "hourCycle", v);
let h12 = matches!(hc.as_str(), "h11" | "h12");
self.realm.set_property(out, "hour12", NanBox::boolean(h12));
} else if let Some(h) = fmt.and_then(|h| self.realm.get_property(h, "hour12")) {
self.realm.set_property(out, "hour12", h);
}
for key in [
"weekday",
"era",
"year",
"month",
"day",
"dayPeriod",
"hour",
"minute",
"second",
"timeZoneName",
"dateStyle",
"timeStyle",
] {
if let Some(v) = get_str(self, key) {
let vv = self.new_str(&v);
self.realm.set_property(out, key, vv);
}
}
if let Some(v) = get_num(self, "fractionalSecondDigits") {
self.realm
.set_property(out, "fractionalSecondDigits", NanBox::number(v));
}
}
NanBox::handle(out.to_raw())
}
pub(crate) fn number_to_locale_string(&self, n: f64, opts: Option<NanBox>) -> String {
let oh = match opts {
Some(v) if !matches!(v.unpack(), Unpacked::Undefined | Unpacked::Null) => {
match v.as_handle() {
Some(raw) => Handle::from_raw(raw),
None => return group_thousands(n),
}
}
_ => return group_thousands(n),
};
if !n.is_finite() {
return group_thousands(n);
}
let str_opt = |key: &str| -> Option<String> {
self.realm
.get_property(oh, key)
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
};
let num_opt = |key: &str| -> Option<i32> {
self.realm
.get_property(oh, key)
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_number(v) as i32)
};
let style = str_opt("style").unwrap_or_else(|| String::from("decimal"));
let (value, prefix, suffix, def_min, def_max) = match style.as_str() {
"percent" => (n * 100.0, String::new(), String::from("%"), 0, 0),
"currency" => {
let sym = currency_symbol(&str_opt("currency").unwrap_or_default());
(n, sym, String::new(), 2, 2)
}
_ => (n, String::new(), String::new(), 0, 3),
};
let min_frac = num_opt("minimumFractionDigits")
.unwrap_or(def_min)
.clamp(0, 100);
let max_frac = num_opt("maximumFractionDigits")
.unwrap_or(def_max.max(min_frac))
.clamp(min_frac, 100);
let neg = value.is_sign_negative() && value != 0.0;
let formatted = alloc::format!("{:.*}", max_frac as usize, value.abs());
let trimmed = if max_frac > min_frac && formatted.contains('.') {
let dot = formatted.find('.').unwrap();
let keep_min = dot + 1 + min_frac as usize;
let mut end = formatted.len();
while end > keep_min && formatted.as_bytes()[end - 1] == b'0' {
end -= 1;
}
if end == dot + 1 {
end = dot; }
String::from(&formatted[..end])
} else {
formatted
};
let grouped = group_thousands_str(&trimmed);
let mut out = String::new();
if neg {
out.push('-');
}
out.push_str(&prefix);
out.push_str(&grouped);
out.push_str(&suffix);
out
}
pub(crate) fn intl_format_value(&mut self, handle: Handle, value: NanBox) -> String {
let kind = self
.realm
.get_property(handle, "\u{0}intl")
.map(|k| self.realm.to_display_string(k))
.unwrap_or_default();
if kind == "datetime" {
let ms = match value.as_handle().map(Handle::from_raw) {
Some(h) if self.realm.date_at(h).is_some() => self.realm.date_at(h).unwrap(),
_ => self.realm.to_number(value),
};
self.format_intl_datetime(handle, ms)
} else {
let n = self.realm.to_number(value);
self.intl_format_number(handle, n)
}
}
pub(crate) fn make_relative_time_format(
&mut self,
args: &[NanBox],
) -> Result<NanBox, ExecError> {
let obj = self.realm.new_object();
let marker = self.new_str("rtf");
self.realm.set_hidden_property(obj, "\u{0}intl", marker);
let requested =
self.canonicalize_locale_list(args.first().copied().unwrap_or(NanBox::undefined()))?;
let locale = requested
.into_iter()
.next()
.unwrap_or_else(|| String::from("en-US"));
let locv = self.new_str(&locale);
self.realm.set_hidden_property(obj, "\u{0}locale", locv);
let opts_arg = args.get(1).copied().unwrap_or(NanBox::undefined());
let opts = match opts_arg.unpack() {
Unpacked::Undefined => None,
Unpacked::Null => {
return Err(self.type_error("Intl.RelativeTimeFormat options cannot be null"));
}
_ => self
.coerce_to_object(opts_arg)
.as_handle()
.map(Handle::from_raw),
};
let _ = self.get_string_option(
opts,
"localeMatcher",
&["lookup", "best fit"],
Some("best fit"),
)?;
let nu = self.get_string_option(opts, "numberingSystem", &[], None)?;
if let Some(ns) = &nu {
if !is_unicode_type_value(ns) {
let m = self.new_str("invalid numberingSystem");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
}
let style = self
.get_string_option(opts, "style", &["long", "short", "narrow"], Some("long"))?
.unwrap();
let numeric = self
.get_string_option(opts, "numeric", &["always", "auto"], Some("always"))?
.unwrap();
self.store_str(obj, "style", &Some(style));
self.store_str(obj, "numeric", &Some(numeric));
self.brand_intl_instance(obj, N_INTL_REL_TIME);
Ok(NanBox::handle(obj.to_raw()))
}
pub(crate) fn rel_time_numeric_style(&mut self, fmt: Option<Handle>) -> (String, String) {
let numeric = fmt
.and_then(|h| self.realm.get_property(h, "numeric"))
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("always"));
let style = fmt
.and_then(|h| self.realm.get_property(h, "style"))
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("long"));
(numeric, style)
}
pub(crate) fn singular_relative_time_unit(
&mut self,
unit: NanBox,
) -> Result<String, ExecError> {
let s = self.coerce_to_string(unit)?;
let singular = match s.as_str() {
"seconds" | "second" => "second",
"minutes" | "minute" => "minute",
"hours" | "hour" => "hour",
"days" | "day" => "day",
"weeks" | "week" => "week",
"months" | "month" => "month",
"quarters" | "quarter" => "quarter",
"years" | "year" => "year",
_ => {
let m = self.new_str(&alloc::format!("invalid relative time unit '{s}'"));
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
};
Ok(String::from(singular))
}
pub(crate) fn make_display_names(&mut self, args: &[NanBox]) -> NanBox {
let obj = self.realm.new_object();
let locale = args
.first()
.and_then(|v| v.as_handle())
.map(Handle::from_raw)
.and_then(|h| self.realm.string_value(h))
.unwrap_or_else(|| String::from("en"));
let locv = self.new_str(&locale);
self.realm.set_hidden_property(obj, "\u{0}locale", locv);
if let Some(opts) = args
.get(1)
.and_then(|v| v.as_handle())
.map(Handle::from_raw)
{
for key in ["type", "style", "fallback"] {
if let Some(v) = self.realm.get_property(opts, key) {
self.realm.set_hidden_property(obj, key, v);
}
}
}
self.brand_intl_instance(obj, N_INTL_DISPLAY_NAMES);
NanBox::handle(obj.to_raw())
}
pub(crate) fn make_collator(&mut self, args: &[NanBox]) -> NanBox {
let obj = self.realm.new_object();
let locale = args
.first()
.and_then(|v| v.as_handle())
.map(Handle::from_raw)
.and_then(|h| self.realm.string_value(h))
.unwrap_or_else(|| String::from("en"));
let locv = self.new_str(&locale);
self.realm.set_hidden_property(obj, "\u{0}locale", locv);
if let Some(opts) = args
.get(1)
.and_then(|v| v.as_handle())
.map(Handle::from_raw)
{
for key in ["sensitivity", "numeric", "caseFirst"] {
if let Some(v) = self.realm.get_property(opts, key) {
self.realm.set_hidden_property(obj, key, v);
}
}
}
self.brand_intl_instance(obj, N_INTL_COLLATOR);
NanBox::handle(obj.to_raw())
}
pub(crate) fn make_list_format(&mut self, args: &[NanBox]) -> Result<NanBox, ExecError> {
let obj = self.realm.new_object();
let marker = self.new_str("list");
self.realm.set_hidden_property(obj, "\u{0}intl", marker);
let requested =
self.canonicalize_locale_list(args.first().copied().unwrap_or(NanBox::undefined()))?;
let locale = requested
.into_iter()
.next()
.unwrap_or_else(|| String::from("en-US"));
let locv = self.new_str(&locale);
self.realm.set_hidden_property(obj, "\u{0}locale", locv);
let opts_arg = args.get(1).copied().unwrap_or(NanBox::undefined());
let opts = if matches!(opts_arg.unpack(), Unpacked::Undefined) {
None
} else if self.is_object_value(opts_arg) {
opts_arg.as_handle().map(Handle::from_raw)
} else {
return Err(self.type_error("Intl.ListFormat options must be an object"));
};
let _ = self.get_string_option(
opts,
"localeMatcher",
&["lookup", "best fit"],
Some("best fit"),
)?;
let list_type = self
.get_string_option(
opts,
"type",
&["conjunction", "disjunction", "unit"],
Some("conjunction"),
)?
.unwrap();
let style = self
.get_string_option(opts, "style", &["long", "short", "narrow"], Some("long"))?
.unwrap();
self.store_str(obj, "type", &Some(list_type));
self.store_str(obj, "style", &Some(style));
self.brand_intl_instance(obj, N_INTL_LIST_FORMAT);
Ok(NanBox::handle(obj.to_raw()))
}
pub(crate) fn string_list_from_iterable(
&mut self,
iterable: NanBox,
) -> Result<Vec<String>, ExecError> {
if matches!(iterable.unpack(), Unpacked::Undefined) {
return Ok(Vec::new());
}
if let Some(ih) = self.for_of_get_iterator(iterable)? {
let mut out = Vec::new();
loop {
let next_fn = self.read_member(ih, "next")?;
let res = self.call_with_this(next_fn, NanBox::handle(ih.to_raw()), &[])?;
let Some(rh) = res.as_handle().map(Handle::from_raw) else {
return Err(self.type_error("iterator result is not an object"));
};
let done = self.read_member(rh, "done")?;
if self.realm.truthy(done) {
break;
}
let value = self.read_member(rh, "value")?;
match value.as_handle().map(Handle::from_raw) {
Some(vh) if self.realm.type_of(vh) == Some("string") => {
out.push(self.realm.string_value(vh).unwrap_or_default());
}
_ => {
let _ = self.iterator_close(ih);
return Err(
self.type_error("Intl.ListFormat: list elements must all be strings")
);
}
}
if out.len() > GEN_CAP {
return Err(self.type_error("iterator did not terminate"));
}
}
return Ok(out);
}
let elems = self.iterate_values(iterable)?;
let mut out = Vec::with_capacity(elems.len());
for e in elems {
match e.as_handle().map(Handle::from_raw) {
Some(vh) if self.realm.type_of(vh) == Some("string") => {
out.push(self.realm.string_value(vh).unwrap_or_default());
}
_ => {
return Err(
self.type_error("Intl.ListFormat: list elements must all be strings")
);
}
}
}
Ok(out)
}
pub(crate) fn list_format_type_style(&self, fmt: Option<Handle>) -> (String, String) {
let get = |key: &str, dflt: &str| -> String {
fmt.and_then(|h| self.realm.get_property(h, key))
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from(dflt))
};
(get("type", "conjunction"), get("style", "long"))
}
pub(crate) fn list_format_parts(
&self,
items: &[String],
list_type: &str,
_style: &str,
) -> Vec<(&'static str, String)> {
let n = items.len();
let mut parts: Vec<(&'static str, String)> = Vec::new();
if n == 0 {
return parts;
}
let (pair, end): (&str, &str) = match list_type {
"disjunction" => (" or ", ", or "),
"unit" => (", ", ", "),
_ => (" and ", ", and "), };
let middle = ", ";
for (i, it) in items.iter().enumerate() {
if i > 0 {
let lit = if n == 2 {
pair
} else if i == n - 1 {
end
} else {
middle
};
parts.push(("literal", String::from(lit)));
}
parts.push(("element", it.clone()));
}
parts
}
pub(crate) fn make_plural_rules(&mut self, args: &[NanBox]) -> Result<NanBox, ExecError> {
let obj = self.realm.new_object();
let kindv = self.new_str("plural");
self.realm.set_hidden_property(obj, "\u{0}intl", kindv);
self.brand_intl_instance(obj, N_INTL_PLURAL_RULES);
let requested =
self.canonicalize_locale_list(args.first().copied().unwrap_or(NanBox::undefined()))?;
let locale = requested
.into_iter()
.next()
.unwrap_or_else(|| String::from("en"));
let locv = self.new_str(&locale);
self.realm.set_hidden_property(obj, "\u{0}locale", locv);
let opts_arg = args.get(1).copied().unwrap_or(NanBox::undefined());
let opts = if matches!(opts_arg.unpack(), Unpacked::Undefined) {
None
} else if self.is_object_value(opts_arg) {
opts_arg.as_handle().map(Handle::from_raw)
} else {
return Err(self.type_error("Intl.PluralRules options must be an object"));
};
let _ = self.get_string_option(
opts,
"localeMatcher",
&["lookup", "best fit"],
Some("best fit"),
)?;
let pr_type = self
.get_string_option(opts, "type", &["cardinal", "ordinal"], Some("cardinal"))?
.unwrap();
self.store_str(obj, "type", &Some(pr_type));
let notation = self
.get_string_option(
opts,
"notation",
&["standard", "compact", "scientific", "engineering"],
Some("standard"),
)?
.unwrap();
let compact_display =
self.get_string_option(opts, "compactDisplay", &["short", "long"], Some("short"))?;
if notation == "compact" {
self.store_str(obj, "compactDisplay", &compact_display);
}
self.store_str(obj, "notation", &Some(notation));
self.set_number_format_digit_options(obj, opts)?;
Ok(NanBox::handle(obj.to_raw()))
}
pub(crate) fn plural_select_category(&mut self, n: f64) -> &'static str {
if !n.is_finite() {
return "other";
}
#[cfg(feature = "intl")]
{
let fmt = self.this_val.as_handle().map(Handle::from_raw);
let locale = fmt
.and_then(|h| self.realm.get_property(h, "\u{0}locale"))
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("en"));
let ordinal = fmt
.and_then(|h| self.realm.get_property(h, "type"))
.map(|v| self.realm.to_display_string(v))
.as_deref()
== Some("ordinal");
let ops = if n == (n as i64) as f64 {
intl::plural::PluralOperands::from_int(n as i64)
} else {
intl::plural::PluralOperands::parse(&alloc::format!("{n}"))
.unwrap_or_else(|| intl::plural::PluralOperands::from_int(n as i64))
};
let cat = if ordinal {
intl::plural::ordinal_category(&locale, &ops)
} else {
intl::plural::plural_category(&locale, &ops)
};
use intl::plural::PluralCategory::*;
match cat {
Zero => "zero",
One => "one",
Two => "two",
Few => "few",
Many => "many",
Other => "other",
}
}
#[cfg(not(feature = "intl"))]
{
if n == 1.0 { "one" } else { "other" }
}
}
pub(crate) fn plural_categories(&mut self, locale: &str, ordinal: bool) -> Vec<&'static str> {
const ORDER: [&str; 6] = ["zero", "one", "two", "few", "many", "other"];
#[cfg(feature = "intl")]
{
use intl::plural::PluralCategory::*;
let name = |c: intl::plural::PluralCategory| -> &'static str {
match c {
Zero => "zero",
One => "one",
Two => "two",
Few => "few",
Many => "many",
Other => "other",
}
};
let mut seen: Vec<&'static str> = Vec::new();
let mut push = |this: &mut Self, s: &str| {
if let Some(ops) = intl::plural::PluralOperands::parse(s) {
let cat = if ordinal {
intl::plural::ordinal_category(locale, &ops)
} else {
intl::plural::plural_category(locale, &ops)
};
let _ = this;
let nm = name(cat);
if !seen.contains(&nm) {
seen.push(nm);
}
}
};
for i in 0..=200u32 {
let s = alloc::format!("{i}");
push(self, &s);
}
for s in ["0.0", "0.1", "1.5", "2.5", "1000000", "1000000.0"] {
push(self, s);
}
ORDER.iter().copied().filter(|c| seen.contains(c)).collect()
}
#[cfg(not(feature = "intl"))]
{
let _ = (locale, ordinal);
let _ = ORDER;
alloc::vec!["one", "other"]
}
}
pub(crate) fn make_segmenter(&mut self, args: &[NanBox]) -> NanBox {
let obj = self.realm.new_object();
if let Some(opts) = args
.get(1)
.and_then(|v| v.as_handle())
.map(Handle::from_raw)
&& let Some(v) = self.realm.get_property(opts, "granularity")
{
self.realm.set_hidden_property(obj, "granularity", v);
}
self.brand_intl_instance(obj, N_INTL_SEGMENTER);
NanBox::handle(obj.to_raw())
}
#[cfg(feature = "intl")]
pub(crate) fn datetime_parts(
&mut self,
handle: Handle,
ms: f64,
) -> Vec<(&'static str, String)> {
use intl::datetime::{
self, DateStyle, DateTime, DateTimeFormatOptions, HourCycle, MonthStyle, NameStyle,
Numeric2Digit, TimeZoneNameStyle,
};
let opt = |this: &mut Self, k: &str| -> Option<String> {
this.realm
.get_property(handle, k)
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| this.realm.to_display_string(v))
};
let locale = opt(self, "\u{0}locale").unwrap_or_else(|| String::from("en"));
let msi = ms as i64;
let day = msi.div_euclid(86_400_000);
let tod = msi.rem_euclid(86_400_000);
let (y, mo, d) = crate::realm::civil_from_days(day);
let dt = DateTime {
year: y as i32,
month: mo as u8,
day: d as u8,
hour: (tod / 3_600_000) as u8,
minute: ((tod / 60_000) % 60) as u8,
second: ((tod / 1_000) % 60) as u8,
millisecond: (tod % 1_000) as u16,
};
let name = |s: &str| match s {
"long" => Some(NameStyle::Long),
"short" => Some(NameStyle::Short),
"narrow" => Some(NameStyle::Narrow),
_ => None,
};
let n2 = |s: &str| match s {
"numeric" => Some(Numeric2Digit::Numeric),
"2-digit" => Some(Numeric2Digit::TwoDigit),
_ => None,
};
let dstyle = |s: &str| match s {
"full" => Some(DateStyle::Full),
"long" => Some(DateStyle::Long),
"medium" => Some(DateStyle::Medium),
"short" => Some(DateStyle::Short),
_ => None,
};
let mut o = DateTimeFormatOptions::default();
if opt(self, "dateStyle").is_some() || opt(self, "timeStyle").is_some() {
o.date_style = opt(self, "dateStyle").as_deref().and_then(dstyle);
o.time_style = opt(self, "timeStyle").as_deref().and_then(dstyle);
} else {
o.weekday = opt(self, "weekday").as_deref().and_then(name);
o.era = opt(self, "era").as_deref().and_then(name);
o.year = opt(self, "year").as_deref().and_then(n2);
o.month = opt(self, "month").as_deref().and_then(|s| match s {
"numeric" => Some(MonthStyle::Numeric),
"2-digit" => Some(MonthStyle::TwoDigit),
"long" => Some(MonthStyle::Long),
"short" => Some(MonthStyle::Short),
"narrow" => Some(MonthStyle::Narrow),
_ => None,
});
o.day = opt(self, "day").as_deref().and_then(n2);
o.hour = opt(self, "hour").as_deref().and_then(n2);
o.minute = opt(self, "minute").as_deref().and_then(n2);
o.second = opt(self, "second").as_deref().and_then(n2);
o.day_period = opt(self, "dayPeriod").as_deref().and_then(name);
o.fractional_second_digits =
opt(self, "fractionalSecondDigits").and_then(|s| s.parse().ok());
if o.weekday.is_none()
&& o.era.is_none()
&& o.year.is_none()
&& o.month.is_none()
&& o.day.is_none()
&& o.hour.is_none()
&& o.minute.is_none()
&& o.second.is_none()
{
o.year = Some(Numeric2Digit::Numeric);
o.month = Some(MonthStyle::Numeric);
o.day = Some(Numeric2Digit::Numeric);
}
}
o.hour12 = self
.realm
.get_property(handle, "hour12")
.and_then(|v| match v.unpack() {
Unpacked::Bool(b) => Some(b),
_ => None,
});
o.hour_cycle = opt(self, "hourCycle").as_deref().and_then(|s| match s {
"h11" => Some(HourCycle::H11),
"h12" => Some(HourCycle::H12),
"h23" => Some(HourCycle::H23),
"h24" => Some(HourCycle::H24),
_ => None,
});
if let Some(tzn) = opt(self, "timeZoneName") {
o.time_zone_name = match tzn.as_str() {
"long" => Some(TimeZoneNameStyle::Long),
"short" => Some(TimeZoneNameStyle::Short),
"shortOffset" => Some(TimeZoneNameStyle::ShortOffset),
"longOffset" => Some(TimeZoneNameStyle::LongOffset),
"shortGeneric" => Some(TimeZoneNameStyle::ShortGeneric),
"longGeneric" => Some(TimeZoneNameStyle::LongGeneric),
_ => None,
};
o.tz_offset_minutes = Some(0); }
match datetime::format_to_parts(&locale, &dt, &o) {
Ok(parts) => parts
.into_iter()
.map(|p| (p.kind.as_str(), p.value))
.collect(),
Err(_) => Vec::new(),
}
}
#[cfg(not(feature = "intl"))]
pub(crate) fn datetime_parts(
&mut self,
handle: Handle,
ms: f64,
) -> Vec<(&'static str, String)> {
let opt = |this: &mut Self, k: &str| -> Option<String> {
this.realm
.get_property(handle, k)
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| this.realm.to_display_string(v))
};
let msi = ms as i64;
let day = msi.div_euclid(86_400_000);
let tod = msi.rem_euclid(86_400_000);
let (y, mo, d) = crate::realm::civil_from_days(day);
let (mo, d) = (i64::from(mo), i64::from(d));
let wd_idx = (day + 4).rem_euclid(7) as usize; let hour24 = tod / 3_600_000;
let minute = (tod / 60_000) % 60;
let second = (tod / 1_000) % 60;
const MONTHS: [&str; 12] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const WEEKDAYS: [&str; 7] = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
let two = |v: i64| alloc::format!("{v:02}");
let bare = |v: i64| alloc::format!("{v}");
let mut weekday = opt(self, "weekday");
let mut year = opt(self, "year");
let mut month = opt(self, "month");
let mut day_o = opt(self, "day");
let mut hour = opt(self, "hour");
let mut minute_o = opt(self, "minute");
let mut second_o = opt(self, "second");
match opt(self, "dateStyle").as_deref() {
Some("full") => {
weekday = Some(String::from("long"));
year = Some(String::from("numeric"));
month = Some(String::from("long"));
day_o = Some(String::from("numeric"));
}
Some("long") => {
year = Some(String::from("numeric"));
month = Some(String::from("long"));
day_o = Some(String::from("numeric"));
}
Some("medium") => {
year = Some(String::from("numeric"));
month = Some(String::from("short"));
day_o = Some(String::from("numeric"));
}
Some("short") => {
year = Some(String::from("2-digit"));
month = Some(String::from("numeric"));
day_o = Some(String::from("numeric"));
}
_ => {}
}
match opt(self, "timeStyle").as_deref() {
Some("full" | "long" | "medium") => {
hour = Some(String::from("numeric"));
minute_o = Some(String::from("2-digit"));
second_o = Some(String::from("2-digit"));
}
Some("short") => {
hour = Some(String::from("numeric"));
minute_o = Some(String::from("2-digit"));
}
_ => {}
}
if weekday.is_none()
&& year.is_none()
&& month.is_none()
&& day_o.is_none()
&& hour.is_none()
&& minute_o.is_none()
&& second_o.is_none()
{
year = Some(String::from("numeric"));
month = Some(String::from("numeric"));
day_o = Some(String::from("numeric"));
}
let year_str = |style: &str| -> String {
if style == "2-digit" {
two(y.rem_euclid(100))
} else {
bare(y)
}
};
let named_month = matches!(month.as_deref(), Some("long" | "short" | "narrow"));
let lit = |s: &str| ("literal", String::from(s));
let mut date: Vec<(&'static str, String)> = Vec::new();
if let Some(ws) = &weekday {
let name = WEEKDAYS[wd_idx];
date.push((
"weekday",
String::from(if ws == "long" { name } else { &name[..3] }),
));
}
if named_month {
if !date.is_empty() {
date.push(lit(", "));
}
if let Some(m) = &month {
let name = MONTHS[(mo as usize).saturating_sub(1).min(11)];
date.push((
"month",
String::from(if m == "long" { name } else { &name[..3] }),
));
}
if let Some(ds) = &day_o {
date.push(lit(" "));
date.push(("day", if ds == "2-digit" { two(d) } else { bare(d) }));
}
if let Some(ys) = &year {
date.push(lit(if day_o.is_some() { ", " } else { " " }));
date.push(("year", year_str(ys)));
}
} else {
if !date.is_empty() && (month.is_some() || day_o.is_some() || year.is_some()) {
date.push(lit(", "));
}
let mut first = true;
if let Some(m) = &month {
date.push(("month", if m == "2-digit" { two(mo) } else { bare(mo) }));
first = false;
}
if let Some(ds) = &day_o {
if !first {
date.push(lit("/"));
}
date.push(("day", if ds == "2-digit" { two(d) } else { bare(d) }));
first = false;
}
if let Some(ys) = &year {
if !first {
date.push(lit("/"));
}
date.push(("year", year_str(ys)));
}
}
if opt(self, "era").is_some() {
if !date.is_empty() {
date.push(lit(" "));
}
date.push(("era", String::from(if y > 0 { "AD" } else { "BC" })));
}
let mut time: Vec<(&'static str, String)> = Vec::new();
if hour.is_some() || minute_o.is_some() || second_o.is_some() {
let h12 = !matches!(
self.realm.get_property(handle, "hour12"),
Some(v) if matches!(v.unpack(), Unpacked::Bool(false))
);
let h = if h12 {
let m = hour24 % 12;
if m == 0 { 12 } else { m }
} else {
hour24
};
time.push((
"hour",
if hour.as_deref() == Some("2-digit") {
two(h)
} else {
bare(h)
},
));
if minute_o.is_some() {
time.push(lit(":"));
time.push(("minute", two(minute)));
}
if second_o.is_some() {
time.push(lit(":"));
time.push(("second", two(second)));
}
if h12 {
time.push(lit("\u{202f}"));
time.push((
"dayPeriod",
String::from(if hour24 < 12 { "AM" } else { "PM" }),
));
}
}
let mut parts = date;
if !parts.is_empty() && !time.is_empty() {
let _ = named_month;
parts.push(lit(", "));
}
parts.extend(time);
parts
}
pub(crate) fn format_intl_datetime(&mut self, handle: Handle, ms: f64) -> String {
let mut s = String::new();
for (_, v) in self.datetime_parts(handle, ms) {
s.push_str(&v);
}
s
}
#[cfg(feature = "intl")]
pub(crate) fn number_format_options(
&mut self,
handle: Handle,
) -> intl::number::NumberFormatOptions {
use intl::number::{
CompactDisplay, CurrencyDisplay, Notation, NumberFormatOptions, NumberStyle,
RoundingMode, SignDisplay, UnitDisplay, UseGrouping,
};
let opt_str = |this: &mut Self, k: &str| -> Option<String> {
this.realm
.get_property(handle, k)
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| this.realm.to_display_string(v))
};
let opt_num = |this: &mut Self, k: &str| -> Option<u8> {
this.realm
.get_property(handle, k)
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| this.realm.to_number(v) as u8)
};
let mut o = NumberFormatOptions::default();
o.style = match opt_str(self, "style").as_deref() {
Some("percent") => NumberStyle::Percent,
Some("currency") => NumberStyle::Currency,
Some("unit") => NumberStyle::Unit,
_ => NumberStyle::Decimal,
};
o.notation = match opt_str(self, "notation").as_deref() {
Some("scientific") => Notation::Scientific,
Some("engineering") => Notation::Engineering,
Some("compact") => Notation::Compact,
_ => Notation::Standard,
};
o.compact_display = match opt_str(self, "compactDisplay").as_deref() {
Some("long") => CompactDisplay::Long,
_ => CompactDisplay::Short,
};
o.sign_display = match opt_str(self, "signDisplay").as_deref() {
Some("always") => SignDisplay::Always,
Some("exceptZero") => SignDisplay::ExceptZero,
Some("negative") => SignDisplay::Negative,
Some("never") => SignDisplay::Never,
_ => SignDisplay::Auto,
};
o.currency_display = match opt_str(self, "currencyDisplay").as_deref() {
Some("code") => CurrencyDisplay::Code,
Some("name") => CurrencyDisplay::Name,
Some("narrowSymbol") => CurrencyDisplay::NarrowSymbol,
_ => CurrencyDisplay::Symbol,
};
o.unit_display = match opt_str(self, "unitDisplay").as_deref() {
Some("long") => UnitDisplay::Long,
Some("narrow") => UnitDisplay::Narrow,
_ => UnitDisplay::Short,
};
o.rounding_mode = match opt_str(self, "roundingMode").as_deref() {
Some("ceil") => RoundingMode::Ceil,
Some("floor") => RoundingMode::Floor,
Some("expand") => RoundingMode::Expand,
Some("trunc") => RoundingMode::Trunc,
Some("halfCeil") => RoundingMode::HalfCeil,
Some("halfFloor") => RoundingMode::HalfFloor,
Some("halfExpand") => RoundingMode::HalfExpand,
Some("halfTrunc") => RoundingMode::HalfTrunc,
Some("halfEven") => RoundingMode::HalfEven,
_ => RoundingMode::HalfExpand,
};
if matches!(
self.realm
.get_property(handle, "useGrouping")
.map(|v| v.unpack()),
Some(Unpacked::Bool(false))
) {
o.use_grouping = UseGrouping::Never;
}
if let Some(mid) = opt_num(self, "minimumIntegerDigits") {
o.minimum_integer_digits = mid;
}
o.minimum_fraction_digits = opt_num(self, "minimumFractionDigits");
o.maximum_fraction_digits = opt_num(self, "maximumFractionDigits");
o.minimum_significant_digits = opt_num(self, "minimumSignificantDigits");
o.maximum_significant_digits = opt_num(self, "maximumSignificantDigits");
if o.maximum_fraction_digits.is_none()
&& o.maximum_significant_digits.is_none()
&& matches!(o.notation, Notation::Scientific | Notation::Engineering)
{
o.maximum_fraction_digits = Some(3);
}
if let Some(c) = opt_str(self, "currency") {
o.currency = Some(self.intern_static(&c));
}
if let Some(u) = opt_str(self, "unit") {
o.unit = Some(self.intern_static(&u));
}
o
}
#[cfg(feature = "intl")]
pub(crate) fn number_uses_handrolled(&mut self, handle: Handle) -> bool {
let get = |this: &mut Self, k: &str| -> Option<String> {
this.realm
.get_property(handle, k)
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| this.realm.to_display_string(v))
};
get(self, "style").as_deref() == Some("unit")
|| get(self, "notation").as_deref() == Some("compact")
}
pub(crate) fn intl_format_number(&mut self, handle: Handle, n: f64) -> String {
#[cfg(feature = "intl")]
if !self.number_uses_handrolled(handle) {
let locale = self
.realm
.get_property(handle, "\u{0}locale")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("en"));
let opts = self.number_format_options(handle);
if n == 0.0 && n.is_sign_negative() {
let body = intl::number::format(&locale, 0.0, &opts);
let sd = self
.realm
.get_property(handle, "signDisplay")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_default();
if !matches!(sd.as_str(), "never" | "exceptZero") && !body.starts_with('-') {
return alloc::format!("-{body}");
}
return body;
}
return intl::number::format(&locale, n, &opts);
}
let opt_str = |this: &mut Self, k: &str| -> Option<String> {
this.realm
.get_property(handle, k)
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| this.realm.to_display_string(v))
};
let opt_num = |this: &mut Self, k: &str| -> Option<i32> {
this.realm
.get_property(handle, k)
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| this.realm.to_number(v) as i32)
};
let style = opt_str(self, "style").unwrap_or_else(|| String::from("decimal"));
let currency = opt_str(self, "currency");
if !n.is_finite() {
let mut out = String::new();
if n.is_sign_negative() && !n.is_nan() {
out.push('-');
}
if style == "currency" {
out.push_str(¤cy_symbol(currency.as_deref().unwrap_or("")));
}
out.push_str(if n.is_nan() { "NaN" } else { "∞" });
if style == "percent" {
out.push('%');
}
return out;
}
let use_grouping = !matches!(
self.realm.get_property(handle, "useGrouping"),
Some(v) if matches!(v.unpack(), Unpacked::Bool(false))
);
let is_jpy = currency.as_deref() == Some("JPY");
let (def_min, def_max) = match style.as_str() {
"currency" if is_jpy => (0, 0),
"currency" => (2, 2),
"percent" => (0, 0),
_ => (0, 3),
};
let min = opt_num(self, "minimumFractionDigits")
.unwrap_or(def_min)
.clamp(0, 20);
let max = opt_num(self, "maximumFractionDigits")
.unwrap_or(def_max.max(min))
.clamp(min, 20);
let value = if style == "percent" { n * 100.0 } else { n };
let fmt_digits = |x: f64| -> String {
let mut s = alloc::format!("{:.*}", max as usize, x);
if max > min && s.contains('.') {
while s.ends_with('0')
&& s.split_once('.').map_or(0, |(_, f)| f.len()) > min as usize
{
s.pop();
}
if s.ends_with('.') {
s.pop();
}
}
s
};
let notation = opt_str(self, "notation").unwrap_or_default();
let (s, do_group) = if matches!(notation.as_str(), "scientific" | "engineering") {
let neg = value < 0.0;
let mag = value.abs();
let mut exp = 0i32;
let mut p = 1.0f64; if mag >= 1.0 {
while mag >= p * 10.0 {
p *= 10.0;
exp += 1;
}
} else if mag > 0.0 {
while mag < p {
p /= 10.0;
exp -= 1;
}
}
if notation == "engineering" {
let shift = exp.rem_euclid(3);
exp -= shift;
for _ in 0..shift {
p /= 10.0;
}
}
let m = if mag == 0.0 { 0.0 } else { mag / p };
let sign = if neg { "-" } else { "" };
(alloc::format!("{sign}{}E{exp}", fmt_digits(m)), false)
} else if notation == "compact" {
let neg = value < 0.0;
let mag = value.abs();
let (div, suffix) = if mag >= 1e12 {
(1e12, "T")
} else if mag >= 1e9 {
(1e9, "B")
} else if mag >= 1e6 {
(1e6, "M")
} else if mag >= 1e3 {
(1e3, "K")
} else {
(1.0, "")
};
let m = mag / div;
let cmax = if m < 10.0 { 1 } else { 0 };
let mut ms = alloc::format!("{m:.*}", cmax as usize);
if ms.contains('.') {
while ms.ends_with('0') {
ms.pop();
}
if ms.ends_with('.') {
ms.pop();
}
}
let sign = if neg { "-" } else { "" };
(alloc::format!("{sign}{ms}{suffix}"), suffix.is_empty())
} else {
(fmt_digits(value), use_grouping)
};
let grouped = if do_group {
let neg = s.starts_with('-');
let body = s.trim_start_matches('-');
let (ip, fp) = body
.split_once('.')
.map_or((body, None), |(i, f)| (i, Some(f)));
let mut g = String::new();
let len = ip.len();
for (i, b) in ip.bytes().enumerate() {
if i > 0 && (len - i) % 3 == 0 {
g.push(',');
}
g.push(b as char);
}
if let Some(f) = fp {
g.push('.');
g.push_str(f);
}
if neg { alloc::format!("-{g}") } else { g }
} else {
s
};
let neg = grouped.starts_with('-');
let magnitude = grouped.trim_start_matches('-');
let styled = match style.as_str() {
"percent" => alloc::format!("{magnitude}%"),
"currency" => {
let sym = match currency.as_deref() {
Some("USD") => "$",
Some("EUR") => "€",
Some("GBP") => "£",
Some("JPY" | "CNY") => "¥",
Some(other) => {
let other = String::from(other);
return alloc::format!(
"{}{other}\u{a0}{magnitude}",
if neg { "-" } else { "" }
);
}
None => "$",
};
alloc::format!("{sym}{magnitude}")
}
"unit" => {
let unit = opt_str(self, "unit").unwrap_or_default();
let sym = unit.split_once("-per-").map_or_else(
|| String::from(unit_symbol(&unit)),
|(a, b)| alloc::format!("{}/{}", unit_symbol(a), unit_symbol(b)),
);
let sep = if matches!(unit.as_str(), "celsius" | "fahrenheit" | "degree") {
""
} else {
"\u{a0}"
};
alloc::format!("{magnitude}{sep}{sym}")
}
_ => String::from(magnitude),
};
let is_zero = magnitude.bytes().all(|b| matches!(b, b'0' | b'.' | b','));
let sign = match opt_str(self, "signDisplay").as_deref() {
Some("never") => "",
Some("always") => {
if neg {
"-"
} else {
"+"
}
}
Some("exceptZero") if !is_zero => {
if neg {
"-"
} else {
"+"
}
}
_ if neg => "-",
_ => "",
};
alloc::format!("{sign}{styled}")
}
pub(crate) fn canonicalize_locale_list(
&mut self,
locales: NanBox,
) -> Result<Vec<String>, ExecError> {
let mut seen: Vec<String> = Vec::new();
if matches!(locales.unpack(), Unpacked::Undefined) {
return Ok(seen);
}
let is_string = locales
.as_handle()
.map(Handle::from_raw)
.is_some_and(|h| self.realm.string_value(h).is_some());
let push_tag =
|this: &mut Self, tag: &str, seen: &mut Vec<String>| -> Result<(), ExecError> {
match canonicalize_locale_id(tag) {
Some(c) => {
if !seen.contains(&c) {
seen.push(c);
}
Ok(())
}
None => {
let m = this.new_str(&alloc::format!(
"Incorrect locale information provided: {tag}"
));
Err(ExecError::Throw(this.make_error(N_RANGE_ERROR, Some(m))))
}
}
};
if is_string {
let s = self.coerce_to_string(locales)?;
push_tag(self, &s, &mut seen)?;
return Ok(seen);
}
if matches!(locales.unpack(), Unpacked::Null) {
let m = self.new_str("Cannot convert null to object");
return Err(ExecError::Throw(self.make_error(N_TYPE_ERROR, Some(m))));
}
let obj = self.coerce_to_object(locales);
let Some(oh) = obj.as_handle().map(Handle::from_raw) else {
return Ok(seen);
};
let len_v = self.read_member(oh, "length")?;
let len_v = self.coerce_to_number(len_v)?;
let len_f = self.realm.to_number(len_v);
let len = if len_f.is_nan() || len_f <= 0.0 {
0u64
} else {
len_f.min(u32::MAX as f64 * 2.0) as u64
};
for i in 0..len {
let key = alloc::format!("{i}");
if !self.has_property(oh, &key) {
continue;
}
let el = self.read_member(oh, &key)?;
let el_is_string = el
.as_handle()
.map(Handle::from_raw)
.is_some_and(|h| self.realm.string_value(h).is_some());
let el_is_object = self.is_object_value(el) && !el_is_string;
if !el_is_string && !el_is_object {
let m = self.new_str("locale list element is not a string or object");
return Err(ExecError::Throw(self.make_error(N_TYPE_ERROR, Some(m))));
}
if let Some(h) = el.as_handle().map(Handle::from_raw)
&& let Some(loc) = self.realm.get_property(h, "\u{0}locale_tag")
{
let tag = self.realm.to_display_string(loc);
if !seen.contains(&tag) {
seen.push(tag);
}
continue;
}
let s = self.coerce_to_string(el)?;
push_tag(self, &s, &mut seen)?;
}
Ok(seen)
}
pub(crate) fn intl_get_canonical_locales(
&mut self,
locales: NanBox,
) -> Result<NanBox, ExecError> {
let tags = self.canonicalize_locale_list(locales)?;
let elems: Vec<NanBox> = tags.iter().map(|t| self.new_str(t)).collect();
Ok(NanBox::handle(self.realm.new_array(elems).to_raw()))
}
pub(crate) fn intl_supported_values_of(&mut self, key: NanBox) -> Result<NanBox, ExecError> {
let k = self.coerce_to_string(key)?;
let values: &[&str] = match k.as_str() {
"calendar" => &[
"buddhist",
"chinese",
"coptic",
"dangi",
"ethioaa",
"ethiopic",
"gregory",
"hebrew",
"indian",
"islamic",
"islamic-civil",
"islamic-rgsa",
"islamic-tbla",
"islamic-umalqura",
"iso8601",
"japanese",
"persian",
"roc",
],
"collation" => &[
"compat", "dict", "emoji", "eor", "phonebk", "phonetic", "pinyin", "searchjl",
"stroke", "trad", "unihan", "zhuyin",
],
"currency" => &[
"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD",
"BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", "BSD", "BTN", "BWP", "BYN",
"BZD", "CAD", "CDF", "CHF", "CLP", "CNY", "COP", "CRC", "CUP", "CVE", "CZK", "DJF",
"DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS",
"GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "ILS",
"INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW",
"KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL",
"MGA", "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MYR", "MZN",
"NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR",
"PLN", "PYG", "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK",
"SGD", "SHP", "SLE", "SOS", "SRD", "SSP", "STN", "SVC", "SYP", "SZL", "THB", "TJS",
"TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS",
"VES", "VND", "VUV", "WST", "XAF", "XCD", "XOF", "XPF", "YER", "ZAR", "ZMW", "ZWL",
],
"numberingSystem" => &[
"adlm", "ahom", "arab", "arabext", "bali", "beng", "bhks", "brah", "cakm", "cham",
"deva", "diak", "fullwide", "gong", "gonm", "gujr", "guru", "hanidec", "hmng",
"hmnp", "java", "kali", "kawi", "khmr", "knda", "lana", "lanatham", "laoo", "latn",
"lepc", "limb", "mathbold", "mathdbl", "mathmono", "mathsanb", "mathsans", "mlym",
"modi", "mong", "mroo", "mtei", "mymr", "mymrshan", "mymrtlng", "nagm", "newa",
"nkoo", "olck", "orya", "osma", "rohg", "saur", "segment", "shrd", "sind", "sinh",
"sora", "sund", "takr", "talu", "tamldec", "telu", "thai", "tibt", "tirh", "tnsa",
"vaii", "wara", "wcho",
],
"timeZone" => &["UTC"],
"unit" => SANCTIONED_UNITS,
_ => {
let m = self.new_str(&alloc::format!("invalid key: {k}"));
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
};
let mut sorted: Vec<&str> = values.to_vec();
sorted.sort_unstable();
sorted.dedup();
let elems: Vec<NanBox> = sorted.iter().map(|s| self.new_str(s)).collect();
Ok(NanBox::handle(self.realm.new_array(elems).to_raw()))
}
fn intl_locale_prototype(&mut self) -> Option<Handle> {
if let Some(p) = self.realm.intl_prototype(N_INTL_LOCALE) {
return Some(p);
}
let ctor = self.intl_ctor_handle("Locale")?;
let obj_proto = self.object_prototype();
let proto = self.realm.new_object_with_proto(obj_proto);
for &name in LOCALE_ACCESSORS {
let label = alloc::format!("get {name}");
let target = self.new_str(name);
let th = target.as_handle().map(Handle::from_raw).unwrap();
let getter = self.realm.new_bound_native(N_INTL_LOCALE_ACCESSOR, th);
self.install_fn_name_length(getter, &label, 0);
self.realm.define_accessor(
proto,
name,
NanBox::handle(getter.to_raw()),
NanBox::undefined(),
);
self.realm.mark_hidden(proto, name);
}
for &m in &["maximize", "minimize", "toString"] {
let target = self.new_str(m);
let th = target.as_handle().map(Handle::from_raw).unwrap();
let f = self.realm.new_bound_native(N_INTL_LOCALE_METHOD, th);
self.install_fn_name_length(f, m, 0);
self.realm
.set_property(proto, m, NanBox::handle(f.to_raw()));
self.realm.mark_hidden(proto, m);
}
self.install_to_string_tag(proto, "Intl.Locale");
self.realm
.set_hidden_property(proto, "constructor", NanBox::handle(ctor.to_raw()));
self.link_ctor_prototype(ctor, proto);
self.realm.set_intl_prototype(N_INTL_LOCALE, proto);
Some(proto)
}
pub(crate) fn make_locale(&mut self, args: &[NanBox]) -> Result<NanBox, ExecError> {
let tag_arg = args.first().copied().unwrap_or(NanBox::undefined());
let base_tag = if let Some(h) = tag_arg.as_handle().map(Handle::from_raw)
&& let Some(t) = self.realm.get_property(h, "\u{0}locale_tag")
{
self.realm.to_display_string(t)
} else if matches!(tag_arg.unpack(), Unpacked::Undefined) {
let m = self.new_str("Intl.Locale: tag must be a string or Locale");
return Err(ExecError::Throw(self.make_error(N_TYPE_ERROR, Some(m))));
} else {
self.coerce_to_string(tag_arg)?
};
let Some(canon) = canonicalize_locale_id(&base_tag) else {
let m = self.new_str(&alloc::format!("invalid language tag: {base_tag}"));
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
};
let opts = args
.get(1)
.copied()
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.coerce_to_object(v))
.and_then(|v| v.as_handle())
.map(Handle::from_raw);
let language = self.get_string_option(opts, "language", &[], None)?;
let script = self.get_string_option(opts, "script", &[], None)?;
let region = self.get_string_option(opts, "region", &[], None)?;
let calendar = self.get_string_option(opts, "calendar", &[], None)?;
let collation = self.get_string_option(opts, "collation", &[], None)?;
let hour_cycle =
self.get_string_option(opts, "hourCycle", &["h11", "h12", "h23", "h24"], None)?;
let case_first =
self.get_string_option(opts, "caseFirst", &["upper", "lower", "false"], None)?;
let numeric = self.get_bool_option(opts, "numeric", None)?;
let numbering = self.get_string_option(opts, "numberingSystem", &[], None)?;
let mut parsed = ParsedLocale::from_canonical(&canon);
if let Some(l) = &language {
parsed.language = l.to_ascii_lowercase();
}
if let Some(s) = &script {
parsed.script = Some(titlecase_script(s));
}
if let Some(r) = ®ion {
parsed.region = Some(r.to_ascii_uppercase());
}
if let Some(c) = &calendar {
parsed.set_keyword("ca", c);
}
if let Some(c) = &collation {
parsed.set_keyword("co", c);
}
if let Some(h) = &hour_cycle {
parsed.set_keyword("hc", h);
}
if let Some(k) = &case_first {
parsed.set_keyword("kf", k);
}
if let Some(b) = numeric {
parsed.set_keyword("kn", if b { "true" } else { "false" });
}
if let Some(n) = &numbering {
parsed.set_keyword("nu", n);
}
let final_tag = parsed.to_tag();
let obj = self.realm.new_object();
let tagv = self.new_str(&final_tag);
self.realm.set_hidden_property(obj, "\u{0}locale_tag", tagv);
let store = |this: &mut Self, key: &str, val: Option<&str>| {
if let Some(v) = val {
let sv = this.new_str(v);
this.realm.set_hidden_property(obj, key, sv);
}
};
store(self, "\u{0}loc_language", Some(&parsed.language));
store(self, "\u{0}loc_script", parsed.script.as_deref());
store(self, "\u{0}loc_region", parsed.region.as_deref());
store(self, "\u{0}loc_baseName", Some(&parsed.base_name()));
store(self, "\u{0}loc_ca", parsed.keyword("ca"));
store(self, "\u{0}loc_co", parsed.keyword("co"));
store(self, "\u{0}loc_hc", parsed.keyword("hc"));
store(self, "\u{0}loc_kf", parsed.keyword("kf"));
store(self, "\u{0}loc_nu", parsed.keyword("nu"));
let kn = parsed.keyword("kn");
let numeric_val = matches!(kn, Some("true") | Some(""));
self.realm
.set_hidden_property(obj, "\u{0}loc_numeric", NanBox::boolean(numeric_val));
self.realm
.set_hidden_property(obj, "\u{0}brand_loc", NanBox::boolean(true));
if let Some(proto) = self.intl_locale_prototype() {
self.realm.set_object_proto(obj, Some(proto));
}
Ok(NanBox::handle(obj.to_raw()))
}
pub(crate) fn intl_locale_accessor_dispatch(
&mut self,
this: NanBox,
name: &str,
) -> Result<NanBox, ExecError> {
let h = self.require_intl_slot(this, "\u{0}brand_loc", "Intl.Locale.prototype getter")?;
let read = |this: &Self, key: &str| -> NanBox {
this.realm
.get_property(h, key)
.unwrap_or(NanBox::undefined())
};
Ok(match name {
"language" => read(self, "\u{0}loc_language"),
"script" => read(self, "\u{0}loc_script"),
"region" => read(self, "\u{0}loc_region"),
"baseName" => read(self, "\u{0}loc_baseName"),
"calendar" => read(self, "\u{0}loc_ca"),
"collation" => read(self, "\u{0}loc_co"),
"hourCycle" => read(self, "\u{0}loc_hc"),
"caseFirst" => read(self, "\u{0}loc_kf"),
"numberingSystem" => read(self, "\u{0}loc_nu"),
"numeric" => read(self, "\u{0}loc_numeric"),
_ => NanBox::undefined(),
})
}
pub(crate) fn intl_locale_method_dispatch(
&mut self,
this: NanBox,
name: &str,
) -> Result<NanBox, ExecError> {
let h = self.require_intl_slot(this, "\u{0}brand_loc", "Intl.Locale.prototype method")?;
let tag = self
.realm
.get_property(h, "\u{0}locale_tag")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_default();
match name {
"toString" => Ok(self.new_str(&tag)),
_ => {
let tagv = self.new_str(&tag);
self.make_locale(&[tagv])
}
}
}
pub(crate) fn number_handle_parts(
&mut self,
handle: Handle,
value: NanBox,
) -> Vec<(&'static str, String)> {
#[cfg(feature = "intl")]
if !self.number_uses_handrolled(handle) {
let n = self.realm.to_number(value);
let locale = self
.realm
.get_property(handle, "\u{0}locale")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("en"));
let opts = self.number_format_options(handle);
let neg_zero = n == 0.0 && n.is_sign_negative();
let feed = if neg_zero { 0.0 } else { n };
let mut parts: Vec<(&'static str, String)> =
intl::number::format_to_parts(&locale, feed, &opts)
.into_iter()
.map(|p| (p.kind.as_str(), p.value))
.collect();
if neg_zero {
let sd = self
.realm
.get_property(handle, "signDisplay")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_default();
let show = !matches!(sd.as_str(), "never" | "exceptZero")
&& !parts.iter().any(|(t, _)| *t == "minusSign");
if show {
parts.insert(0, ("minusSign", String::from("-")));
}
}
return parts;
}
let formatted = self.intl_format_value(handle, value);
let style = self
.realm
.get_property(handle, "style")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("decimal"));
let currency_sym = if style == "currency" {
let code = self
.realm
.get_property(handle, "currency")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_default();
currency_symbol(&code)
} else {
String::new()
};
let mut entries: Vec<(&'static str, String)> = Vec::new();
let mut s = formatted.as_str();
if let Some(rest) = s.strip_prefix('-') {
entries.push(("minusSign", String::from("-")));
s = rest;
}
if !currency_sym.is_empty() && s.starts_with(currency_sym.as_str()) {
entries.push(("currency", currency_sym.clone()));
s = &s[currency_sym.len()..];
}
let mut percent = false;
if style == "percent" && s.ends_with('%') {
percent = true;
s = &s[..s.len() - '%'.len_utf8()];
}
if s == "NaN" {
entries.push(("nan", String::from("NaN")));
} else if s == "∞" {
entries.push(("infinity", String::from("∞")));
} else {
let (int_part, frac_part) = match s.split_once('.') {
Some((i, f)) => (i, Some(f)),
None => (s, None),
};
for (gi, grp) in int_part.split(',').enumerate() {
if gi > 0 {
entries.push(("group", String::from(",")));
}
entries.push(("integer", String::from(grp)));
}
if let Some(f) = frac_part {
entries.push(("decimal", String::from(".")));
entries.push(("fraction", String::from(f)));
}
}
if percent {
entries.push(("percentSign", String::from("%")));
}
entries
}
fn intl_duration_prototype(&mut self) -> Option<Handle> {
if let Some(p) = self.realm.intl_prototype(N_INTL_DURATION_FORMAT) {
return Some(p);
}
let ctor = self.intl_ctor_handle("DurationFormat")?;
let obj_proto = self.object_prototype();
let proto = self.realm.new_object_with_proto(obj_proto);
for &(m, arity) in &[
("resolvedOptions", 0u32),
("format", 1),
("formatToParts", 1),
] {
let target = self.new_str(m);
let th = target.as_handle().map(Handle::from_raw).unwrap();
let f = self.realm.new_bound_native(N_INTL_DURATION_METHOD, th);
self.install_fn_name_length(f, m, arity);
self.realm
.set_property(proto, m, NanBox::handle(f.to_raw()));
self.realm.mark_hidden(proto, m);
}
self.install_to_string_tag(proto, "Intl.DurationFormat");
self.realm
.set_hidden_property(proto, "constructor", NanBox::handle(ctor.to_raw()));
self.link_ctor_prototype(ctor, proto);
self.realm.set_intl_prototype(N_INTL_DURATION_FORMAT, proto);
Some(proto)
}
pub(crate) fn make_duration_format(&mut self, args: &[NanBox]) -> Result<NanBox, ExecError> {
let obj = self.realm.new_object();
let requested =
self.canonicalize_locale_list(args.first().copied().unwrap_or(NanBox::undefined()))?;
let opts_arg = args.get(1).copied().unwrap_or(NanBox::undefined());
let opts = if matches!(opts_arg.unpack(), Unpacked::Undefined) {
None
} else if self.is_object_value(opts_arg) {
opts_arg.as_handle().map(Handle::from_raw)
} else {
return Err(self.type_error("Intl.DurationFormat options must be an object"));
};
let _ = self.get_string_option(
opts,
"localeMatcher",
&["lookup", "best fit"],
Some("best fit"),
)?;
let nu_opt = self.get_string_option(opts, "numberingSystem", &[], None)?;
if let Some(ns) = &nu_opt
&& !is_unicode_type_value(ns)
{
let m = self.new_str("invalid numberingSystem");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
let (locale, numbering) =
self.resolve_duration_locale(requested.first().map(String::as_str), nu_opt.as_deref());
let locv = self.new_str(&locale);
self.realm.set_hidden_property(obj, "\u{0}locale", locv);
let nuv = self.new_str(&numbering);
self.realm.set_hidden_property(obj, "numberingSystem", nuv);
let style = self
.get_string_option(
opts,
"style",
&["long", "short", "narrow", "digital"],
Some("short"),
)?
.unwrap();
self.store_str(obj, "style", &Some(style.clone()));
let digital = style == "digital";
const SUB: &[&str] = &["long", "short", "narrow"];
const TIME: &[&str] = &["long", "short", "narrow", "numeric", "2-digit"];
const FRAC: &[&str] = &["long", "short", "narrow", "numeric"];
let table: &[(&str, &[&str], &str)] = &[
("years", SUB, "short"),
("months", SUB, "short"),
("weeks", SUB, "short"),
("days", SUB, "short"),
("hours", TIME, "numeric"),
("minutes", TIME, "numeric"),
("seconds", TIME, "numeric"),
("milliseconds", FRAC, "numeric"),
("microseconds", FRAC, "numeric"),
("nanoseconds", FRAC, "numeric"),
];
let mut prev_style: Option<String> = None;
for (unit, styles, digital_base) in table {
let (ust, udisp) = self.get_duration_unit_options(
opts,
unit,
&style,
styles,
digital_base,
prev_style.as_deref(),
digital,
)?;
self.store_str(obj, unit, &Some(ust.clone()));
let disp_key = alloc::format!("{unit}Display");
self.store_str(obj, &disp_key, &Some(udisp));
if matches!(
*unit,
"hours" | "minutes" | "seconds" | "milliseconds" | "microseconds"
) {
prev_style = Some(ust);
}
}
if let Some(fd) = self.get_int_option(opts, "fractionalDigits", 0.0, 9.0, None)? {
self.realm
.set_hidden_property(obj, "fractionalDigits", NanBox::number(fd));
}
self.realm
.set_hidden_property(obj, "\u{0}brand_df", NanBox::boolean(true));
if let Some(proto) = self.intl_duration_prototype() {
self.realm.set_object_proto(obj, Some(proto));
}
Ok(NanBox::handle(obj.to_raw()))
}
fn resolve_duration_locale(
&mut self,
requested: Option<&str>,
option: Option<&str>,
) -> (String, String) {
let tag = requested.unwrap_or("en-US");
let parsed = ParsedLocale::from_canonical(tag);
let ext_nu = parsed.keyword("nu").map(String::from);
let default_nu = String::from("latn");
let supported = |ns: &str| is_supported_numbering_system(ns);
let mut base = parsed.base_name();
for e in &parsed.other_ext {
base.push('-');
base.push_str(e);
}
match option {
Some(opt) if supported(opt) => {
if ext_nu.as_deref() == Some(opt) {
(alloc::format!("{base}-u-nu-{opt}"), String::from(opt))
} else {
(base, String::from(opt))
}
}
_ => {
match ext_nu {
Some(ns) if supported(&ns) => (alloc::format!("{base}-u-nu-{ns}"), ns),
_ => (base, default_nu),
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn get_duration_unit_options(
&mut self,
opts: Option<Handle>,
unit: &str,
base_style: &str,
styles_list: &[&str],
digital_base: &str,
prev_style: Option<&str>,
two_digit_hours: bool,
) -> Result<(String, String), ExecError> {
let mut style = self.get_string_option(opts, unit, styles_list, None)?;
let mut display_default = "always";
if style.is_none() {
if base_style == "digital" {
if !matches!(unit, "hours" | "minutes" | "seconds") {
display_default = "auto";
}
style = Some(String::from(digital_base));
} else {
display_default = "auto";
if matches!(prev_style, Some("numeric") | Some("2-digit")) {
style = Some(String::from("numeric"));
} else {
style = Some(String::from(base_style));
}
}
}
let mut style = style.unwrap();
let disp_key = alloc::format!("{unit}Display");
let display = self
.get_string_option(opts, &disp_key, &["auto", "always"], Some(display_default))?
.unwrap();
if matches!(prev_style, Some("numeric") | Some("2-digit")) {
if style != "numeric" && style != "2-digit" {
let m = self.new_str(&alloc::format!(
"invalid style '{style}' for {unit} following a numeric unit"
));
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
} else if matches!(unit, "minutes" | "seconds") {
style = String::from("2-digit");
}
}
if unit == "hours" && two_digit_hours && style == "numeric" {
}
Ok((style, display))
}
const DURATION_UNITS: [&'static str; 10] = [
"years",
"months",
"weeks",
"days",
"hours",
"minutes",
"seconds",
"milliseconds",
"microseconds",
"nanoseconds",
];
pub(crate) fn intl_duration_method_dispatch(
&mut self,
this: NanBox,
name: &str,
args: &[NanBox],
) -> Result<NanBox, ExecError> {
let h = self.require_intl_slot(
this,
"\u{0}brand_df",
"Intl.DurationFormat.prototype method",
)?;
match name {
"resolvedOptions" => self.duration_resolved_options(h),
"formatToParts" => {
let rec = self
.read_duration_record(args.first().copied().unwrap_or(NanBox::undefined()))?;
let parts = self.partition_duration(h, &rec);
let mut arr = Vec::with_capacity(parts.len());
for (ty, val, unit) in parts {
let o = self.realm.new_object();
let tv = self.new_str(ty);
self.realm.set_property(o, "type", tv);
let vv = self.new_str(&val);
self.realm.set_property(o, "value", vv);
if let Some(u) = unit {
let uv = self.new_str(u);
self.realm.set_property(o, "unit", uv);
}
arr.push(NanBox::handle(o.to_raw()));
}
Ok(NanBox::handle(self.realm.new_array(arr).to_raw()))
}
_ => {
let rec = self
.read_duration_record(args.first().copied().unwrap_or(NanBox::undefined()))?;
let parts = self.partition_duration(h, &rec);
let s: String = parts.into_iter().map(|(_, v, _)| v).collect();
Ok(self.new_str(&s))
}
}
}
fn duration_resolved_options(&mut self, h: Handle) -> Result<NanBox, ExecError> {
let out = self.realm.new_object();
let read = |this: &mut Self, key: &str, dflt: &str| -> String {
this.realm
.get_property(h, key)
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| this.realm.to_display_string(v))
.unwrap_or_else(|| String::from(dflt))
};
let locale = read(self, "\u{0}locale", "en-US");
let lv = self.new_str(&locale);
self.realm.set_property(out, "locale", lv);
let nu = read(self, "numberingSystem", "latn");
let nuv = self.new_str(&nu);
self.realm.set_property(out, "numberingSystem", nuv);
let style = read(self, "style", "short");
let sv = self.new_str(&style);
self.realm.set_property(out, "style", sv);
for unit in Self::DURATION_UNITS {
let ust = read(self, unit, "short");
let uv = self.new_str(&ust);
self.realm.set_property(out, unit, uv);
let disp_key = alloc::format!("{unit}Display");
let udisp = read(self, &disp_key, "auto");
let dv = self.new_str(&udisp);
self.realm.set_property(out, &disp_key, dv);
}
if let Some(v) = self
.realm
.get_property(h, "fractionalDigits")
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
{
self.realm.set_property(out, "fractionalDigits", v);
}
Ok(NanBox::handle(out.to_raw()))
}
fn read_duration_record(&mut self, input: NanBox) -> Result<[f64; 10], ExecError> {
if let Some(s) = input
.as_handle()
.map(Handle::from_raw)
.and_then(|hh| self.realm.string_value(hh))
{
return self.parse_duration_string(&s);
}
if !self.is_object_value(input) {
return Err(self.type_error("Intl.DurationFormat.format: argument must be an object"));
}
let oh = input.as_handle().map(Handle::from_raw).unwrap();
let mut rec = [0.0f64; 10];
let mut any = false;
for (i, unit) in Self::DURATION_UNITS.iter().enumerate() {
let v = self.read_member(oh, unit)?;
if matches!(v.unpack(), Unpacked::Undefined) {
continue;
}
any = true;
let nv = self.coerce_to_number(v)?;
let n = self.realm.to_number(nv);
if !n.is_finite() || trunc_toward_zero(n) != n {
let m = self.new_str(&alloc::format!("duration field {unit} is not an integer"));
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
rec[i] = n;
}
if !any {
return Err(self.type_error("Intl.DurationFormat.format: no duration fields present"));
}
if !is_valid_duration(&rec) {
let m = self.new_str("invalid Duration");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
Ok(rec)
}
fn parse_duration_string(&mut self, s: &str) -> Result<[f64; 10], ExecError> {
let bad = |this: &mut Self| -> ExecError {
let m = this.new_str("invalid Duration string");
ExecError::Throw(this.make_error(N_RANGE_ERROR, Some(m)))
};
let mut rec = [0.0f64; 10];
let bytes = s.trim();
let (sign, rest) = match bytes.strip_prefix('-') {
Some(r) => (-1.0, r),
None => (1.0, bytes.strip_prefix('+').unwrap_or(bytes)),
};
let rest = match rest.strip_prefix(['P', 'p']) {
Some(r) => r,
None => return Err(bad(self)),
};
let (date_part, time_part) = match rest.split_once(['T', 't']) {
Some((d, t)) => (d, Some(t)),
None => (rest, None),
};
let mut saw_any = false;
let parse_section = |this: &mut Self,
sect: &str,
allowed: &[(char, usize)],
rec: &mut [f64; 10],
saw: &mut bool|
-> Result<(), ExecError> {
let mut chars = sect.chars().peekable();
let mut last_idx: i32 = -1;
while chars.peek().is_some() {
let mut num = String::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() || c == '.' || c == ',' {
num.push(if c == ',' { '.' } else { c });
chars.next();
} else {
break;
}
}
let Some(desig) = chars.next() else {
if num.is_empty() {
break;
}
return Err(bad(this));
};
if num.is_empty() {
return Err(bad(this));
}
let Some(&(_, slot)) = allowed.iter().find(|(d, _)| d.eq_ignore_ascii_case(&desig))
else {
return Err(bad(this));
};
if (slot as i32) <= last_idx {
return Err(bad(this));
}
last_idx = slot as i32;
*saw = true;
if num.contains('.') && slot != 6 {
return Err(bad(this));
}
if slot == 6 {
let (int_s, frac) = num.split_once('.').unwrap_or((num.as_str(), ""));
rec[6] = int_s.parse::<f64>().map_err(|_| bad(this))?;
let mut f: String = frac.chars().take(9).collect();
while f.len() < 9 {
f.push('0');
}
rec[7] = f[0..3].parse::<f64>().unwrap_or(0.0);
rec[8] = f[3..6].parse::<f64>().unwrap_or(0.0);
rec[9] = f[6..9].parse::<f64>().unwrap_or(0.0);
} else {
rec[slot] = num.parse::<f64>().map_err(|_| bad(this))?;
}
}
Ok(())
};
parse_section(
self,
date_part,
&[('Y', 0), ('M', 1), ('W', 2), ('D', 3)],
&mut rec,
&mut saw_any,
)?;
if let Some(tp) = time_part {
if tp.is_empty() {
return Err(bad(self));
}
parse_section(
self,
tp,
&[('H', 4), ('M', 5), ('S', 6)],
&mut rec,
&mut saw_any,
)?;
}
if !saw_any {
return Err(bad(self));
}
for v in &mut rec {
*v *= sign;
}
if !is_valid_duration(&rec) {
let m = self.new_str("invalid Duration");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
Ok(rec)
}
fn duration_unit_resolved(&mut self, h: Handle, unit: &str) -> (String, String) {
let style = self
.realm
.get_property(h, unit)
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("short"));
let disp = self
.realm
.get_property(h, &alloc::format!("{unit}Display"))
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("auto"));
(style, disp)
}
fn partition_duration(
&mut self,
h: Handle,
duration: &[f64; 10],
) -> Vec<(&'static str, String, Option<&'static str>)> {
let style = self
.realm
.get_property(h, "style")
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("short"));
let numbering = self
.realm
.get_property(h, "numberingSystem")
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("latn"));
let fractional_digits = self
.realm
.get_property(h, "fractionalDigits")
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_number(v) as i32);
let units = Self::DURATION_UNITS;
let mut ustyle = [const { String::new() }; 10];
let mut udisp = [const { String::new() }; 10];
for (i, u) in units.iter().enumerate() {
let (s, d) = self.duration_unit_resolved(h, u);
ustyle[i] = s;
udisp[i] = d;
}
let time_separator = ":";
let mut result: Vec<Vec<(&'static str, String, Option<&'static str>)>> = Vec::new();
let mut need_separator = false;
let mut display_negative_sign = true;
let any_negative = duration
.iter()
.any(|&v| v < 0.0 || (v == 0.0 && v.is_sign_negative()));
for idx in 0..units.len() {
let unit = units[idx];
let singular: &'static str = duration_singular(unit);
let mut value = duration[idx];
let style_u = ustyle[idx].clone();
let display_u = udisp[idx].clone();
let mut value_str: Option<String> = None;
let mut done = false;
let (mut nf_min_frac, mut nf_max_frac, mut nf_trunc) =
(None::<i32>, None::<i32>, false);
if matches!(unit, "seconds" | "milliseconds" | "microseconds") {
let next_style = ustyle[idx + 1].as_str();
if next_style == "numeric" {
let exp = match unit {
"seconds" => 9,
"milliseconds" => 6,
_ => 3,
};
value_str = Some(duration_to_fractional(duration, exp));
nf_max_frac = Some(fractional_digits.unwrap_or(9));
nf_min_frac = Some(fractional_digits.unwrap_or(0));
nf_trunc = true;
done = true;
}
}
let mut display_required = false;
if unit == "minutes" && need_separator {
display_required = udisp[6] == "always"
|| duration[6] != 0.0
|| duration[7] != 0.0
|| duration[8] != 0.0
|| duration[9] != 0.0;
}
let nonzero = value != 0.0
|| value_str
.as_deref()
.is_some_and(|s| s.bytes().any(|b| matches!(b, b'1'..=b'9')));
if nonzero || display_u != "auto" || display_required {
let mut sign_never = false;
if display_negative_sign {
display_negative_sign = false;
if value == 0.0 && value_str.is_none() && any_negative {
value = -0.0;
}
} else {
sign_never = true;
}
let nf = self.realm.new_object();
let marker = self.new_str("number");
self.realm.set_hidden_property(nf, "\u{0}intl", marker);
let loc = self.new_str("en");
self.realm.set_hidden_property(nf, "\u{0}locale", loc);
let nuv = self.new_str(&numbering);
self.realm.set_hidden_property(nf, "numberingSystem", nuv);
if sign_never {
let sd = self.new_str("never");
self.realm.set_hidden_property(nf, "signDisplay", sd);
}
if style_u == "2-digit" {
self.realm
.set_hidden_property(nf, "minimumIntegerDigits", NanBox::number(2.0));
}
if style_u != "numeric" && style_u != "2-digit" {
let st = self.new_str("unit");
self.realm.set_hidden_property(nf, "style", st);
let uu = self.new_str(singular);
self.realm.set_hidden_property(nf, "unit", uu);
let ud = self.new_str(&style_u);
self.realm.set_hidden_property(nf, "unitDisplay", ud);
} else {
self.realm
.set_hidden_property(nf, "useGrouping", NanBox::boolean(false));
}
if let Some(mn) = nf_min_frac {
self.realm.set_hidden_property(
nf,
"minimumFractionDigits",
NanBox::number(mn as f64),
);
}
if let Some(mx) = nf_max_frac {
self.realm.set_hidden_property(
nf,
"maximumFractionDigits",
NanBox::number(mx as f64),
);
}
if nf_trunc {
let rm = self.new_str("trunc");
self.realm.set_hidden_property(nf, "roundingMode", rm);
}
let value_box = match &value_str {
Some(s) => self.new_str(s),
None => NanBox::number(value),
};
let number_parts = self.number_handle_parts(nf, value_box);
let mut list: Vec<(&'static str, String, Option<&'static str>)> = if !need_separator
{
Vec::new()
} else {
let mut prev = result.pop().unwrap();
prev.push(("literal", String::from(time_separator), None));
prev
};
for (ty, val) in number_parts {
list.push((ty, val, Some(singular)));
}
if !need_separator {
if style_u == "2-digit" || style_u == "numeric" {
need_separator = true;
}
result.push(list);
} else {
result.push(list);
}
}
if done {
break;
}
}
let list_style = if style == "digital" {
String::from("short")
} else {
style
};
let strings: Vec<String> = result
.iter()
.map(|parts| parts.iter().map(|(_, v, _)| v.as_str()).collect())
.collect();
let lf = self.realm.new_object();
let lm = self.new_str("list");
self.realm.set_hidden_property(lf, "\u{0}intl", lm);
let llo = self.new_str("en");
self.realm.set_hidden_property(lf, "\u{0}locale", llo);
let lt = self.new_str("unit");
self.realm.set_hidden_property(lf, "type", lt);
let ls = self.new_str(&list_style);
self.realm.set_hidden_property(lf, "style", ls);
let list_parts = self.list_format_parts(&strings, "unit", &list_style);
let mut flattened: Vec<(&'static str, String, Option<&'static str>)> = Vec::new();
let mut iter = result.into_iter();
for (ty, val) in list_parts {
if ty == "element" {
if let Some(parts) = iter.next() {
flattened.extend(parts);
}
} else {
flattened.push((ty, val, None));
}
}
let _ = lf;
flattened
}
}
struct ParsedLocale {
language: String,
script: Option<String>,
region: Option<String>,
variants: Vec<String>,
keywords: Vec<(String, String)>,
other_ext: Vec<String>,
}
impl ParsedLocale {
fn from_canonical(canon: &str) -> Self {
let mut language = String::new();
let mut script = None;
let mut region = None;
let mut variants = Vec::new();
let mut keywords = Vec::new();
let mut other_ext = Vec::new();
let parts: Vec<&str> = canon.split('-').collect();
let mut i = 0;
if i < parts.len() {
language = parts[i].to_ascii_lowercase();
i += 1;
}
if i < parts.len()
&& parts[i].len() == 4
&& parts[i].bytes().all(|b| b.is_ascii_alphabetic())
{
script = Some(titlecase_script(parts[i]));
i += 1;
}
if i < parts.len()
&& ((parts[i].len() == 2 && parts[i].bytes().all(|b| b.is_ascii_alphabetic()))
|| (parts[i].len() == 3 && parts[i].bytes().all(|b| b.is_ascii_digit())))
{
region = Some(parts[i].to_ascii_uppercase());
i += 1;
}
while i < parts.len() && parts[i].len() != 1 {
variants.push(parts[i].to_ascii_lowercase());
i += 1;
}
while i < parts.len() {
let singleton = parts[i];
if singleton == "u" {
i += 1;
while i < parts.len() && parts[i].len() != 1 {
if parts[i].len() != 2 {
i += 1;
continue;
}
let key = String::from(parts[i]);
i += 1;
let mut vals: Vec<String> = Vec::new();
while i < parts.len() && parts[i].len() != 1 && parts[i].len() != 2 {
vals.push(String::from(parts[i]));
i += 1;
}
keywords.push((key, vals.join("-")));
}
} else {
let mut buf = alloc::vec![String::from(singleton)];
i += 1;
while i < parts.len() && parts[i].len() != 1 {
buf.push(String::from(parts[i]));
i += 1;
}
other_ext.push(buf.join("-"));
}
}
ParsedLocale {
language,
script,
region,
variants,
keywords,
other_ext,
}
}
fn keyword(&self, key: &str) -> Option<&str> {
self.keywords
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
fn set_keyword(&mut self, key: &str, val: &str) {
let v = val.to_ascii_lowercase();
if let Some(e) = self.keywords.iter_mut().find(|(k, _)| k == key) {
e.1 = v;
} else {
self.keywords.push((String::from(key), v));
}
}
fn base_name(&self) -> String {
let mut out = self.language.clone();
if let Some(s) = &self.script {
out.push('-');
out.push_str(s);
}
if let Some(r) = &self.region {
out.push('-');
out.push_str(r);
}
for v in &self.variants {
out.push('-');
out.push_str(v);
}
out
}
fn to_tag(&self) -> String {
let mut out = self.base_name();
let mut kw = self.keywords.clone();
kw.sort_by(|a, b| a.0.cmp(&b.0));
if !kw.is_empty() {
out.push_str("-u");
for (k, v) in &kw {
out.push('-');
out.push_str(k);
if !v.is_empty() && v != "true" {
out.push('-');
out.push_str(v);
}
}
}
for e in &self.other_ext {
out.push('-');
out.push_str(e);
}
out
}
}
fn is_supported_numbering_system(ns: &str) -> bool {
matches!(ns, "latn" | "arab")
}
fn duration_singular(unit: &str) -> &'static str {
match unit {
"years" => "year",
"months" => "month",
"weeks" => "week",
"days" => "day",
"hours" => "hour",
"minutes" => "minute",
"seconds" => "second",
"milliseconds" => "millisecond",
"microseconds" => "microsecond",
_ => "nanosecond",
}
}
fn is_valid_duration(rec: &[f64; 10]) -> bool {
let mut sign = 0i32;
for &v in rec {
if !v.is_finite() {
return false;
}
let s = if v > 0.0 {
1
} else if v < 0.0 {
-1
} else {
0
};
if s != 0 {
if sign != 0 && sign != s {
return false;
}
sign = s;
}
}
let two32 = 4_294_967_296.0f64; if rec[0].abs() >= two32 || rec[1].abs() >= two32 || rec[2].abs() >= two32 {
return false;
}
let two53_ns = (1i128 << 53) * 1_000_000_000; let term_ns = |v: f64, scale_ns: i128| -> Option<i128> {
if v.abs() >= 9.0e30 {
return None; }
(v as i128).checked_mul(scale_ns)
};
let mut total: i128 = 0;
let scales: [(usize, i128); 7] = [
(3, 86_400 * 1_000_000_000),
(4, 3_600 * 1_000_000_000),
(5, 60 * 1_000_000_000),
(6, 1_000_000_000),
(7, 1_000_000),
(8, 1_000),
(9, 1),
];
for (i, scale) in scales {
match term_ns(rec[i], scale).and_then(|t| total.checked_add(t)) {
Some(t) => total = t,
None => return false,
}
}
total.abs() < two53_ns
}
fn duration_to_fractional(duration: &[f64; 10], exponent: u32) -> String {
let (seconds, milliseconds, microseconds, nanoseconds) =
(duration[6], duration[7], duration[8], duration[9]);
match exponent {
9 if milliseconds == 0.0 && microseconds == 0.0 && nanoseconds == 0.0 => {
return format_integral(seconds);
}
6 if microseconds == 0.0 && nanoseconds == 0.0 => {
return format_integral(milliseconds);
}
3 if nanoseconds == 0.0 => {
return format_integral(microseconds);
}
_ => {}
}
let mut ns: i128 = nanoseconds as i128;
if exponent >= 9 {
ns += (seconds as i128) * 1_000_000_000;
}
if exponent >= 6 {
ns += (milliseconds as i128) * 1_000_000;
}
if exponent >= 3 {
ns += (microseconds as i128) * 1_000;
}
let e: i128 = 10i128.pow(exponent);
let q = ns / e;
let mut r = ns % e;
if r < 0 {
r = -r;
}
let mut rs = alloc::format!("{r}");
while rs.len() < exponent as usize {
rs.insert(0, '0');
}
alloc::format!("{q}.{rs}")
}
fn format_integral(v: f64) -> String {
alloc::format!("{}", v as i128)
}
fn titlecase_script(s: &str) -> String {
let mut out = String::new();
for (i, c) in s.chars().enumerate() {
if i == 0 {
out.push(c.to_ascii_uppercase());
} else {
out.push(c.to_ascii_lowercase());
}
}
out
}