use super::*;
#[cfg(not(feature = "std"))]
use crate::common::FloatExt;
enum UseGroupingResolved {
Bool(bool),
Str(&'static str),
}
fn numbering_system_digit_base(nu: &str) -> Option<u32> {
Some(match nu {
"adlm" => 0x1E950,
"ahom" => 0x11730,
"arab" => 0x0660,
"arabext" => 0x06F0,
"bali" => 0x1B50,
"beng" => 0x09E6,
"bhks" => 0x11C50,
"brah" => 0x11066,
"cakm" => 0x11136,
"cham" => 0xAA50,
"deva" => 0x0966,
"diak" => 0x11950,
"fullwide" => 0xFF10,
"gara" => 0x10D40,
"gong" => 0x11DA0,
"gonm" => 0x11D50,
"gujr" => 0x0AE6,
"gukh" => 0x16130,
"guru" => 0x0A66,
"hmng" => 0x16B50,
"hmnp" => 0x1E140,
"java" => 0xA9D0,
"kali" => 0xA900,
"kawi" => 0x11F50,
"khmr" => 0x17E0,
"knda" => 0x0CE6,
"krai" => 0x16D70,
"lana" => 0x1A80,
"lanatham" => 0x1A90,
"laoo" => 0x0ED0,
"latn" => 0x0030,
"lepc" => 0x1C40,
"limb" => 0x1946,
"mathbold" => 0x1D7CE,
"mathdbl" => 0x1D7D8,
"mathmono" => 0x1D7F6,
"mathsanb" => 0x1D7EC,
"mathsans" => 0x1D7E2,
"mlym" => 0x0D66,
"modi" => 0x11650,
"mong" => 0x1810,
"mroo" => 0x16A60,
"mtei" => 0xABF0,
"mymr" => 0x1040,
"mymrepka" => 0x116DA,
"mymrpao" => 0x116D0,
"mymrshan" => 0x1090,
"mymrtlng" => 0xA9F0,
"nagm" => 0x1E4F0,
"newa" => 0x11450,
"nkoo" => 0x07C0,
"olck" => 0x1C50,
"onao" => 0x1E5F1,
"orya" => 0x0B66,
"osma" => 0x104A0,
"outlined" => 0x1CCF0,
"rohg" => 0x10D30,
"saur" => 0xA8D0,
"segment" => 0x1FBF0,
"shrd" => 0x111D0,
"sind" => 0x112F0,
"sinh" => 0x0DE6,
"sora" => 0x110F0,
"sund" => 0x1BB0,
"sunu" => 0x11BF0,
"takr" => 0x116C0,
"talu" => 0x19D0,
"tamldec" => 0x0BE6,
"telu" => 0x0C66,
"thai" => 0x0E50,
"tibt" => 0x0F20,
"tirh" => 0x114D0,
"tnsa" => 0x16AC0,
"tols" => 0x11DE0,
"vaii" => 0xA620,
"wara" => 0x118E0,
"wcho" => 0x1E2F0,
_ => return None,
})
}
fn substitute_numbering_digits(nu: &str, s: String) -> String {
if nu == "hanidec" {
const HANIDEC: [char; 10] = ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
return s
.chars()
.map(|c| {
if c.is_ascii_digit() {
HANIDEC[(c as u8 - b'0') as usize]
} else {
c
}
})
.collect();
}
match numbering_system_digit_base(nu) {
Some(base) if base != 0x0030 => s
.chars()
.map(|c| {
if c.is_ascii_digit() {
char::from_u32(base + (c as u32 - '0' as u32)).unwrap_or(c)
} else {
c
}
})
.collect(),
_ => s,
}
}
#[cfg(feature = "intl")]
fn numbering_system_name_from_zero(c: char) -> Option<&'static str> {
Some(match c as u32 {
0x0030 => "latn",
0x0660 => "arab",
0x06F0 => "arabext",
0x1B50 => "bali",
0x09E6 => "beng",
0x0966 => "deva",
0xFF10 => "fullwide",
0x0AE6 => "gujr",
0x0A66 => "guru",
0x17E0 => "khmr",
0x0CE6 => "knda",
0x0ED0 => "laoo",
0x1946 => "limb",
0x0D66 => "mlym",
0x1810 => "mong",
0x1040 => "mymr",
0x0B66 => "orya",
0x104A0 => "osma",
0xA8D0 => "saur",
0x1BB0 => "sund",
0x19D0 => "talu",
0x0BE6 => "tamldec",
0x0C66 => "telu",
0x0E50 => "thai",
0x0F20 => "tibt",
0xA620 => "vaii",
_ => return None,
})
}
#[cfg(feature = "intl")]
fn dec_round_up(
digits: &[u8],
cut: usize,
mode: intl::number::RoundingMode,
negative: bool,
) -> bool {
use intl::number::RoundingMode::*;
if cut >= digits.len() {
return false;
}
let first = digits[cut];
let rest_nonzero = digits[cut + 1..].iter().any(|&d| d != 0);
let any = first != 0 || rest_nonzero;
let gt_half = first > 5 || (first == 5 && rest_nonzero);
let eq_half = first == 5 && !rest_nonzero;
let kept_last_odd = cut > 0 && digits[cut - 1] % 2 == 1;
match mode {
Trunc => false,
Expand => any,
Ceil => any && !negative,
Floor => any && negative,
HalfExpand => gt_half || eq_half,
HalfTrunc => gt_half,
HalfEven => gt_half || (eq_half && kept_last_odd),
HalfCeil => gt_half || (eq_half && !negative),
HalfFloor => gt_half || (eq_half && negative),
}
}
#[cfg(feature = "intl")]
fn dec_add_units(digits: &mut alloc::vec::Vec<u8>, point: &mut usize, delta: u64) {
let mut carry = delta;
let mut i = digits.len();
while carry > 0 && i > 0 {
i -= 1;
let sum = digits[i] as u64 + carry;
digits[i] = (sum % 10) as u8;
carry = sum / 10;
}
while carry > 0 {
digits.insert(0, (carry % 10) as u8);
carry /= 10;
*point += 1;
}
}
#[cfg(feature = "intl")]
fn dec_sub_units(digits: &mut [u8], mut delta: u64) {
let mut i = digits.len();
while delta > 0 && i > 0 {
i -= 1;
let cur = digits[i] as i64 - (delta % 10) as i64;
delta /= 10;
if cur < 0 {
digits[i] = (cur + 10) as u8;
delta += 1;
} else {
digits[i] = cur as u8;
}
}
}
#[cfg(feature = "intl")]
fn intl_decimal_round(
n: f64,
keep_frac: usize,
sig: Option<usize>,
increment: u32,
mode: intl::number::RoundingMode,
) -> f64 {
if !n.is_finite() || n == 0.0 {
return n;
}
let negative = n.is_sign_negative();
let abs = n.abs();
let s = alloc::format!("{abs}");
let (ip, fp) = s.split_once('.').unwrap_or((s.as_str(), ""));
let mut digits: alloc::vec::Vec<u8> = ip.bytes().chain(fp.bytes()).map(|b| b - b'0').collect();
let mut point = ip.len();
if increment > 1 && sig.is_none() {
let inc = increment as u64;
while digits.len() < point + keep_frac {
digits.push(0);
}
let cut = point + keep_frac;
let mut rem_int: u64 = 0;
let mut q_low: u64 = 0;
for &d in &digits[..cut] {
let cur = rem_int * 10 + d as u64;
q_low = q_low.wrapping_mul(10).wrapping_add(cur / inc);
rem_int = cur % inc;
}
let first = digits.get(cut).copied().unwrap_or(0);
let rest_nonzero = cut < digits.len() && digits[cut + 1..].iter().any(|&d| d != 0);
let frac_pos = cut < digits.len() && (first != 0 || rest_nonzero);
let frac_gt_half = first > 5 || (first == 5 && rest_nonzero);
let frac_eq_half = first == 5 && !rest_nonzero;
digits.truncate(cut);
let twice = rem_int * 2;
let tie = {
use intl::number::RoundingMode::*;
match mode {
Trunc | HalfTrunc => false,
Floor => negative,
Ceil => !negative,
HalfFloor => negative,
HalfCeil | HalfExpand | Expand => true,
HalfEven => (q_low & 1) == 1,
}
};
let up_snap = match twice.cmp(&inc) {
core::cmp::Ordering::Greater => true,
core::cmp::Ordering::Equal => {
if frac_pos {
true
} else {
tie
}
}
core::cmp::Ordering::Less => {
if inc - twice == 1 {
frac_gt_half || (frac_eq_half && tie)
} else {
false
}
}
};
if up_snap {
dec_add_units(&mut digits, &mut point, inc - rem_int);
} else if rem_int > 0 {
dec_sub_units(&mut digits, rem_int);
}
} else {
let cut = if let Some(ms) = sig {
match digits.iter().position(|&d| d != 0) {
Some(fz) => (fz + ms).min(digits.len()),
None => point,
}
} else {
(point + keep_frac).min(digits.len())
};
let up = dec_round_up(&digits, cut, mode, negative);
for d in digits.iter_mut().skip(cut).take(point.saturating_sub(cut)) {
*d = 0;
}
digits.truncate(cut.max(point));
if up {
let mut i = cut;
loop {
if i == 0 {
digits.insert(0, 1);
point += 1;
break;
}
i -= 1;
if digits[i] == 9 {
digits[i] = 0;
} else {
digits[i] += 1;
break;
}
}
}
}
let int_s: String = digits[..point]
.iter()
.map(|&d| (b'0' + d) as char)
.collect();
let frac_s: String = digits[point..]
.iter()
.map(|&d| (b'0' + d) as char)
.collect();
let mut out = String::new();
if negative {
out.push('-');
}
out.push_str(if int_s.is_empty() { "0" } else { &int_s });
if !frac_s.is_empty() {
out.push('.');
out.push_str(&frac_s);
}
out.parse::<f64>().unwrap_or(n)
}
#[cfg(feature = "intl")]
fn accounting_uses_parens(locale: &str) -> bool {
let lang = locale
.split(['-', '_'])
.next()
.unwrap_or(locale)
.to_ascii_lowercase();
!matches!(lang.as_str(), "de" | "nl" | "fi" | "hu" | "et")
}
fn weekday_to_string(s: &str) -> Option<String> {
Some(String::from(match s {
"1" => "mon",
"2" => "tue",
"3" => "wed",
"4" => "thu",
"5" => "fri",
"6" => "sat",
"0" | "7" => "sun",
other if is_unicode_type_value(other) => return Some(String::from(other)),
_ => return None,
}))
}
fn split_u_keyword(locale: &str, key: &str) -> (String, Option<String>) {
let segs: Vec<&str> = locale.split('-').collect();
let mut ustart = None;
for (i, s) in segs.iter().enumerate() {
if s.len() == 1 && s.eq_ignore_ascii_case("u") {
ustart = Some(i);
break;
}
}
let Some(ustart) = ustart else {
return (String::from(locale), None);
};
let mut ext_end = ustart + 1;
while ext_end < segs.len() && segs[ext_end].len() != 1 {
ext_end += 1;
}
let is_key = |s: &str| s.len() == 2 && s.bytes().all(|b| b.is_ascii_alphanumeric());
let mut found: Option<String> = None;
let mut kept: Vec<&str> = Vec::new();
let mut j = ustart + 1;
while j < ext_end {
let cur = segs[j];
if is_key(cur) {
let mut k = j + 1;
while k < ext_end && !is_key(segs[k]) {
k += 1;
}
if cur.eq_ignore_ascii_case(key) {
found = Some(segs[j + 1..k].join("-").to_ascii_lowercase());
} else {
kept.extend_from_slice(&segs[j..k]);
}
j = k;
} else {
kept.push(cur);
j += 1;
}
}
if found.is_none() {
return (String::from(locale), None);
}
let mut out: Vec<&str> = segs[..ustart].to_vec();
if !kept.is_empty() {
out.push("u");
out.extend_from_slice(&kept);
}
out.extend_from_slice(&segs[ext_end..]);
(out.join("-"), found)
}
#[cfg(feature = "intl")]
fn exact_round_up(dropped: &[u8], mode: intl::number::RoundingMode, neg: bool) -> bool {
use intl::number::RoundingMode::*;
if dropped.is_empty() {
return false;
}
let first = dropped[0];
let rest_nonzero = dropped[1..].iter().any(|&d| d != 0);
let any_nonzero = first != 0 || rest_nonzero;
match mode {
Trunc => false,
Expand => any_nonzero,
Ceil => any_nonzero && !neg,
Floor => any_nonzero && neg,
HalfExpand => first >= 5,
HalfTrunc => first > 5 || (first == 5 && rest_nonzero),
HalfCeil => (first > 5 || (first == 5 && rest_nonzero)) || (first == 5 && !neg),
HalfFloor => (first > 5 || (first == 5 && rest_nonzero)) || (first == 5 && neg),
_ => first >= 5,
}
}
#[cfg(feature = "intl")]
fn exact_increment(int: &mut alloc::vec::Vec<u8>, frac: &mut [u8]) {
let mut carry = 1u8;
for d in frac.iter_mut().rev() {
let s = *d + carry;
*d = s % 10;
carry = s / 10;
if carry == 0 {
return;
}
}
for d in int.iter_mut().rev() {
let s = *d + carry;
*d = s % 10;
carry = s / 10;
if carry == 0 {
return;
}
}
if carry > 0 {
int.insert(0, carry);
}
}
#[cfg(feature = "intl")]
fn intl_mode_to_rounding(mode: intl::number::RoundingMode, neg: bool) -> puremp::decimal::Rounding {
use intl::number::RoundingMode as M;
use puremp::decimal::Rounding as R;
match mode {
M::Trunc => R::Down,
M::Expand => R::Up,
M::Floor => R::Floor,
M::Ceil => R::Ceiling,
M::HalfExpand => R::HalfUp,
M::HalfTrunc => R::HalfDown,
M::HalfEven => R::HalfEven,
M::HalfCeil => {
if neg {
R::HalfDown
} else {
R::HalfUp
}
}
M::HalfFloor => {
if neg {
R::HalfUp
} else {
R::HalfDown
}
}
}
}
#[cfg(feature = "intl")]
fn exact_significant_digits(
neg: bool,
int_part: &str,
frac_part: &str,
min_sig: Option<usize>,
max_sig: Option<usize>,
mode: intl::number::RoundingMode,
) -> Option<(alloc::vec::Vec<u8>, alloc::vec::Vec<u8>)> {
use puremp::decimal::Decimal;
let mut lit = String::new();
if neg {
lit.push('-');
}
lit.push_str(if int_part.is_empty() { "0" } else { int_part });
if !frac_part.is_empty() {
lit.push('.');
lit.push_str(frac_part);
}
let d: Decimal = lit.parse().ok()?;
let rounded = match max_sig {
Some(m) => d.round_to_digits(m.max(1) as u32, intl_mode_to_rounding(mode, neg)),
None => d,
};
let s = alloc::format!("{}", rounded.abs());
let (ip, fp) = s.split_once('.').unwrap_or((s.as_str(), ""));
let mut int_digits: alloc::vec::Vec<u8> = ip.bytes().map(|b| b - b'0').collect();
let mut frac_digits: alloc::vec::Vec<u8> = fp.bytes().map(|b| b - b'0').collect();
if int_digits.is_empty() {
int_digits.push(0);
}
if let Some(min_sig) = min_sig {
let first = int_digits
.iter()
.chain(frac_digits.iter())
.position(|&d| d != 0)
.unwrap_or(int_digits.len());
let sig_now = (int_digits.len() + frac_digits.len()).saturating_sub(first);
if sig_now < min_sig {
frac_digits.resize(frac_digits.len() + (min_sig - sig_now), 0);
}
}
Some((int_digits, frac_digits))
}
#[cfg(feature = "intl")]
fn compact_wants_reround(opts: &intl::number::NumberFormatOptions) -> bool {
matches!(opts.notation, intl::number::Notation::Compact)
&& opts.minimum_fraction_digits.is_none()
&& opts.maximum_fraction_digits.is_none()
&& opts.minimum_significant_digits.is_none()
&& opts.maximum_significant_digits.is_none()
}
#[cfg(feature = "intl")]
fn compact_reround_parts(
parts: &mut alloc::vec::Vec<(&'static str, String)>,
mode: intl::number::RoundingMode,
) {
let Some(int_idx) = parts.iter().position(|(k, _)| *k == "integer") else {
return;
};
if parts.get(int_idx + 1).map(|(k, _)| *k) == Some("group") {
return;
}
let neg = parts.iter().any(|(k, _)| *k == "minusSign");
let int_str = parts[int_idx].1.clone();
let (dec_idx, frac_idx) = if parts.get(int_idx + 1).map(|(k, _)| *k) == Some("decimal") {
let f = if parts.get(int_idx + 2).map(|(k, _)| *k) == Some("fraction") {
Some(int_idx + 2)
} else {
None
};
(Some(int_idx + 1), f)
} else {
(None, None)
};
let frac_str = frac_idx.map(|i| parts[i].1.clone()).unwrap_or_default();
if !int_str.bytes().all(|b| b.is_ascii_digit()) || !frac_str.bytes().all(|b| b.is_ascii_digit())
{
return;
}
let int_stripped = int_str.trim_start_matches('0');
let e: i32 = if !int_stripped.is_empty() {
int_stripped.len() as i32 - 1
} else {
match frac_str.bytes().position(|b| b != b'0') {
Some(p) => -(p as i32) - 1,
None => 0,
}
};
let keep = (1 - e).max(0) as usize;
let mut int_d: alloc::vec::Vec<u8> = int_str.bytes().map(|b| b - b'0').collect();
let mut frac_d: alloc::vec::Vec<u8> = frac_str.bytes().map(|b| b - b'0').collect();
if frac_d.len() > keep {
let up = exact_round_up(&frac_d[keep..], mode, neg);
frac_d.truncate(keep);
if up {
exact_increment(&mut int_d, &mut frac_d);
}
}
while frac_d.last() == Some(&0) {
frac_d.pop();
}
while int_d.len() > 1 && int_d[0] == 0 {
int_d.remove(0);
}
let new_int: String = int_d.iter().map(|d| (b'0' + d) as char).collect();
let new_frac: String = frac_d.iter().map(|d| (b'0' + d) as char).collect();
let dec_sep = dec_idx.map(|i| parts[i].1.clone());
if let Some(fi) = frac_idx {
parts.remove(fi);
}
if let Some(di) = dec_idx {
parts.remove(di);
}
parts[int_idx].1 = new_int;
if !new_frac.is_empty() {
let sep = dec_sep.unwrap_or_else(|| String::from("."));
parts.insert(int_idx + 1, ("decimal", sep));
parts.insert(int_idx + 2, ("fraction", new_frac));
}
}
#[cfg(feature = "intl")]
fn split_compact_affix_parts(parts: &mut alloc::vec::Vec<(&'static str, String)>) {
let needs = parts.iter().any(|(k, v)| {
*k == "compact" && (v.starts_with(char::is_whitespace) || v.ends_with(char::is_whitespace))
});
if !needs {
return;
}
let mut out: alloc::vec::Vec<(&'static str, String)> =
alloc::vec::Vec::with_capacity(parts.len() + 2);
for (k, v) in core::mem::take(parts) {
if k != "compact" || v.trim().is_empty() {
out.push((k, v));
continue;
}
let lead_len = v.len() - v.trim_start().len();
let core_end = v.trim_end().len();
if lead_len > 0 {
out.push(("literal", String::from(&v[..lead_len])));
}
out.push(("compact", String::from(&v[lead_len..core_end])));
if core_end < v.len() {
out.push(("literal", String::from(&v[core_end..])));
}
}
*parts = out;
}
#[cfg(feature = "intl")]
fn split_number_scaffold(probe: &str) -> (String, String, String) {
let chars: alloc::vec::Vec<char> = probe.chars().collect();
let mut i = 0;
let mut prefix = String::new();
while i < chars.len() && !chars[i].is_ascii_digit() {
prefix.push(chars[i]);
i += 1;
}
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
let mut sep = String::new();
while i < chars.len() && !chars[i].is_ascii_digit() {
sep.push(chars[i]);
i += 1;
}
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
let suffix: String = chars[i..].iter().collect();
(prefix, sep, suffix)
}
#[cfg(feature = "intl")]
fn extract_group_sep(probe: &str) -> String {
let chars: alloc::vec::Vec<char> = probe.chars().collect();
let mut i = 0;
while i < chars.len() && !chars[i].is_ascii_digit() {
i += 1; }
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1; }
let mut sep = String::new();
while i < chars.len() && !chars[i].is_ascii_digit() {
sep.push(chars[i]);
i += 1;
}
sep
}
#[cfg(feature = "intl")]
fn to_raw_fixed(
neg: bool,
mut int: alloc::vec::Vec<u8>,
mut frac: alloc::vec::Vec<u8>,
max_frac: usize,
mode: intl::number::RoundingMode,
) -> (alloc::vec::Vec<u8>, alloc::vec::Vec<u8>) {
if frac.len() > max_frac {
let up = exact_round_up(&frac[max_frac..], mode, neg);
frac.truncate(max_frac);
if up {
exact_increment(&mut int, &mut frac);
}
}
(int, frac)
}
#[cfg(feature = "intl")]
fn to_raw_precision(
neg: bool,
int: alloc::vec::Vec<u8>,
frac: alloc::vec::Vec<u8>,
max_sig: usize,
mode: intl::number::RoundingMode,
) -> (alloc::vec::Vec<u8>, alloc::vec::Vec<u8>) {
let mut digits: alloc::vec::Vec<u8> = int.iter().chain(frac.iter()).copied().collect();
let mut point = int.len();
let Some(first_sig) = digits.iter().position(|&d| d != 0) else {
return (int, frac);
};
let last = first_sig + max_sig.max(1) - 1;
if last + 1 < digits.len() {
let up = exact_round_up(&digits[last + 1..], mode, neg);
digits.truncate(last + 1);
if up {
let mut carry = 1u8;
for d in digits.iter_mut().rev() {
let s = *d + carry;
*d = s % 10;
carry = s / 10;
if carry == 0 {
break;
}
}
if carry > 0 {
digits.insert(0, carry);
point += 1;
}
}
}
while digits.len() < point {
digits.push(0);
}
let int_out = digits[..point].to_vec();
let frac_out = digits[point..].to_vec();
(int_out, frac_out)
}
#[cfg(feature = "intl")]
fn group_thousands_sep(int_str: &str, sep: &str) -> String {
if sep.is_empty() || int_str.len() <= 3 {
return String::from(int_str);
}
let bytes = int_str.as_bytes();
let mut out = String::new();
let first = bytes.len() % 3;
let first = if first == 0 { 3 } else { first };
out.push_str(&int_str[..first]);
let mut i = first;
while i < bytes.len() {
out.push_str(sep);
out.push_str(&int_str[i..i + 3]);
i += 3;
}
out
}
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),
}
}
fn grandfathered_canonical(base: &str) -> Option<&'static str> {
Some(match base {
"art-lojban" => "jbo",
"cel-gaulish" => "xtg",
"zh-guoyu" => "zh",
"zh-hakka" => "hak",
"zh-xiang" => "hsn",
_ => return None,
})
}
pub(crate) fn unicode_type_alias(key: &str, value: &str) -> Option<&'static str> {
Some(match (key, value) {
("ca", "ethiopic-amete-alem") => "ethioaa",
("ca", "islamicc") => "islamic-civil",
("ks", "primary") => "level1",
("ks", "tertiary") => "level3",
("ms", "imperial") => "uksystem",
("tz", v) => return super::intl_aliases::lookup(super::intl_aliases::TIMEZONE, v),
("rg" | "sd", v) => {
return super::intl_aliases::lookup(super::intl_aliases::SUBDIVISION, v);
}
_ => return None,
})
}
pub(crate) fn transform_value_alias(value: &str) -> Option<&'static str> {
super::intl_aliases::lookup(super::intl_aliases::TRANSFORM_VALUE, value)
}
#[cfg(feature = "intl")]
pub(crate) fn locale_unicode_calendar(locale: &str) -> Option<String> {
let lower = locale.to_ascii_lowercase();
let subtags: Vec<&str> = lower.split('-').collect();
let mut i = 0;
while i < subtags.len() {
if subtags[i] == "u" {
let mut j = i + 1;
while j < subtags.len() && subtags[j] != "u" {
if subtags[j] == "ca" {
let mut parts = Vec::new();
let mut k = j + 1;
while k < subtags.len() && subtags[k].len() >= 3 {
parts.push(subtags[k]);
k += 1;
}
if parts.is_empty() {
return None;
}
let value = parts.join("-");
return Some(
unicode_type_alias("ca", &value)
.map(String::from)
.unwrap_or(value),
);
}
j += 1;
}
}
i += 1;
}
None
}
pub(crate) fn locale_unicode_keyword(locale: &str, key: &str) -> Option<String> {
let lower = locale.to_ascii_lowercase();
let subtags: Vec<&str> = lower.split('-').collect();
let mut i = 0;
while i < subtags.len() {
if subtags[i] == "x" {
break;
}
if subtags[i] == "u" {
let mut j = i + 1;
while j < subtags.len() && subtags[j].len() != 1 {
if subtags[j] == key {
let mut parts = Vec::new();
let mut k = j + 1;
while k < subtags.len() && subtags[k].len() >= 3 {
parts.push(subtags[k]);
k += 1;
}
if parts.is_empty() {
return None;
}
return Some(parts.join("-"));
}
j += 1;
}
}
i += 1;
}
None
}
pub(crate) fn locale_unicode_bool_keyword(locale: &str, key: &str) -> Option<bool> {
let lower = locale.to_ascii_lowercase();
let subs: Vec<&str> = lower.split('-').collect();
let mut i = 0;
while i < subs.len() {
if subs[i] == "x" {
break;
}
if subs[i] == "u" {
let mut j = i + 1;
while j < subs.len() && subs[j].len() != 1 {
if subs[j] == key {
return Some(match subs.get(j + 1) {
Some(v) if v.len() >= 3 => *v != "false",
_ => true,
});
}
j += 1;
}
}
i += 1;
}
None
}
pub(crate) fn is_supported_collation(co: &str) -> bool {
matches!(
co,
"compat"
| "dict"
| "emoji"
| "eor"
| "phonebk"
| "phonetic"
| "pinyin"
| "searchjl"
| "stroke"
| "trad"
| "unihan"
| "zhuyin"
)
}
#[cfg(feature = "intl")]
fn dtf_pad_time_parts(
parts: alloc::vec::Vec<intl::datetime::DateTimePart>,
) -> Vec<(&'static str, String)> {
use intl::datetime::DateTimePartType;
let has_hour = parts.iter().any(|p| p.kind == DateTimePartType::Hour);
let has_min = parts.iter().any(|p| p.kind == DateTimePartType::Minute);
let has_sec = parts.iter().any(|p| p.kind == DateTimePartType::Second);
parts
.into_iter()
.map(|p| {
let mut v = p.value;
let widen = match p.kind {
DateTimePartType::Minute => has_hour || has_sec,
DateTimePartType::Second => has_hour || has_min,
_ => false,
};
if widen && v.len() == 1 && v.as_bytes()[0].is_ascii_digit() {
v.insert(0, '0');
}
(p.kind.as_str(), v)
})
.collect()
}
#[cfg(feature = "intl")]
fn sexagenary_year_name(cyclic1: i64) -> String {
const STEMS: [&str; 10] = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
const BRANCHES: [&str; 12] = [
"子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥",
];
let i = (cyclic1 - 1).rem_euclid(60);
alloc::format!(
"{}{}",
STEMS[(i % 10) as usize],
BRANCHES[(i % 12) as usize]
)
}
pub(crate) const NUMBERING_SYSTEMS: &[&str] = &[
"adlm", "ahom", "arab", "arabext", "armn", "armnlow", "bali", "beng", "bhks", "brah", "cakm",
"cham", "cyrl", "deva", "diak", "ethi", "finance", "fullwide", "gara", "geor", "gong", "gonm",
"grek", "greklow", "gujr", "gukh", "guru", "hanidays", "hanidec", "hans", "hansfin", "hant",
"hantfin", "hebr", "hmng", "hmnp", "java", "jpan", "jpanfin", "jpanyear", "kali", "kawi",
"khmr", "knda", "krai", "lana", "lanatham", "laoo", "latn", "lepc", "limb", "mathbold",
"mathdbl", "mathmono", "mathsanb", "mathsans", "mlym", "modi", "mong", "mroo", "mtei", "mymr",
"mymrepka", "mymrpao", "mymrshan", "mymrtlng", "nagm", "native", "newa", "nkoo", "olck",
"onao", "orya", "osma", "outlined", "rohg", "roman", "romanlow", "saur", "segment", "shrd",
"sind", "sinh", "sora", "sund", "sunu", "takr", "talu", "taml", "tamldec", "tnsa", "telu",
"thai", "tirh", "tibt", "tols", "traditio", "vaii", "wara", "wcho",
];
pub(crate) fn is_known_numbering_system(nu: &str) -> bool {
NUMBERING_SYSTEMS.contains(&nu)
}
#[cfg(feature = "intl")]
fn decimal_magnitude(a: f64) -> i32 {
if a <= 0.0 || !a.is_finite() {
return 0;
}
let mut m = 0i32;
let mut x = a;
if x >= 1.0 {
while x >= 10.0 {
x /= 10.0;
m += 1;
}
} else {
while x < 1.0 {
x *= 10.0;
m -= 1;
}
}
m
}
#[cfg(feature = "intl")]
fn plural_notation_operand_string(n: f64, notation: &str) -> Option<String> {
let a = n.abs();
if a == 0.0 || !a.is_finite() {
return None;
}
let mag = decimal_magnitude(a);
let e = match notation {
"scientific" => mag,
"engineering" => (mag as f64 / 3.0).floor() as i32 * 3,
"compact" => {
if mag < 3 {
0
} else {
(mag as f64 / 3.0).floor() as i32 * 3
}
}
_ => return None,
};
if e <= 0 {
return None;
}
let mantissa = a / libm_pow10(e);
Some(alloc::format!("{mantissa}c{e}"))
}
#[cfg(feature = "intl")]
fn libm_pow10(e: i32) -> f64 {
let mut p = 1.0f64;
for _ in 0..e {
p *= 10.0;
}
p
}
pub(crate) fn default_numbering_for_locale_str(locale: &str) -> String {
#[cfg(feature = "intl")]
{
let zero = intl::number::format_decimal_native(locale, 0.0);
if let Some(c) = zero.chars().next()
&& let Some(name) = numbering_system_name_from_zero(c)
{
return String::from(name);
}
}
let _ = locale;
String::from("latn")
}
pub(crate) fn resolve_nu_key(base: &str, locale: &str, option: Option<&str>) -> (String, String) {
let selectable = |nu: &str| {
is_known_numbering_system(nu) && !matches!(nu, "native" | "traditio" | "finance")
};
let mut value = default_numbering_for_locale_str(base);
let mut addition = String::new();
#[cfg(feature = "intl")]
if let Some(ext) = locale_unicode_keyword(locale, "nu")
&& selectable(&ext)
{
value = ext.clone();
addition = alloc::format!("-nu-{ext}");
}
let _ = locale;
if let Some(opt) = option
&& selectable(opt)
&& opt != value
{
value = String::from(opt);
addition = String::new();
}
(value, addition)
}
pub(crate) const AVAILABLE_CALENDARS: [&str; 16] = [
"buddhist",
"chinese",
"coptic",
"dangi",
"ethioaa",
"ethiopic",
"gregory",
"hebrew",
"indian",
"islamic-civil",
"islamic-tbla",
"islamic-umalqura",
"iso8601",
"japanese",
"persian",
"roc",
];
pub(crate) fn canonicalize_calendar(value: &str) -> String {
let lower = value.to_ascii_lowercase();
let canon = unicode_type_alias("ca", &lower)
.map(String::from)
.unwrap_or(lower);
match canon.as_str() {
"islamic" | "islamic-rgsa" => String::from("islamic-civil"),
_ => canon,
}
}
pub(crate) fn resolve_ca_key(_base: &str, locale: &str, option: Option<&str>) -> (String, String) {
let mut value = String::from("gregory");
let mut addition = String::new();
#[cfg(feature = "intl")]
if let Some(ext) = locale_unicode_calendar(locale) {
let ext = canonicalize_calendar(&ext);
if AVAILABLE_CALENDARS.contains(&ext.as_str()) {
addition = alloc::format!("-ca-{ext}");
value = ext;
}
}
let _ = locale;
if let Some(opt) = option {
let opt = canonicalize_calendar(opt);
if AVAILABLE_CALENDARS.contains(&opt.as_str()) && opt != value {
value = opt;
addition = String::new();
}
}
(value, addition)
}
pub(crate) fn strip_unicode_extension(locale: &str) -> String {
let mut out: Vec<&str> = Vec::new();
for sub in locale.split('-') {
if sub.len() == 1 {
break; }
out.push(sub);
}
out.join("-")
}
pub(crate) fn build_resolved_locale(base: &str, additions: &[String]) -> String {
let mut adds: Vec<&String> = additions.iter().filter(|a| !a.is_empty()).collect();
if adds.is_empty() {
return String::from(base);
}
adds.sort();
let mut out = String::from(base);
out.push_str("-u");
for a in adds {
out.push_str(a);
}
out
}
fn canonicalize_tlang(tlang: &str) -> Option<String> {
let structural = canonicalize_locale_id_structural(tlang)?;
#[cfg(feature = "intl")]
{
Some(
intl::locale::canonicalize(&structural)
.and_then(|t| canonicalize_locale_id_structural(&t))
.unwrap_or(structural),
)
}
#[cfg(not(feature = "intl"))]
{
Some(structural)
}
}
pub(crate) fn canonicalize_locale_id(tag: &str) -> Option<String> {
let structural = canonicalize_locale_id_structural(tag)?;
let subs: Vec<&str> = structural.split('-').collect();
let ext_at = subs.iter().position(|s| s.len() == 1).unwrap_or(subs.len());
let base = subs[..ext_at].join("-");
#[cfg(feature = "intl")]
let aliased_base = intl::locale::canonicalize(&base)
.and_then(|b| canonicalize_locale_id_structural(&b))
.unwrap_or(base);
#[cfg(not(feature = "intl"))]
let aliased_base = base;
if ext_at == subs.len() {
Some(aliased_base)
} else {
Some(alloc::format!(
"{aliased_base}-{}",
subs[ext_at..].join("-")
))
}
}
fn canonicalize_locale_id_structural(tag: &str) -> Option<String> {
if tag.is_empty() || !tag.is_ascii() || tag.contains('_') {
return None;
}
{
let lower = tag.to_ascii_lowercase();
let subs: Vec<&str> = lower.split('-').collect();
let base_end = subs.iter().position(|p| p.len() == 1).unwrap_or(subs.len());
if let Some(repl) = grandfathered_canonical(&subs[..base_end].join("-")) {
let mut rebuilt = String::from(repl);
for p in &subs[base_end..] {
rebuilt.push('-');
rebuilt.push_str(p);
}
return canonicalize_locale_id_structural(&rebuilt);
}
}
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)
}
pub(crate) fn validate_display_code(ty: &str, code: &str) -> Option<String> {
let is_alpha = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_alphabetic());
let is_digit = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit());
let is_alnum = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_alphanumeric());
match ty {
"language" => {
if code.split('-').any(|p| p.len() == 1) {
return None;
}
canonicalize_locale_id(code)
}
"region" => {
if code.len() == 2 && is_alpha(code) {
Some(code.to_ascii_uppercase())
} else if code.len() == 3 && is_digit(code) {
Some(String::from(code))
} else {
None
}
}
"script" => {
if code.len() == 4 && is_alpha(code) {
let mut t = String::new();
for (i, c) in code.chars().enumerate() {
if i == 0 {
t.push(c.to_ascii_uppercase());
} else {
t.push(c.to_ascii_lowercase());
}
}
Some(t)
} else {
None
}
}
"currency" => (code.len() == 3 && is_alpha(code)).then(|| code.to_ascii_uppercase()),
"calendar" => {
if code
.split('-')
.all(|p| (3..=8).contains(&p.len()) && is_alnum(p))
{
Some(code.to_ascii_lowercase())
} else {
None
}
}
"dateTimeField" => {
const FIELDS: &[&str] = &[
"era",
"year",
"quarter",
"month",
"weekOfYear",
"weekday",
"day",
"dayPeriod",
"hour",
"minute",
"second",
"timeZoneName",
];
FIELDS.contains(&code).then(|| String::from(code))
}
_ => Some(String::from(code)),
}
}
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 let Some(canon) = unicode_type_alias(&key, &vals.join("-")) {
vals = canon.split('-').map(String::from).collect();
}
if vals.len() == 1
&& vals[0] == "yes"
&& matches!(key.as_str(), "kb" | "kc" | "kh" | "kk" | "kn")
{
vals[0] = String::from("true");
}
if vals.len() == 1 && vals[0] == "true" {
vals.clear();
}
if !keywords.iter().any(|(k, _)| k == &key) {
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_tlang(&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; }
if let Some(canon) = transform_value_alias(&vals.join("-")) {
vals = canon.split('-').map(String::from).collect();
}
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",
"formatRange",
"formatRangeToParts",
],
bound_accessor: Some(("format", "format")),
},
IntlService {
ctor_id: N_INTL_DATETIME_FORMAT,
tag: "Intl.DateTimeFormat",
marker: "\u{0}brand_dtf",
methods: &[
"resolvedOptions",
"formatToParts",
"formatRange",
"formatRangeToParts",
],
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",
"firstDayOfWeek",
"hourCycle",
"language",
"numberingSystem",
"numeric",
"region",
"script",
"variants",
];
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,
(_, "formatRange" | "formatRangeToParts") => 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,
"formatRange" => N_INTL_FORMAT_RANGE,
"formatRangeToParts" => N_INTL_FORMAT_RANGE_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)
}
pub(crate) 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));
}
}
}
fn set_intl_marker(&mut self, obj: Handle, ctor_id: u16) {
if let Some(svc) = INTL_SERVICES.iter().find(|s| s.ctor_id == ctor_id) {
self.realm
.set_hidden_property(obj, svc.marker, NanBox::boolean(true));
}
}
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);
let len = if selector == "compare" { 2 } else { 1 };
self.install_fn_name_length(bound, "", len);
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_checked(inst, arg0)?;
Ok(self.new_str(&s))
}
}
}
pub(crate) fn intl_format_checked(
&mut self,
inst: Handle,
value: NanBox,
) -> Result<String, ExecError> {
let is_datetime = self
.realm
.get_property(inst, "\u{0}intl")
.map(|k| self.realm.to_display_string(k))
.as_deref()
== Some("datetime");
if is_datetime {
#[cfg(feature = "intl")]
if let Some(s) = self.temporal_format_flat(inst, value, false)? {
return Ok(s);
}
let ms = self.datetime_operand(value)?;
Ok(self.format_intl_datetime(inst, ms))
} else {
#[cfg(feature = "intl")]
if let Some(s) = self.try_exact_decimal_format(inst, value) {
return Ok(s);
}
let n = self.coerce_intl_number(value)?;
Ok(self.intl_format_value(inst, NanBox::number(n)))
}
}
pub(crate) fn coerce_intl_number(&mut self, value: NanBox) -> Result<f64, ExecError> {
if let Some(h) = value.as_handle().map(Handle::from_raw)
&& let Some(big) = self.realm.bigint_at(h)
{
return Ok(big.to_f64());
}
let prim = self.coerce_to_number(value)?;
Ok(self.realm.to_number(prim))
}
pub(crate) fn make_intl_formatter(
&mut self,
id: u16,
args: &[NanBox],
) -> Result<NanBox, ExecError> {
let obj = self.realm.new_object();
self.brand_intl_instance(obj, id);
self.init_intl_formatter_state(obj, id, args)?;
Ok(NanBox::handle(obj.to_raw()))
}
pub(crate) fn init_intl_formatter_state(
&mut self,
obj: Handle,
id: u16,
args: &[NanBox],
) -> Result<(), ExecError> {
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.set_intl_marker(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 if matches!(opts_arg.unpack(), Unpacked::Null) {
return Err(self.type_error("Intl formatter options must not be null"));
} 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(())
}
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))));
}
}
let raw_locale = self
.realm
.get_property(obj, "\u{0}locale")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("en-US"));
let base = strip_unicode_extension(&raw_locale);
let (resolved_nu, add) = resolve_nu_key(&base, &raw_locale, nu.as_deref());
let resolved_locale = build_resolved_locale(&base, &[add]);
let locv = self.new_str(&resolved_locale);
self.realm.set_hidden_property(obj, "\u{0}locale", locv);
self.store_str(obj, "numberingSystem", &Some(resolved_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, ¬ation)?;
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 (mnsd, mxsd) = if mnsd.is_some() || mxsd.is_some() {
(Some(mnsd.unwrap_or(1.0)), Some(mxsd.unwrap_or(21.0)))
} else {
(mnsd, mxsd)
};
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();
if rinc != 1.0 {
let rounding_type = if rounding_priority == "morePrecision" {
"morePrecision"
} else if rounding_priority == "lessPrecision" {
"lessPrecision"
} else if mnsd.is_some() {
"significantDigits"
} else {
"fractionDigits"
};
if rounding_type != "fractionDigits" {
return Err(self.type_error(
"roundingIncrement other than 1 requires the fractionDigits rounding type",
));
}
if let (Some(a), Some(b)) = (mnfd, mxfd)
&& a != b
{
let m = self.new_str("roundingIncrement requires equal min/max fraction digits");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
}
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,
notation: &str,
) -> Result<UseGroupingResolved, ExecError> {
let fallback = if notation == "compact" {
"min2"
} else {
"auto"
};
if matches!(raw.unpack(), Unpacked::Undefined) {
return Ok(UseGroupingResolved::Str(fallback));
}
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(fallback)),
"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))));
}
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))));
}
let raw_locale = self
.realm
.get_property(obj, "\u{0}locale")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("en-US"));
let base = strip_unicode_extension(&raw_locale);
let (resolved_ca, ca_add) = resolve_ca_key(&base, &raw_locale, ca.as_deref());
let (resolved_nu, nu_add) = resolve_nu_key(&base, &raw_locale, nu.as_deref());
let hc_ext = split_u_keyword(&raw_locale, "hc")
.1
.filter(|v| matches!(v.as_str(), "h11" | "h12" | "h23" | "h24"));
let hc_add = hc_ext
.as_deref()
.map(|v| alloc::format!("-hc-{v}"))
.unwrap_or_default();
let resolved_locale = build_resolved_locale(&base, &[ca_add, hc_add, nu_add]);
let locv = self.new_str(&resolved_locale);
self.realm.set_hidden_property(obj, "\u{0}locale", locv);
self.store_str(obj, "calendar", &Some(resolved_ca));
self.store_str(obj, "numberingSystem", &Some(resolved_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 = match self.get_string_option(opts, "timeZone", &[], None)? {
Some(s) => self.dtf_resolve_time_zone(&s)?,
None => String::from("UTC"),
};
self.store_str(obj, "timeZone", &Some(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))));
}
let any_field = weekday.is_some()
|| era.is_some()
|| year.is_some()
|| month.is_some()
|| day.is_some()
|| day_period.is_some()
|| hour.is_some()
|| minute.is_some()
|| second.is_some()
|| fsd.is_some()
|| tzn.is_some();
if !any_field && date_style.is_none() && time_style.is_none() {
let numeric = Some(String::from("numeric"));
self.store_str(obj, "year", &numeric);
self.store_str(obj, "month", &numeric);
self.store_str(obj, "day", &numeric);
self.realm
.set_hidden_property(obj, "\u{0}dtf_default_date", NanBox::boolean(true));
}
Ok(())
}
fn dtf_resolve_time_zone(&mut self, s: &str) -> Result<String, ExecError> {
use super::temporal_zoneddatetime::{parse_offset_id, resolve_named};
if !s.is_empty() {
if let Some((_, canon)) = parse_offset_id(s) {
return Ok(canon);
}
if let Some(name) = resolve_named(s) {
return Ok(name);
}
}
let m = self.new_str(&alloc::format!("invalid time zone: {s}"));
Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))))
}
#[cfg(feature = "intl")]
fn dtf_zone_offset_ms(&self, handle: Handle, epoch_ms: i64) -> i64 {
let Some(tz) = self
.realm
.get_property(handle, "timeZone")
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
else {
return 0;
};
if tz == "UTC" || tz.is_empty() {
return 0;
}
let epoch_ns = i128::from(epoch_ms) * 1_000_000;
(super::temporal_zoneddatetime::tz_offset_at(&tz, epoch_ns) / 1_000_000) as i64
}
#[cfg(feature = "intl")]
fn dtf_apply_temporal_zone(
&self,
handle: Handle,
ms: f64,
kind: crate::temporal_iso::TemporalKind,
o: &mut intl::datetime::DateTimeFormatOptions,
) -> f64 {
use crate::temporal_iso::TemporalKind;
if matches!(kind, TemporalKind::Instant | TemporalKind::ZonedDateTime) {
let off = self.dtf_zone_offset_ms(handle, ms as i64);
if o.time_zone_name.is_some() {
o.tz_offset_minutes = Some((off / 60_000) as i32);
}
ms + off as f64
} else {
ms
}
}
fn locale_hour_defaults(&self, base_locale: &str) -> (&'static str, bool) {
let primary = base_locale.split('-').next().unwrap_or("");
let hc12 = if primary.eq_ignore_ascii_case("ja") {
"h11"
} else {
"h12"
};
#[cfg(feature = "intl")]
{
use intl::datetime::{DateTime, DateTimeFormatOptions, Numeric2Digit};
let dt = DateTime {
year: 2020,
month: 1,
day: 1,
hour: 13,
minute: 0,
second: 0,
millisecond: 0,
};
let mut o = DateTimeFormatOptions::default();
o.hour = Some(Numeric2Digit::Numeric);
if let Ok(parts) = intl::datetime::format_to_parts(base_locale, &dt, &o) {
let is_12h = parts.iter().any(|p| {
matches!(p.kind, intl::datetime::DateTimePartType::DayPeriod)
|| (matches!(p.kind, intl::datetime::DateTimePartType::Hour)
&& p.value != "13")
});
return (hc12, is_12h);
}
}
let is_12h = primary.eq_ignore_ascii_case("en");
(hc12, is_12h)
}
fn dtf_hour_resolution(&self, handle: Handle) -> (String, Option<String>, Option<bool>) {
let raw_locale = self
.realm
.get_property(handle, "\u{0}locale")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("en-US"));
let (base_locale, hc_ext) = split_u_keyword(&raw_locale, "hc");
let hc_ext = hc_ext.filter(|v| matches!(v.as_str(), "h11" | "h12" | "h23" | "h24"));
let get_str = |k: &str| -> Option<String> {
self.realm
.get_property(handle, k)
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
};
let hc_opt = get_str("hourCycle");
let hour12 = self
.realm
.get_property(handle, "hour12")
.and_then(|v| v.as_boolean());
let hour_present = get_str("hour").is_some() || get_str("timeStyle").is_some();
let keep_ext = hc_ext.is_some()
&& hour12.is_none()
&& match &hc_opt {
Some(opt) => Some(opt.as_str()) == hc_ext.as_deref(),
None => true,
};
let resolved_locale = if keep_ext {
raw_locale.clone()
} else {
base_locale.clone()
};
if !hour_present {
return (resolved_locale, None, None);
}
let (hc12, default_is_12h) = self.locale_hour_defaults(&base_locale);
let resolved_hc = match hour12 {
Some(true) => String::from(hc12),
Some(false) => String::from("h23"),
None => hc_opt
.or(hc_ext)
.unwrap_or_else(|| String::from(if default_is_12h { hc12 } else { "h23" })),
};
let h12 = matches!(resolved_hc.as_str(), "h11" | "h12");
(resolved_locale, Some(resolved_hc), Some(h12))
}
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 == "segmenter" {
let gran = get_str(self, "granularity").unwrap_or_else(|| String::from("grapheme"));
let gv = self.new_str(&gran);
self.realm.set_property(out, "granularity", gv);
} else if kind == "collator" {
let usage = get_str(self, "usage").unwrap_or_else(|| String::from("sort"));
let uv = self.new_str(&usage);
self.realm.set_property(out, "usage", uv);
let sensitivity =
get_str(self, "sensitivity").unwrap_or_else(|| String::from("variant"));
let sv = self.new_str(&sensitivity);
self.realm.set_property(out, "sensitivity", sv);
let ip = fmt
.and_then(|h| self.realm.get_property(h, "ignorePunctuation"))
.is_some_and(|v| self.realm.truthy(v));
self.realm
.set_property(out, "ignorePunctuation", NanBox::boolean(ip));
let collation = get_str(self, "collation").unwrap_or_else(|| String::from("default"));
let cv = self.new_str(&collation);
self.realm.set_property(out, "collation", cv);
if let Some(n) = fmt.and_then(|h| self.realm.get_property(h, "numeric")) {
let nv = NanBox::boolean(self.realm.truthy(n));
self.realm.set_property(out, "numeric", nv);
}
if let Some(cf) = get_str(self, "caseFirst") {
let cfv = self.new_str(&cf);
self.realm.set_property(out, "caseFirst", cfv);
}
} else if kind == "display" {
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);
if let Some(t) = get_str(self, "type") {
let tv = self.new_str(&t);
self.realm.set_property(out, "type", tv);
}
let fallback = get_str(self, "fallback").unwrap_or_else(|| String::from("code"));
let fv = self.new_str(&fallback);
self.realm.set_property(out, "fallback", fv);
if get_str(self, "type").as_deref() == Some("language") {
let ld =
get_str(self, "languageDisplay").unwrap_or_else(|| String::from("dialect"));
let ldv = self.new_str(&ld);
self.realm.set_property(out, "languageDisplay", ldv);
}
} else 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 notation = get_str(self, "notation").unwrap_or_else(|| String::from("standard"));
let (def_min, def_max): (f64, f64) = match style.as_str() {
"currency" if notation == "standard" => (2.0, 2.0),
"percent" => (0.0, 0.0),
_ if notation == "compact" => (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 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 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 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(fmt) = fmt {
let (resolved_locale, hour_cycle, hour12) = self.dtf_hour_resolution(fmt);
let lv = self.new_str(&resolved_locale);
self.realm.set_property(out, "locale", lv);
if let Some(hc) = hour_cycle {
let v = self.new_str(&hc);
self.realm.set_property(out, "hourCycle", v);
}
if let Some(h12) = hour12 {
self.realm.set_property(out, "hour12", NanBox::boolean(h12));
}
}
for key in [
"weekday",
"era",
"year",
"month",
"day",
"dayPeriod",
"hour",
"minute",
"second",
] {
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));
}
for key in ["timeZoneName", "dateStyle", "timeStyle"] {
if let Some(v) = get_str(self, key) {
let vv = self.new_str(&v);
self.realm.set_property(out, key, vv);
}
}
}
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 datetime_operand(&mut self, value: NanBox) -> Result<f64, ExecError> {
let x = if matches!(value.unpack(), Unpacked::Undefined) {
now_ms()
} else {
let n = self.coerce_to_number(value)?;
self.realm.to_number(n)
};
if !x.is_finite() || x.abs() > 8.64e15_f64 {
let m = self.new_str("date value is not a finite time value");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
Ok(x)
}
fn intl_range_operands(
&mut self,
this: NanBox,
start: NanBox,
end: NanBox,
) -> Result<(Handle, f64, f64), ExecError> {
let Some(inst) = this
.as_handle()
.map(Handle::from_raw)
.filter(|h| self.realm.get_property(*h, "\u{0}intl").is_some())
else {
return Err(self.type_error("formatRange called on an incompatible receiver"));
};
if matches!(start.unpack(), Unpacked::Undefined)
|| matches!(end.unpack(), Unpacked::Undefined)
{
return Err(self.type_error("formatRange requires two defined arguments"));
}
let is_number_format = self
.realm
.get_property(inst, "\u{0}intl")
.map(|k| self.realm.to_display_string(k))
.as_deref()
== Some("number");
let coerce_operand = |this: &mut Self, v: NanBox| -> Result<f64, ExecError> {
if is_number_format
&& let Some(h) = v.as_handle().map(Handle::from_raw)
&& let Some(big) = this.realm.bigint_at(h)
{
return Ok(big.to_f64());
}
let n = this.coerce_to_number(v)?;
Ok(this.realm.to_number(n))
};
let x = coerce_operand(self, start)?;
let y = coerce_operand(self, end)?;
if x.is_nan() || y.is_nan() {
let m = self.new_str("formatRange arguments must not be NaN");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
let is_datetime = self
.realm
.get_property(inst, "\u{0}intl")
.map(|k| self.realm.to_display_string(k))
.as_deref()
== Some("datetime");
if is_datetime && (!x.is_finite() || x.abs() > 8.64e15_f64 || y.abs() > 8.64e15_f64) {
let m = self.new_str("formatRange date value is not a finite time value");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
Ok((inst, x, y))
}
pub(crate) fn intl_format_range(
&mut self,
this: NanBox,
start: NanBox,
end: NanBox,
) -> Result<String, ExecError> {
let Some(inst) = this
.as_handle()
.map(Handle::from_raw)
.filter(|h| self.realm.get_property(*h, "\u{0}intl").is_some())
else {
return Err(self.type_error("formatRange called on an incompatible receiver"));
};
if self.intl_kind_is_datetime(inst) {
let parts = self.dtf_range_dispatch(inst, start, end)?;
return Ok(parts.iter().map(|(_, v, _)| v.as_str()).collect());
}
let (inst, x, y) = self.intl_range_operands(this, start, end)?;
let fx = self.intl_format_value(inst, NanBox::number(x));
if x == y {
return Ok(fx);
}
let fy = self.intl_format_value(inst, NanBox::number(y));
Ok(alloc::format!("{fx}\u{2013}{fy}"))
}
fn intl_kind_is_datetime(&self, inst: Handle) -> bool {
self.realm
.get_property(inst, "\u{0}intl")
.map(|k| self.realm.to_display_string(k))
.as_deref()
== Some("datetime")
}
pub(crate) fn intl_format_range_to_parts(
&mut self,
this: NanBox,
start: NanBox,
end: NanBox,
) -> Result<NanBox, ExecError> {
let Some(inst) = this
.as_handle()
.map(Handle::from_raw)
.filter(|h| self.realm.get_property(*h, "\u{0}intl").is_some())
else {
return Err(self.type_error("formatRange called on an incompatible receiver"));
};
if self.intl_kind_is_datetime(inst) {
let tagged = self.dtf_range_dispatch(inst, start, end)?;
return Ok(self.intl_build_source_parts(tagged));
}
let (inst, x, y) = self.intl_range_operands(this, start, end)?;
let fx = self.intl_format_value(inst, NanBox::number(x));
let mut parts: Vec<(&str, String, &str)> = alloc::vec![("literal", fx, "startRange")];
if x != y {
let fy = self.intl_format_value(inst, NanBox::number(y));
parts.push(("literal", String::from("\u{2013}"), "shared"));
parts.push(("literal", fy, "endRange"));
}
Ok(self.intl_build_source_parts(parts))
}
fn dtf_resolve_range_operands(
&mut self,
start: NanBox,
end: NanBox,
) -> Result<(bool, f64, bool, f64), ExecError> {
if matches!(start.unpack(), Unpacked::Undefined)
|| matches!(end.unpack(), Unpacked::Undefined)
{
return Err(self.type_error("formatRange requires two defined arguments"));
}
#[cfg(feature = "intl")]
let sx_temporal = self.is_temporal_value(start);
#[cfg(not(feature = "intl"))]
let sx_temporal = false;
let sx_num = if sx_temporal {
0.0
} else {
let n = self.coerce_to_number(start)?;
self.realm.to_number(n)
};
#[cfg(feature = "intl")]
let sy_temporal = self.is_temporal_value(end);
#[cfg(not(feature = "intl"))]
let sy_temporal = false;
let sy_num = if sy_temporal {
0.0
} else {
let n = self.coerce_to_number(end)?;
self.realm.to_number(n)
};
#[cfg(feature = "intl")]
if (sx_temporal || sy_temporal) && self.range_type_tag(start) != self.range_type_tag(end) {
return Err(
self.type_error("formatRange arguments must be of the same Date/Temporal type")
);
}
Ok((sx_temporal, sx_num, sy_temporal, sy_num))
}
fn dtf_range_dispatch(
&mut self,
inst: Handle,
start: NanBox,
end: NanBox,
) -> Result<Vec<(&'static str, String, &'static str)>, ExecError> {
let (sx_temporal, sx_num, sy_temporal, sy_num) =
self.dtf_resolve_range_operands(start, end)?;
#[cfg(feature = "intl")]
{
let cal = self.dtf_resolved_calendar(inst);
let gregorian = matches!(cal.as_str(), "gregory" | "gregorian" | "iso8601");
if gregorian && !sx_temporal && !sy_temporal {
for n in [sx_num, sy_num] {
if !n.is_finite() || n.abs() > 8.64e15_f64 {
let m = self.new_str("date value is not a finite time value");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
}
let sp = self.datetime_parts(inst, sx_num);
let ep = self.datetime_parts(inst, sy_num);
if sp == ep {
return Ok(sp.into_iter().map(|(t, v)| (t, v, "shared")).collect());
}
let crate_parts = self.dtf_range_crate_parts(inst, sx_num, sy_num);
if crate_parts.iter().any(|(_, _, src)| *src != "shared") {
return Ok(crate_parts);
}
let mut v: Vec<(&'static str, String, &'static str)> =
Vec::with_capacity(sp.len() + ep.len() + 1);
for (t, val) in sp {
v.push((t, val, "startRange"));
}
v.push((
"literal",
String::from("\u{2009}\u{2013}\u{2009}"),
"shared",
));
for (t, val) in ep {
v.push((t, val, "endRange"));
}
return Ok(v);
}
}
let sp = self.dtf_operand_parts(inst, start, sx_temporal, sx_num)?;
let ep = self.dtf_operand_parts(inst, end, sy_temporal, sy_num)?;
Ok(if sp == ep {
sp.into_iter().map(|(t, v)| (t, v, "shared")).collect()
} else {
let mut v: Vec<(&'static str, String, &'static str)> =
Vec::with_capacity(sp.len() + ep.len() + 1);
for (t, val) in sp {
v.push((t, val, "startRange"));
}
v.push(("literal", String::from("\u{2013}"), "shared"));
for (t, val) in ep {
v.push((t, val, "endRange"));
}
v
})
}
#[cfg(feature = "intl")]
fn dtf_range_crate_parts(
&mut self,
inst: Handle,
sx_num: f64,
sy_num: f64,
) -> Vec<(&'static str, String, &'static str)> {
use intl::datetime::{self, DateTimePartType};
let (locale, dt1, o) = self.dtf_locale_dt_opts(inst, sx_num);
let (_, dt2, _) = self.dtf_locale_dt_opts(inst, sy_num);
let raw = match datetime::format_range_to_parts(&locale, &dt1, &dt2, &o) {
Ok(parts) => parts,
Err(_) => return Vec::new(),
};
let has_hour = raw.iter().any(|p| p.kind == DateTimePartType::Hour);
let has_min = raw.iter().any(|p| p.kind == DateTimePartType::Minute);
let has_sec = raw.iter().any(|p| p.kind == DateTimePartType::Second);
raw.into_iter()
.map(|p| {
let widen = match p.kind {
DateTimePartType::Minute => has_hour || has_sec,
DateTimePartType::Second => has_hour || has_min,
_ => false,
};
let mut v = p.value;
if widen && v.len() == 1 && v.as_bytes()[0].is_ascii_digit() {
v.insert(0, '0');
}
let value = self.apply_numbering_digits(inst, v);
(p.kind.as_str(), value, p.source.as_str())
})
.collect()
}
#[cfg_attr(not(feature = "intl"), expect(unused_variables))]
fn dtf_operand_parts(
&mut self,
inst: Handle,
value: NanBox,
is_temporal: bool,
num: f64,
) -> Result<Vec<(&'static str, String)>, ExecError> {
#[cfg(feature = "intl")]
if is_temporal && let Some(p) = self.temporal_format_parts(inst, value, false)? {
return Ok(p);
}
if !num.is_finite() || num.abs() > 8.64e15_f64 {
let m = self.new_str("date value is not a finite time value");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
Ok(self.datetime_parts(inst, num))
}
fn intl_build_source_parts(&mut self, parts: Vec<(&str, String, &str)>) -> NanBox {
let mut elems = Vec::with_capacity(parts.len());
for (ty, val, src) in parts {
let o = self.realm.new_object();
let tv = self.new_str(ty);
let vv = self.new_str(&val);
let sv = self.new_str(src);
self.realm.set_property(o, "type", tv);
self.realm.set_property(o, "value", vv);
self.realm.set_property(o, "source", sv);
elems.push(NanBox::handle(o.to_raw()));
}
NanBox::handle(self.realm.new_array(elems).to_raw())
}
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();
self.init_relative_time_format(obj, args)?;
Ok(NanBox::handle(obj.to_raw()))
}
pub(crate) fn init_relative_time_format(
&mut self,
obj: Handle,
args: &[NanBox],
) -> Result<(), ExecError> {
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();
let base = strip_unicode_extension(&locale);
let (resolved_nu, add) = resolve_nu_key(&base, &locale, nu.as_deref());
let resolved_locale = build_resolved_locale(&base, &[add]);
let locv = self.new_str(&resolved_locale);
self.realm.set_hidden_property(obj, "\u{0}locale", locv);
self.store_str(obj, "numberingSystem", &Some(resolved_nu));
self.store_str(obj, "style", &Some(style));
self.store_str(obj, "numeric", &Some(numeric));
self.brand_intl_instance(obj, N_INTL_REL_TIME);
Ok(())
}
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]) -> Result<NanBox, ExecError> {
let obj = self.realm.new_object();
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 = match args.get(1).copied() {
Some(v) if !matches!(v.unpack(), Unpacked::Undefined) => {
if !self.is_object_value(v) {
return Err(self.type_error("Intl.DisplayNames: options must be an object"));
}
v.as_handle().map(Handle::from_raw)
}
_ => None,
};
let _ = self.get_string_option(
opts,
"localeMatcher",
&["lookup", "best fit"],
Some("best fit"),
)?;
let style =
self.get_string_option(opts, "style", &["narrow", "short", "long"], Some("long"))?;
let type_s = self.get_string_option(
opts,
"type",
&[
"language",
"region",
"script",
"currency",
"calendar",
"dateTimeField",
],
None,
)?;
let Some(type_s) = type_s else {
return Err(self.type_error("Intl.DisplayNames: the `type` option is required"));
};
let tv = self.new_str(&type_s);
self.realm.set_hidden_property(obj, "type", tv);
let kindv = self.new_str("display");
self.realm.set_hidden_property(obj, "\u{0}intl", kindv);
self.store_str(obj, "style", &style);
let fallback = self.get_string_option(opts, "fallback", &["code", "none"], Some("code"))?;
self.store_str(obj, "fallback", &fallback);
if type_s == "language" {
let ld = self.get_string_option(
opts,
"languageDisplay",
&["dialect", "standard"],
Some("dialect"),
)?;
self.store_str(obj, "languageDisplay", &ld);
}
self.brand_intl_instance(obj, N_INTL_DISPLAY_NAMES);
Ok(NanBox::handle(obj.to_raw()))
}
pub(crate) fn make_collator(&mut self, args: &[NanBox]) -> Result<NanBox, ExecError> {
let obj = self.realm.new_object();
self.init_collator(obj, args)?;
Ok(NanBox::handle(obj.to_raw()))
}
pub(crate) fn init_collator(&mut self, obj: Handle, args: &[NanBox]) -> Result<(), ExecError> {
let marker = self.new_str("collator");
self.realm.set_hidden_property(obj, "\u{0}intl", marker);
let locale = self
.canonicalize_locale_list(args.first().copied().unwrap_or(NanBox::undefined()))?
.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.Collator options must be an object"));
};
let usage = self
.get_string_option(opts, "usage", &["sort", "search"], Some("sort"))?
.unwrap();
let _ = self.get_string_option(
opts,
"localeMatcher",
&["lookup", "best fit"],
Some("best fit"),
)?;
let collation = self.get_string_option(opts, "collation", &[], None)?;
if let Some(c) = &collation
&& !c
.split('-')
.all(|p| (3..=8).contains(&p.len()) && p.bytes().all(|b| b.is_ascii_alphanumeric()))
{
let m = self.new_str(&alloc::format!("invalid collation option: {c}"));
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
let numeric = self.get_bool_option(opts, "numeric", None)?;
let case_first =
self.get_string_option(opts, "caseFirst", &["upper", "lower", "false"], None)?;
let sensitivity = self
.get_string_option(
opts,
"sensitivity",
&["base", "accent", "case", "variant"],
None,
)?
.unwrap_or_else(|| String::from("variant"));
let primary = locale.split(['-', '_']).next().unwrap_or("");
let ip_default = primary.eq_ignore_ascii_case("th");
let ignore_punct = self
.get_bool_option(opts, "ignorePunctuation", Some(ip_default))?
.unwrap_or(ip_default);
let base = strip_unicode_extension(&locale);
let mut additions: Vec<String> = Vec::new();
let mut co_value = String::from("default");
if let Some(ext) = locale_unicode_keyword(&locale, "co")
&& is_supported_collation(&ext)
{
co_value = ext.clone();
additions.push(alloc::format!("-co-{ext}"));
}
if let Some(opt) = &collation
&& is_supported_collation(opt)
&& *opt != co_value
{
co_value = opt.clone();
additions.retain(|a| !a.starts_with("-co-"));
}
let ext_kn = locale_unicode_bool_keyword(&locale, "kn");
let mut kn_value = ext_kn.unwrap_or(false);
if let Some(b) = ext_kn {
additions.push(if b {
String::from("-kn")
} else {
String::from("-kn-false")
});
}
if let Some(opt) = numeric
&& opt != kn_value
{
kn_value = opt;
additions.retain(|a| a != "-kn" && a != "-kn-false");
}
let kn_reported = ext_kn.is_some() || numeric.is_some();
let mut kf_value: Option<String> = None;
if let Some(ext) = locale_unicode_keyword(&locale, "kf")
&& matches!(ext.as_str(), "upper" | "lower" | "false")
{
kf_value = Some(ext.clone());
additions.push(alloc::format!("-kf-{ext}"));
}
if let Some(opt) = &case_first
&& kf_value.as_deref() != Some(opt.as_str())
{
kf_value = Some(opt.clone());
additions.retain(|a| !a.starts_with("-kf-"));
}
let resolved_locale = build_resolved_locale(&base, &additions);
let rlv = self.new_str(&resolved_locale);
self.realm.set_hidden_property(obj, "\u{0}locale", rlv);
self.store_str(obj, "usage", &Some(usage));
self.store_str(obj, "sensitivity", &Some(sensitivity));
let ipv = NanBox::boolean(ignore_punct);
self.realm
.set_hidden_property(obj, "ignorePunctuation", ipv);
self.store_str(obj, "collation", &Some(co_value));
if kn_reported {
self.realm
.set_hidden_property(obj, "numeric", NanBox::boolean(kn_value));
}
if let Some(cf) = kf_value {
self.store_str(obj, "caseFirst", &Some(cf));
}
self.brand_intl_instance(obj, N_INTL_COLLATOR);
Ok(())
}
#[cfg(feature = "intl")]
pub(crate) fn collator_ordering(
&mut self,
ch: Option<Handle>,
a: &str,
b: &str,
) -> core::cmp::Ordering {
use intl::unicode::collate::{AlternateHandling, Collator, Strength};
let strength = match ch
.and_then(|h| self.realm.get_property(h, "sensitivity"))
.map(|v| self.realm.to_display_string(v))
.as_deref()
{
Some("base") => Strength::Primary,
Some("accent") => Strength::Secondary,
_ => Strength::Tertiary,
};
let numeric = matches!(
ch.and_then(|h| self.realm.get_property(h, "numeric"))
.map(|v| v.unpack()),
Some(Unpacked::Bool(true))
);
let alternate = if matches!(
ch.and_then(|h| self.realm.get_property(h, "ignorePunctuation"))
.map(|v| v.unpack()),
Some(Unpacked::Bool(true))
) {
AlternateHandling::Shifted
} else {
AlternateHandling::NonIgnorable
};
Collator::new(alternate)
.with_strength(strength)
.with_numeric(numeric)
.compare(a, b)
}
pub(crate) fn make_list_format(&mut self, args: &[NanBox]) -> Result<NanBox, ExecError> {
let obj = self.realm.new_object();
self.init_list_format(obj, args)?;
Ok(NanBox::handle(obj.to_raw()))
}
pub(crate) fn init_list_format(
&mut self,
obj: Handle,
args: &[NanBox],
) -> Result<(), ExecError> {
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(())
}
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, middle, end): (&str, &str, &str) = match (list_type, style) {
("disjunction", _) => (" or ", ", ", ", or "),
("unit", "narrow") => (" ", " ", " "),
("unit", _) => (", ", ", ", ", "),
(_, "short") => (" & ", ", ", ", & "), (_, "narrow") => (", ", ", ", ", "), _ => (" and ", ", ", ", and "), };
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();
self.init_plural_rules(obj, args)?;
Ok(NanBox::handle(obj.to_raw()))
}
pub(crate) fn init_plural_rules(
&mut self,
obj: Handle,
args: &[NanBox],
) -> Result<(), ExecError> {
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(())
}
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 notation = fmt
.and_then(|h| self.realm.get_property(h, "notation"))
.map(|v| self.realm.to_display_string(v))
.unwrap_or_else(|| String::from("standard"));
let ops = match plural_notation_operand_string(n, ¬ation) {
Some(s) => intl::plural::PluralOperands::parse(&s)
.unwrap_or_else(|| intl::plural::PluralOperands::from_int(n as i64)),
None if n == (n as i64) as f64 => intl::plural::PluralOperands::from_int(n as i64),
None => 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]) -> Result<NanBox, ExecError> {
let obj = self.realm.new_object();
self.init_segmenter(obj, args)?;
Ok(NanBox::handle(obj.to_raw()))
}
pub(crate) fn init_segmenter(&mut self, obj: Handle, args: &[NanBox]) -> Result<(), ExecError> {
let locale = self
.canonicalize_locale_list(args.first().copied().unwrap_or(NanBox::undefined()))?
.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.Segmenter options must be an object"));
};
let _ = self.get_string_option(
opts,
"localeMatcher",
&["lookup", "best fit"],
Some("best fit"),
)?;
let gran = self
.get_string_option(
opts,
"granularity",
&["grapheme", "word", "sentence"],
Some("grapheme"),
)?
.unwrap();
let granv = self.new_str(&gran);
self.realm.set_hidden_property(obj, "granularity", granv);
let kindv = self.new_str("segmenter");
self.realm.set_hidden_property(obj, "\u{0}intl", kindv);
self.brand_intl_instance(obj, N_INTL_SEGMENTER);
Ok(())
}
#[cfg(feature = "intl")]
pub(crate) fn datetime_parts(
&mut self,
handle: Handle,
ms: f64,
) -> Vec<(&'static str, String)> {
use intl::datetime;
let (locale, dt, o) = self.dtf_locale_dt_opts(handle, ms);
if let Some(p) = self.lunisolar_parts(handle, &locale, &dt, &o) {
return p;
}
match datetime::format_to_parts(&locale, &dt, &o) {
Ok(parts) => dtf_pad_time_parts(parts),
Err(_) => Vec::new(),
}
}
#[cfg(feature = "intl")]
fn dtf_locale_dt_opts(
&mut self,
handle: Handle,
ms: f64,
) -> (
String,
intl::datetime::DateTime,
intl::datetime::DateTimeFormatOptions,
) {
use intl::datetime::{
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 zone_off_ms = self.dtf_zone_offset_ms(handle, ms as i64);
let msi = ms as i64 + zone_off_ms;
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.year.is_none()
&& o.month.is_none()
&& o.day.is_none()
&& o.day_period.is_none()
&& o.hour.is_none()
&& o.minute.is_none()
&& o.second.is_none()
&& o.fractional_second_digits.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((zone_off_ms / 60_000) as i32);
}
(locale, dt, o)
}
#[cfg(feature = "intl")]
fn lunisolar_parts(
&self,
handle: Handle,
locale: &str,
dt: &intl::datetime::DateTime,
o: &intl::datetime::DateTimeFormatOptions,
) -> Option<Vec<(&'static str, String)>> {
use intl::calendar;
use intl::datetime::{self, DateTimeFormatOptions, MonthStyle, Numeric2Digit};
let cal = self.dtf_resolved_calendar(handle);
let (y, m, d) = (dt.year as i64, dt.month as i64, dt.day as i64);
let (cyear, cmonth, cday, _leap) = match cal.as_str() {
"chinese" => calendar::gregorian_to_chinese(y, m, d)?,
"dangi" => calendar::gregorian_to_dangi(y, m, d)?,
_ => return None,
};
let related = if cal == "dangi" {
calendar::dangi_to_gregorian(cyear, 1, 1, false)
} else {
calendar::chinese_to_gregorian(cyear, 1, 1, false)
}
.map_or(cyear, |g| g.0);
let cyclic1 = (related - 4).rem_euclid(60) + 1;
let zh = locale.starts_with("zh");
let two = |v: i64| alloc::format!("{v:02}");
let mut parts: Vec<(&'static str, String)> = Vec::new();
if o.year.is_some() {
parts.push(("relatedYear", alloc::format!("{related}")));
parts.push(("yearName", sexagenary_year_name(cyclic1)));
if zh {
parts.push(("literal", String::from("年")));
}
}
if let Some(mstyle) = o.month {
if !zh && !parts.is_empty() {
parts.push(("literal", String::from(", ")));
}
parts.push((
"month",
match mstyle {
MonthStyle::TwoDigit => two(cmonth),
_ => alloc::format!("{cmonth}"),
},
));
if zh {
parts.push(("literal", String::from("月")));
}
}
if let Some(dstyle) = o.day {
if !zh && !parts.is_empty() {
parts.push(("literal", String::from(" ")));
}
parts.push((
"day",
match dstyle {
Numeric2Digit::TwoDigit => two(cday),
_ => alloc::format!("{cday}"),
},
));
if zh {
parts.push(("literal", String::from("日")));
}
}
let has_time = o.hour.is_some()
|| o.minute.is_some()
|| o.second.is_some()
|| o.day_period.is_some()
|| o.fractional_second_digits.is_some();
if has_time {
let mut to = DateTimeFormatOptions::default();
to.hour = o.hour;
to.minute = o.minute;
to.second = o.second;
to.day_period = o.day_period;
to.fractional_second_digits = o.fractional_second_digits;
to.hour12 = o.hour12;
to.hour_cycle = o.hour_cycle;
to.time_zone_name = o.time_zone_name;
to.tz_offset_minutes = o.tz_offset_minutes;
if let Ok(tp) = datetime::format_to_parts(locale, dt, &to) {
if !parts.is_empty() {
parts.push(("literal", String::from(", ")));
}
parts.extend(dtf_pad_time_parts(tp));
}
}
Some(parts)
}
#[cfg(feature = "intl")]
pub(crate) fn temporal_dtf_value(
&mut self,
handle: Handle,
value: NanBox,
zoned_ok: bool,
) -> Result<Option<(f64, crate::temporal_iso::TemporalKind)>, ExecError> {
use crate::temporal_iso::{TemporalKind, iso_to_epoch_days};
let Some(h) = value.as_handle().map(Handle::from_raw) else {
return Ok(None);
};
let Some(d) = self.realm.temporal_at(h) else {
return Ok(None);
};
let dtf_cal = self
.realm
.get_property(handle, "calendar")
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
.or_else(|| {
self.realm
.get_property(handle, "\u{0}locale")
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
.and_then(|loc| locale_unicode_calendar(&loc))
})
.unwrap_or_else(|| String::from("gregory"));
let cal = d.calendar.clone();
let cal_ok_iso = cal == dtf_cal || cal == "iso8601";
let cal_ok_exact = cal == dtf_cal;
let noon = 43_200_000_i64;
let tod = |t: &crate::temporal_iso::IsoTime| -> i64 {
i64::from(t.hour) * 3_600_000
+ i64::from(t.minute) * 60_000
+ i64::from(t.second) * 1_000
+ i64::from(t.millisecond)
};
let ms = match d.kind {
TemporalKind::ZonedDateTime if !zoned_ok => {
return Err(self.type_error(
"Temporal.ZonedDateTime is not supported by Intl.DateTimeFormat.prototype.format; \
use toLocaleString() or explicit options",
));
}
TemporalKind::ZonedDateTime => {
if !cal_ok_iso {
let m = self.new_str(
"Temporal object calendar is incompatible with this Intl.DateTimeFormat",
);
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
(d.epoch_ns / 1_000_000) as f64
}
TemporalKind::Duration => return Ok(None),
TemporalKind::Instant => (d.epoch_ns / 1_000_000) as f64,
TemporalKind::PlainDate | TemporalKind::PlainDateTime => {
if !cal_ok_iso {
let m = self.new_str(
"Temporal object calendar is incompatible with this Intl.DateTimeFormat",
);
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
let days = iso_to_epoch_days(d.date);
if d.kind == TemporalKind::PlainDate {
(days * 86_400_000 + noon) as f64
} else {
(days * 86_400_000 + tod(&d.time)) as f64
}
}
TemporalKind::PlainYearMonth | TemporalKind::PlainMonthDay => {
if !cal_ok_exact {
let m = self.new_str(
"Temporal object calendar is incompatible with this Intl.DateTimeFormat",
);
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
(iso_to_epoch_days(d.date) * 86_400_000 + noon) as f64
}
TemporalKind::PlainTime => tod(&d.time) as f64,
};
Ok(Some((ms, d.kind)))
}
#[cfg(feature = "intl")]
pub(crate) fn temporal_plain_options(
&mut self,
handle: Handle,
kind: crate::temporal_iso::TemporalKind,
) -> Option<intl::datetime::DateTimeFormatOptions> {
use crate::temporal_iso::TemporalKind;
use intl::datetime::{
DateStyle, DateTimeFormatOptions, HourCycle, MonthStyle, NameStyle, Numeric2Digit,
TimeZoneNameStyle,
};
let opt = |this: &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 defaulted = self
.realm
.get_property(handle, "\u{0}dtf_default_date")
.and_then(|v| v.as_boolean())
.unwrap_or(false);
let weekday = opt(self, "weekday");
let era = opt(self, "era");
let (year, month, day) = if defaulted {
(None, None, None)
} else {
(opt(self, "year"), opt(self, "month"), opt(self, "day"))
};
let day_period = opt(self, "dayPeriod");
let hour = opt(self, "hour");
let minute = opt(self, "minute");
let second = opt(self, "second");
let frac = self
.realm
.get_property(handle, "fractionalSecondDigits")
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_number(v) as u8);
let tz_name = opt(self, "timeZoneName");
let hour_cycle_s = opt(self, "hourCycle");
let hour12 = self
.realm
.get_property(handle, "hour12")
.and_then(|v| v.as_boolean());
let date_style = opt(self, "dateStyle");
let time_style = opt(self, "timeStyle");
let ds = date_style.is_some();
let ts = time_style.is_some();
let p_weekday = weekday.is_some() || (ds && date_style.as_deref() == Some("full"));
let p_year = year.is_some() || ds;
let p_month = month.is_some() || ds;
let p_day = day.is_some() || ds;
let p_hour = hour.is_some() || ts;
let p_minute = minute.is_some() || ts;
let p_second = second.is_some() || (ts && time_style.as_deref() != Some("short"));
let p_day_period = day_period.is_some();
let p_frac = frac.is_some();
let any_present = p_weekday
|| p_year
|| p_month
|| p_day
|| p_hour
|| p_minute
|| p_second
|| p_day_period
|| p_frac;
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 mstyle = |s: &str| match s {
"numeric" => Some(MonthStyle::Numeric),
"2-digit" => Some(MonthStyle::TwoDigit),
"long" => Some(MonthStyle::Long),
"short" => Some(MonthStyle::Short),
"narrow" => Some(MonthStyle::Narrow),
_ => None,
};
let hc = |s: &str| match s {
"h11" => Some(HourCycle::H11),
"h12" => Some(HourCycle::H12),
"h23" => Some(HourCycle::H23),
"h24" => Some(HourCycle::H24),
_ => 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 tstyle_no_tz = |s: &str| match s {
"short" => Some(DateStyle::Short),
_ => Some(DateStyle::Medium),
};
let ds_year = |s: &str| match s {
"short" => Numeric2Digit::TwoDigit,
_ => Numeric2Digit::Numeric,
};
let ds_month = |s: &str| match s {
"full" | "long" => MonthStyle::Long,
"medium" => MonthStyle::Short,
_ => MonthStyle::Numeric,
};
let mut o = DateTimeFormatOptions::default();
match kind {
TemporalKind::PlainDate => {
let need_defaults = !(p_weekday || p_year || p_month || p_day);
if need_defaults {
if any_present {
return None;
}
o.era = era.as_deref().and_then(name);
o.year = Some(Numeric2Digit::Numeric);
o.month = Some(MonthStyle::Numeric);
o.day = Some(Numeric2Digit::Numeric);
} else if ds {
o.date_style = date_style.as_deref().and_then(dstyle);
} else {
o.weekday = weekday.as_deref().and_then(name);
o.era = era.as_deref().and_then(name);
o.year = year.as_deref().and_then(n2);
o.month = month.as_deref().and_then(mstyle);
o.day = day.as_deref().and_then(n2);
}
}
TemporalKind::PlainYearMonth => {
let need_defaults = !(p_year || p_month);
if need_defaults {
if any_present {
return None;
}
o.era = era.as_deref().and_then(name);
o.year = Some(Numeric2Digit::Numeric);
o.month = Some(MonthStyle::Numeric);
} else if ds {
let s = date_style.as_deref().unwrap_or("short");
o.year = Some(ds_year(s));
o.month = Some(ds_month(s));
} else {
o.era = era.as_deref().and_then(name);
o.year = year.as_deref().and_then(n2);
o.month = month.as_deref().and_then(mstyle);
}
}
TemporalKind::PlainMonthDay => {
let need_defaults = !(p_month || p_day);
if need_defaults {
if any_present {
return None;
}
o.month = Some(MonthStyle::Numeric);
o.day = Some(Numeric2Digit::Numeric);
} else if ds {
let s = date_style.as_deref().unwrap_or("short");
o.month = Some(ds_month(s));
o.day = Some(Numeric2Digit::Numeric);
} else {
o.month = month.as_deref().and_then(mstyle);
o.day = day.as_deref().and_then(n2);
}
}
TemporalKind::PlainTime => {
let need_defaults = !(p_day_period || p_hour || p_minute || p_second || p_frac);
if need_defaults {
if any_present {
return None;
}
o.hour_cycle = hour_cycle_s.as_deref().and_then(hc);
o.hour12 = hour12;
o.hour = Some(Numeric2Digit::Numeric);
o.minute = Some(Numeric2Digit::Numeric);
o.second = Some(Numeric2Digit::Numeric);
} else {
o.hour_cycle = hour_cycle_s.as_deref().and_then(hc);
o.hour12 = hour12;
if ts {
o.time_style = time_style.as_deref().and_then(tstyle_no_tz);
} else {
o.hour = hour.as_deref().and_then(n2);
o.minute = minute.as_deref().and_then(n2);
o.second = second.as_deref().and_then(n2);
o.day_period = day_period.as_deref().and_then(name);
o.fractional_second_digits = frac;
}
}
}
TemporalKind::PlainDateTime => {
o.hour_cycle = hour_cycle_s.as_deref().and_then(hc);
o.hour12 = hour12;
if !any_present {
o.era = era.as_deref().and_then(name);
o.year = Some(Numeric2Digit::Numeric);
o.month = Some(MonthStyle::Numeric);
o.day = Some(Numeric2Digit::Numeric);
o.hour = Some(Numeric2Digit::Numeric);
o.minute = Some(Numeric2Digit::Numeric);
o.second = Some(Numeric2Digit::Numeric);
} else {
if ds {
o.date_style = date_style.as_deref().and_then(dstyle);
} else {
o.weekday = weekday.as_deref().and_then(name);
o.era = era.as_deref().and_then(name);
o.year = year.as_deref().and_then(n2);
o.month = month.as_deref().and_then(mstyle);
o.day = day.as_deref().and_then(n2);
}
if ts {
o.time_style = time_style.as_deref().and_then(tstyle_no_tz);
} else {
o.hour = hour.as_deref().and_then(n2);
o.minute = minute.as_deref().and_then(n2);
o.second = second.as_deref().and_then(n2);
o.day_period = day_period.as_deref().and_then(name);
o.fractional_second_digits = frac;
}
}
}
TemporalKind::Instant | TemporalKind::ZonedDateTime => {
o.hour_cycle = hour_cycle_s.as_deref().and_then(hc);
o.hour12 = hour12;
if !any_present {
o.era = era.as_deref().and_then(name);
o.year = Some(Numeric2Digit::Numeric);
o.month = Some(MonthStyle::Numeric);
o.day = Some(Numeric2Digit::Numeric);
o.hour = Some(Numeric2Digit::Numeric);
o.minute = Some(Numeric2Digit::Numeric);
o.second = Some(Numeric2Digit::Numeric);
if kind == TemporalKind::ZonedDateTime && tz_name.is_none() {
o.time_zone_name = Some(TimeZoneNameStyle::Short);
o.tz_offset_minutes = Some(0); }
} else {
if ds {
o.date_style = date_style.as_deref().and_then(dstyle);
} else {
o.weekday = weekday.as_deref().and_then(name);
o.era = era.as_deref().and_then(name);
o.year = year.as_deref().and_then(n2);
o.month = month.as_deref().and_then(mstyle);
o.day = day.as_deref().and_then(n2);
}
if ts {
o.time_style = time_style.as_deref().and_then(dstyle);
} else {
o.hour = hour.as_deref().and_then(n2);
o.minute = minute.as_deref().and_then(n2);
o.second = second.as_deref().and_then(n2);
o.day_period = day_period.as_deref().and_then(name);
o.fractional_second_digits = frac;
}
}
if let Some(tzn) = tz_name.as_deref() {
o.time_zone_name = match tzn {
"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); }
return Some(o);
}
_ => return None,
}
let _ = tz_name;
Some(o)
}
#[cfg(feature = "intl")]
pub(crate) fn temporal_datetime_parts(
&self,
handle: Handle,
ms: f64,
o: &intl::datetime::DateTimeFormatOptions,
) -> Vec<(&'static str, String)> {
use intl::datetime::{self, DateTime};
let locale = self
.realm
.get_property(handle, "\u{0}locale")
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
.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,
};
if let Some(alt) = self.alt_calendar_date_parts(handle, &locale, &dt, o) {
return alt;
}
match datetime::format_to_parts(&locale, &dt, o) {
Ok(parts) => dtf_pad_time_parts(parts),
Err(_) => Vec::new(),
}
}
#[cfg(feature = "intl")]
fn alt_calendar_date_parts(
&self,
handle: Handle,
locale: &str,
dt: &intl::datetime::DateTime,
o: &intl::datetime::DateTimeFormatOptions,
) -> Option<Vec<(&'static str, String)>> {
use crate::temporal_iso::IsoDate;
use intl::datetime;
let date_style = o.date_style?;
let cal = self.dtf_resolved_calendar(handle);
let is_islamic = cal.starts_with("islamic");
let is_persian = cal == "persian";
if !is_islamic && !is_persian {
return None;
}
let iso = IsoDate {
year: dt.year,
month: dt.month,
day: dt.day,
};
let f = crate::nbexec::temporal_calendar::iso_to_fields(&cal, iso);
let (yy, mm, dd) = (f.year, f.month, f.day);
let date = if is_islamic {
datetime::format_islamic_date(locale, yy, mm, dd, date_style)
} else {
datetime::format_persian_date(locale, yy, mm, dd, date_style)
};
let mut parts: Vec<(&'static str, String)> = alloc::vec![("month", date)];
if let Some(ts) = o.time_style {
parts.push(("literal", String::from(", ")));
parts.push(("hour", datetime::format_time(locale, dt, ts)));
}
Some(parts)
}
#[cfg(feature = "intl")]
fn dtf_resolved_calendar(&self, handle: Handle) -> String {
self.realm
.get_property(handle, "calendar")
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
.or_else(|| {
self.realm
.get_property(handle, "\u{0}locale")
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
.map(|v| self.realm.to_display_string(v))
.and_then(|loc| locale_unicode_calendar(&loc))
})
.unwrap_or_else(|| String::from("gregory"))
}
#[cfg(feature = "intl")]
pub(crate) fn temporal_format_flat(
&mut self,
handle: Handle,
value: NanBox,
zoned_ok: bool,
) -> Result<Option<String>, ExecError> {
let Some((ms, kind)) = self.temporal_dtf_value(handle, value, zoned_ok)? else {
return Ok(None);
};
let Some(mut o) = self.temporal_plain_options(handle, kind) else {
return Err(self.type_error(
"the requested Intl.DateTimeFormat options are not compatible with this Temporal type",
));
};
let ms = self.dtf_apply_temporal_zone(handle, ms, kind, &mut o);
let mut s = String::new();
for (_, v) in self.temporal_datetime_parts(handle, ms, &o) {
s.push_str(&v);
}
Ok(Some(self.apply_numbering_digits(handle, s)))
}
#[cfg(feature = "intl")]
pub(crate) fn temporal_format_parts(
&mut self,
handle: Handle,
value: NanBox,
zoned_ok: bool,
) -> Result<Option<Vec<(&'static str, String)>>, ExecError> {
let Some((ms, kind)) = self.temporal_dtf_value(handle, value, zoned_ok)? else {
return Ok(None);
};
let Some(mut o) = self.temporal_plain_options(handle, kind) else {
return Err(self.type_error(
"the requested Intl.DateTimeFormat options are not compatible with this Temporal type",
));
};
let ms = self.dtf_apply_temporal_zone(handle, ms, kind, &mut o);
Ok(Some(self.temporal_datetime_parts(handle, ms, &o)))
}
#[cfg(feature = "intl")]
pub(crate) fn temporal_to_locale_string(
&mut self,
this: NanBox,
args: &[NanBox],
) -> Result<NanBox, ExecError> {
let locales = args.first().copied().unwrap_or(NanBox::undefined());
let options = args.get(1).copied().unwrap_or(NanBox::undefined());
let fmt_args = [locales, options];
let inst = self.make_intl_formatter(N_INTL_DATETIME_FORMAT, &fmt_args)?;
let Some(h) = inst.as_handle().map(Handle::from_raw) else {
return Ok(self.new_str(""));
};
let s = self
.temporal_format_flat(h, this, true)?
.unwrap_or_default();
Ok(self.new_str(&s))
}
#[cfg(feature = "intl")]
fn range_type_tag(&self, value: NanBox) -> u8 {
use crate::temporal_iso::TemporalKind;
if let Some(h) = value.as_handle().map(Handle::from_raw)
&& let Some(d) = self.realm.temporal_at(h)
{
return match d.kind {
TemporalKind::PlainDate => 1,
TemporalKind::PlainTime => 2,
TemporalKind::PlainDateTime => 3,
TemporalKind::Duration => 4,
TemporalKind::Instant => 5,
TemporalKind::PlainYearMonth => 6,
TemporalKind::PlainMonthDay => 7,
TemporalKind::ZonedDateTime => 8,
};
}
0
}
#[cfg(feature = "intl")]
fn is_temporal_value(&self, value: NanBox) -> bool {
value
.as_handle()
.map(Handle::from_raw)
.and_then(|h| self.realm.temporal_at(h))
.is_some()
}
#[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
}
#[cfg(feature = "intl")]
pub(crate) fn date_time_options(
&mut self,
user: NanBox,
want_date: bool,
want_time: bool,
) -> Result<Handle, ExecError> {
let uh = user.as_handle().map(Handle::from_raw);
let present = |this: &mut Self, keys: &[&str]| -> bool {
uh.is_some_and(|h| {
keys.iter().any(|k| {
this.realm
.get_property(h, k)
.is_some_and(|v| !matches!(v.unpack(), Unpacked::Undefined))
})
})
};
let has_date = present(self, &["weekday", "year", "month", "day"]);
let has_time = present(
self,
&[
"dayPeriod",
"hour",
"minute",
"second",
"fractionalSecondDigits",
],
);
let has_style = present(self, &["dateStyle", "timeStyle"]);
let obj = self.realm.new_object();
if let Some(h) = uh {
for key in [
"localeMatcher",
"weekday",
"era",
"year",
"month",
"day",
"dayPeriod",
"hour",
"minute",
"second",
"fractionalSecondDigits",
"timeZoneName",
"hour12",
"hourCycle",
"timeZone",
"calendar",
"numberingSystem",
"dateStyle",
"timeStyle",
"formatMatcher",
] {
if let Some(v) = self
.realm
.get_property(h, key)
.filter(|v| !matches!(v.unpack(), Unpacked::Undefined))
{
self.realm.set_property(obj, key, v);
}
}
}
let need_defaults = !((want_date && has_date) || (want_time && has_time));
if !has_style && need_defaults {
let num = self.new_str("numeric");
if want_date {
for k in ["year", "month", "day"] {
self.realm.set_property(obj, k, num);
}
}
if want_time {
for k in ["hour", "minute", "second"] {
self.realm.set_property(obj, k, num);
}
}
}
Ok(obj)
}
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);
}
self.apply_numbering_digits(handle, s)
}
pub(crate) fn apply_numbering_digits(&mut self, handle: Handle, s: String) -> String {
let nu = self
.realm
.get_property(handle, "numberingSystem")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_default();
substitute_numbering_digits(&nu, 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,
};
match self
.realm
.get_property(handle, "useGrouping")
.map(|v| v.unpack())
{
Some(Unpacked::Bool(false)) => o.use_grouping = UseGrouping::Never,
Some(Unpacked::Bool(true)) => o.use_grouping = UseGrouping::Always,
Some(_) => match opt_str(self, "useGrouping").as_deref() {
Some("min2") => o.use_grouping = UseGrouping::Min2,
Some("always") | Some("true") => o.use_grouping = UseGrouping::Always,
Some("false") => o.use_grouping = UseGrouping::Never,
_ => {}
},
None => {}
}
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")
}
pub(crate) fn intl_format_number(&mut self, handle: Handle, n: f64) -> String {
#[cfg(feature = "intl")]
if let Some(s) = self.try_rounding_priority(handle, n) {
return s;
}
let s = self.intl_format_number_inner(handle, n);
self.apply_numbering_digits(handle, s)
}
#[cfg(feature = "intl")]
fn extract_exact_decimal(&self, value: NanBox) -> Option<(bool, String, String)> {
let h = value.as_handle().map(Handle::from_raw)?;
let raw = if let Some(big) = self.realm.bigint_at(h) {
alloc::format!("{big}")
} else {
self.realm.string_value(h)?
};
let s = raw.trim();
let (neg, body) = match s.strip_prefix('-') {
Some(r) => (true, r),
None => (false, s.strip_prefix('+').unwrap_or(s)),
};
if body.is_empty() || body.matches('.').count() > 1 {
return None;
}
if !body.bytes().all(|b| b.is_ascii_digit() || b == b'.') {
return None;
}
let mut it = body.splitn(2, '.');
let int_part = it.next().unwrap_or("");
let frac_part = it.next().unwrap_or("");
let all = alloc::format!("{int_part}{frac_part}");
let first_nz = all.bytes().position(|b| b != b'0');
let last_nz = all.bytes().rposition(|b| b != b'0');
let sig = match (first_nz, last_nz) {
(Some(a), Some(b)) => b - a + 1,
_ => 0,
};
let int_magnitude = int_part.trim_start_matches('0').len();
if sig <= 15 && int_magnitude <= 15 {
return None;
}
Some((neg, String::from(int_part), String::from(frac_part)))
}
#[cfg(feature = "intl")]
fn try_exact_decimal_format(&mut self, handle: Handle, value: NanBox) -> Option<String> {
use intl::number::{Notation, NumberStyle, UseGrouping};
let opts = self.number_format_options(handle);
if opts.notation != Notation::Standard || !matches!(opts.style, NumberStyle::Decimal) {
return None;
}
let increment = self
.realm
.get_property(handle, "roundingIncrement")
.and_then(|v| v.as_number())
.unwrap_or(1.0);
if increment != 1.0 {
return None;
}
let (neg, int_part, frac_part) = self.extract_exact_decimal(value)?;
let min_int = (opts.minimum_integer_digits.max(1)) as usize;
let (mut int_digits, frac_digits) = if opts.minimum_significant_digits.is_some()
|| opts.maximum_significant_digits.is_some()
{
exact_significant_digits(
neg,
&int_part,
&frac_part,
opts.minimum_significant_digits.map(|m| m as usize),
opts.maximum_significant_digits.map(|m| m as usize),
opts.rounding_mode,
)?
} else {
let max_frac = opts.maximum_fraction_digits.unwrap_or(3) as usize;
let min_frac = opts.minimum_fraction_digits.unwrap_or(0) as usize;
let mut int_digits: alloc::vec::Vec<u8> = int_part.bytes().map(|b| b - b'0').collect();
let mut frac_digits: alloc::vec::Vec<u8> =
frac_part.bytes().map(|b| b - b'0').collect();
if frac_digits.len() > max_frac {
let up = exact_round_up(&frac_digits[max_frac..], opts.rounding_mode, neg);
frac_digits.truncate(max_frac);
if up {
exact_increment(&mut int_digits, &mut frac_digits);
}
}
while frac_digits.len() > min_frac && frac_digits.last() == Some(&0) {
frac_digits.pop();
}
while frac_digits.len() < min_frac {
frac_digits.push(0);
}
(int_digits, frac_digits)
};
while int_digits.len() > 1 && int_digits.first() == Some(&0) {
int_digits.remove(0);
}
while int_digits.len() < min_int {
int_digits.insert(0, 0);
}
let grouping = !matches!(opts.use_grouping, UseGrouping::Never);
Some(self.assemble_decimal(handle, neg, &int_digits, &frac_digits, grouping))
}
#[cfg(feature = "intl")]
fn assemble_decimal(
&mut self,
handle: Handle,
neg: bool,
int: &[u8],
frac: &[u8],
grouping: bool,
) -> String {
let int_str: String = int.iter().map(|d| (b'0' + d) as char).collect();
let frac_str: String = frac.iter().map(|d| (b'0' + d) as char).collect();
let probe = self.intl_format_number_inner(handle, if neg { -1.1 } else { 1.1 });
let (prefix, dec_sep, suffix) = split_number_scaffold(&probe);
let grouped = if grouping {
let gp = self.intl_format_number_inner(handle, if neg { -1111.0 } else { 1111.0 });
let group_sep = extract_group_sep(&gp);
group_thousands_sep(&int_str, &group_sep)
} else {
int_str
};
let mut out = String::new();
out.push_str(&prefix);
out.push_str(&grouped);
if !frac_str.is_empty() {
out.push_str(&dec_sep);
out.push_str(&frac_str);
}
out.push_str(&suffix);
self.apply_numbering_digits(handle, out)
}
#[cfg(feature = "intl")]
fn try_rounding_priority(&mut self, handle: Handle, n: f64) -> Option<String> {
use intl::number::{Notation, NumberStyle, UseGrouping};
if !n.is_finite() || n == 0.0 {
return None;
}
let priority = self
.realm
.get_property(handle, "roundingPriority")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_default();
if !matches!(priority.as_str(), "morePrecision" | "lessPrecision") {
return None;
}
let opts = self.number_format_options(handle);
if opts.notation != Notation::Standard || !matches!(opts.style, NumberStyle::Decimal) {
return None;
}
let min_sig = opts.minimum_significant_digits.unwrap_or(1) as usize;
let max_sig = opts.maximum_significant_digits.unwrap_or(21) as usize;
let min_frac = opts.minimum_fraction_digits.unwrap_or(0) as usize;
let max_frac = opts
.maximum_fraction_digits
.map(|m| m as usize)
.unwrap_or_else(|| min_frac.max(3));
let neg = n.is_sign_negative();
let shortest = alloc::format!("{}", n.abs());
if shortest.contains(['e', 'E', 'i', 'n']) {
return None;
}
let mut it = shortest.splitn(2, '.');
let int_part = it.next().unwrap_or("0");
let frac_part = it.next().unwrap_or("");
let int_v: alloc::vec::Vec<u8> = int_part.bytes().map(|b| b - b'0').collect();
let frac_v: alloc::vec::Vec<u8> = frac_part.bytes().map(|b| b - b'0').collect();
let (s_int, s_frac) = to_raw_precision(
neg,
int_v.clone(),
frac_v.clone(),
max_sig,
opts.rounding_mode,
);
let (f_int, f_frac) = to_raw_fixed(neg, int_v, frac_v, max_frac, opts.rounding_mode);
let e = {
let int_stripped = int_part.trim_start_matches('0');
if !int_stripped.is_empty() {
int_stripped.len() as i32 - 1
} else {
match frac_part.bytes().position(|b| b != b'0') {
Some(p) => -(p as i32) - 1,
None => 0,
}
}
};
let s_mag = e - max_sig as i32 + 1;
let f_mag = -(max_frac as i32);
let use_s = if priority == "morePrecision" {
s_mag <= f_mag
} else {
s_mag > f_mag
};
let (int_d, frac_d) = if use_s {
let (mut i, mut f) = (s_int, s_frac);
let sig_now = {
let first = i
.iter()
.chain(f.iter())
.position(|&d| d != 0)
.unwrap_or(i.len());
(i.len() + f.len()).saturating_sub(first)
};
if sig_now < min_sig {
f.resize(f.len() + (min_sig - sig_now), 0);
}
while i.len() > 1 && i[0] == 0 {
i.remove(0);
}
(i, f)
} else {
let (i, mut f) = (f_int, f_frac);
while f.len() > min_frac && f.last() == Some(&0) {
f.pop();
}
if f.len() < min_frac {
f.resize(min_frac, 0);
}
(i, f)
};
let min_int = opts.minimum_integer_digits.max(1) as usize;
let mut int_d = int_d;
while int_d.len() < min_int {
int_d.insert(0, 0);
}
let grouping = !matches!(opts.use_grouping, UseGrouping::Never);
Some(self.assemble_decimal(handle, neg, &int_d, &frac_d, grouping))
}
#[cfg(feature = "intl")]
fn number_precision_round(
&mut self,
handle: Handle,
opts: &mut intl::number::NumberFormatOptions,
n: f64,
) -> f64 {
use intl::number::{Notation, NumberStyle};
if !n.is_finite() || n == 0.0 || opts.notation != Notation::Standard {
return n;
}
if matches!(opts.style, NumberStyle::Percent) {
return n;
}
let priority = self
.realm
.get_property(handle, "roundingPriority")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_default();
if matches!(priority.as_str(), "morePrecision" | "lessPrecision")
&& opts.maximum_significant_digits.is_some()
&& opts.maximum_fraction_digits.is_some()
{
return n;
}
let increment = self
.realm
.get_property(handle, "roundingIncrement")
.and_then(|v| v.as_number())
.unwrap_or(1.0) as u32;
let sig = opts.maximum_significant_digits.map(|s| s as usize);
let default_max = match opts.style {
NumberStyle::Currency if opts.currency == Some("JPY") => 0,
NumberStyle::Currency => 2,
_ => 3,
};
let keep_frac = opts
.maximum_fraction_digits
.map(|m| m as usize)
.unwrap_or(default_max);
let rounded = intl_decimal_round(n, keep_frac, sig, increment, opts.rounding_mode);
opts.rounding_mode = intl::number::RoundingMode::HalfExpand;
rounded
}
fn intl_format_number_inner(&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 mut opts = self.number_format_options(handle);
let n = self.number_precision_round(handle, &mut opts, n);
if n.is_finite() && n.fract() == 0.0 {
let tzd = self
.realm
.get_property(handle, "trailingZeroDisplay")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_default();
if tzd == "stripIfInteger" {
opts.minimum_fraction_digits = Some(0);
if opts.minimum_significant_digits.is_some() {
opts.minimum_significant_digits = Some(1);
}
}
}
let n = if n == 0.0 && n.is_sign_negative() {
-f64::from_bits(1)
} else {
n
};
if n.is_finite() && compact_wants_reround(&opts) {
let mut o = opts;
o.maximum_fraction_digits = Some(6);
let mut parts: Vec<(&'static str, String)> =
intl::number::format_to_parts(&locale, n, &o)
.into_iter()
.map(|p| (p.kind.as_str(), p.value))
.collect();
compact_reround_parts(&mut parts, opts.rounding_mode);
return parts.into_iter().map(|(_, v)| v).collect();
}
let formatted = intl::number::format(&locale, n, &opts);
if formatted.starts_with('-') && n.is_finite() {
let digits = formatted.bytes().filter(u8::is_ascii_digit);
let mut any = false;
let all_zero = digits.inspect(|_| any = true).all(|b| b == b'0');
if any && all_zero {
let sd = self
.realm
.get_property(handle, "signDisplay")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_default();
if matches!(sd.as_str(), "negative" | "never" | "exceptZero") {
return String::from(&formatted[1..]);
}
}
}
if n.is_nan()
&& !formatted.starts_with(['+', '-'])
&& self
.realm
.get_property(handle, "signDisplay")
.map(|v| self.realm.to_display_string(v))
.as_deref()
== Some("always")
{
return alloc::format!("+{formatted}");
}
if accounting_uses_parens(&locale)
&& formatted.starts_with('-')
&& self
.realm
.get_property(handle, "currencySign")
.map(|v| self.realm.to_display_string(v))
.as_deref()
== Some("accounting")
{
return alloc::format!("({})", &formatted[1..]);
}
return formatted;
}
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),
};
if neg
&& style == "currency"
&& opt_str(self, "currencySign").as_deref() == Some("accounting")
{
return alloc::format!("({styled})");
}
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 {
"+"
}
}
Some("negative") if neg && !is_zero => "-",
Some("negative") => "",
_ 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.is_string_handle(h));
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 let Some(h) = locales.as_handle().map(Handle::from_raw)
&& self.realm.get_property(h, "\u{0}brand_loc").is_some()
&& let Some(loc) = self.realm.get_property(h, "\u{0}locale_tag")
{
seen.push(self.realm.to_display_string(loc));
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_proxied(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.is_string_handle(h));
let el_is_object = self.is_object_value(el) && !el_is_string;
let ty = self.realm.type_of_value(el);
let el_is_primitive_nonstring =
!el_is_string && matches!(ty, "symbol" | "number" | "boolean" | "bigint");
if el_is_primitive_nonstring || (!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" => &AVAILABLE_CALENDARS,
"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" => NUMBERING_SYSTEMS,
"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();
if k == "numberingSystem" {
sorted.retain(|s| !matches!(*s, "native" | "traditio" | "finance"));
}
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",
"getCalendars",
"getCollations",
"getHourCycles",
"getNumberingSystems",
"getTimeZones",
"getTextInfo",
"getWeekInfo",
] {
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 obj = self.realm.new_object();
self.init_locale(obj, args)?;
Ok(NanBox::handle(obj.to_raw()))
}
pub(crate) fn init_locale(&mut self, obj: Handle, args: &[NanBox]) -> Result<(), 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) {
if let Some(t) = self.realm.get_property(h, "\u{0}locale_tag") {
self.realm.to_display_string(t)
} else if matches!(self.realm.type_of(h), Some("symbol") | Some("bigint")) {
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)?
}
} else {
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))));
};
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_arg = args.get(1).copied().unwrap_or(NanBox::undefined());
let opts = match opts_arg.unpack() {
Unpacked::Undefined => None,
Unpacked::Null => {
let m = self.new_str("Intl.Locale options must not be null");
return Err(ExecError::Throw(self.make_error(N_TYPE_ERROR, Some(m))));
}
_ => self
.coerce_to_object(opts_arg)
.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 variants = self.get_string_option(opts, "variants", &[], 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 first_day = self.get_string_option(opts, "firstDayOfWeek", &[], None)?;
let first_day = if let Some(fd) = &first_day {
let Some(w) = weekday_to_string(fd) else {
let m = self.new_str("invalid firstDayOfWeek");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
};
Some(w)
} else {
None
};
if let Some(l) = &language {
let n = l.len();
let ok = ((2..=3).contains(&n) || (5..=8).contains(&n))
&& l.bytes().all(|b| b.is_ascii_alphabetic());
if !ok {
let m = self.new_str("invalid language");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
}
if let Some(s) = &script
&& !(s.len() == 4 && s.bytes().all(|b| b.is_ascii_alphabetic()))
{
let m = self.new_str("invalid script");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
if let Some(r) = ®ion {
let alpha2 = r.len() == 2 && r.bytes().all(|b| b.is_ascii_alphabetic());
let digit3 = r.len() == 3 && r.bytes().all(|b| b.is_ascii_digit());
if !alpha2 && !digit3 {
let m = self.new_str("invalid region");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
}
for (val, name) in [
(&calendar, "calendar"),
(&collation, "collation"),
(&numbering, "numberingSystem"),
] {
if let Some(v) = val
&& !is_unicode_type_value(v)
{
let m = self.new_str(&alloc::format!("invalid {name}"));
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
}
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(v) = &variants {
let mut vs: Vec<String> = Vec::new();
for sub in v.split('-') {
let s = sub.to_ascii_lowercase();
let alnum = s.bytes().all(|b| b.is_ascii_alphanumeric());
let ok = ((5..=8).contains(&s.len()) && alnum)
|| (s.len() == 4 && s.as_bytes()[0].is_ascii_digit() && alnum);
if !ok || vs.contains(&s) {
let m = self.new_str("invalid variants");
return Err(ExecError::Throw(self.make_error(N_RANGE_ERROR, Some(m))));
}
vs.push(s);
}
vs.sort();
parsed.variants = vs;
}
if let Some(c) = &calendar {
parsed.set_keyword("ca", unicode_type_alias("ca", c).unwrap_or(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);
}
if let Some(fw) = &first_day {
parsed.set_keyword("fw", fw);
}
if grandfathered_canonical(&parsed.base_name()).is_some()
&& let Some(canon) = canonicalize_locale_id(&parsed.to_tag())
{
parsed = ParsedLocale::from_canonical(&canon);
}
let final_tag = parsed.to_tag();
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());
if !parsed.variants.is_empty() {
store(self, "\u{0}loc_variants", Some(&parsed.variants.join("-")));
}
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"));
store(self, "\u{0}loc_fw", parsed.keyword("fw"));
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(())
}
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"),
"variants" => read(self, "\u{0}loc_variants"),
"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"),
"firstDayOfWeek" => read(self, "\u{0}loc_fw"),
_ => 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();
let array_of = |this: &mut Self, items: &[&str]| {
let vals: Vec<NanBox> = items.iter().map(|s| this.new_str(s)).collect();
NanBox::handle(this.realm.new_array(vals).to_raw())
};
match name {
"toString" => Ok(self.new_str(&tag)),
"getCalendars" => Ok(array_of(self, &["gregory"])),
"getCollations" => Ok(array_of(self, &["default"])),
"getHourCycles" => Ok(array_of(self, &["h23"])),
"getNumberingSystems" => Ok(array_of(self, &["latn"])),
"getTimeZones" => {
if ParsedLocale::from_canonical(&tag)
.region
.as_deref()
.unwrap_or("")
.is_empty()
{
Ok(NanBox::undefined())
} else {
Ok(array_of(self, &["UTC"]))
}
}
"getTextInfo" => {
let obj = self.realm.new_object();
let pl = ParsedLocale::from_canonical(&tag);
let rtl = matches!(
pl.script.as_deref().unwrap_or(""),
"Arab" | "Hebr" | "Syrc" | "Thaa" | "Nkoo" | "Rohg" | "Adlm"
) || matches!(
pl.language.as_str(),
"ar" | "he" | "fa" | "ur" | "ps" | "sd" | "ug" | "yi" | "dv" | "ku" | "ckb"
);
let dir = if rtl { "rtl" } else { "ltr" };
let d = self.new_str(dir);
self.realm.set_property(obj, "direction", d);
Ok(NanBox::handle(obj.to_raw()))
}
"getWeekInfo" => {
let obj = self.realm.new_object();
let fw = self
.realm
.get_property(h, "\u{0}loc_fw")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_default();
let first_day = match fw.as_str() {
"tue" => 2.0,
"wed" => 3.0,
"thu" => 4.0,
"fri" => 5.0,
"sat" => 6.0,
"sun" => 7.0,
_ => 1.0,
};
self.realm
.set_property(obj, "firstDay", NanBox::number(first_day));
let weekend = self
.realm
.new_array(alloc::vec![NanBox::number(6.0), NanBox::number(7.0)]);
self.realm
.set_property(obj, "weekend", NanBox::handle(weekend.to_raw()));
Ok(NanBox::handle(obj.to_raw()))
}
"maximize" | "minimize" => {
#[allow(unused_mut)]
let mut pl = ParsedLocale::from_canonical(&tag);
#[cfg(feature = "intl")]
{
let base = pl.base_name();
if let Ok(loc) = intl::locale::Locale::parse(&base) {
let result = if name == "maximize" {
loc.maximize()
} else {
loc.maximize().minimize()
};
pl.language = if result.language.is_empty() {
String::from("und")
} else {
result.language.clone()
};
pl.script = result.script.clone();
pl.region = result.region.clone();
pl.variants = result.variants.clone();
}
}
let new_tag = pl.to_tag();
let tagv = self.new_str(&new_tag);
self.make_locale(&[tagv])
}
_ => {
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)> {
let mut parts = self.number_handle_parts_inner(handle, value);
let nu = self
.realm
.get_property(handle, "numberingSystem")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_default();
if numbering_system_digit_base(&nu).is_some_and(|b| b != 0x0030) || nu == "hanidec" {
for (_, v) in &mut parts {
if v.chars().any(|c| c.is_ascii_digit()) {
*v = substitute_numbering_digits(&nu, core::mem::take(v));
}
}
}
parts
}
fn number_handle_parts_inner(
&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 mut opts = self.number_format_options(handle);
let n = self.number_precision_round(handle, &mut opts, n);
let feed = if n == 0.0 && n.is_sign_negative() {
-f64::from_bits(1)
} else {
n
};
let reround = feed.is_finite() && compact_wants_reround(&opts);
if reround {
opts.maximum_fraction_digits = Some(6);
}
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 reround {
compact_reround_parts(&mut parts, opts.rounding_mode);
}
if matches!(opts.notation, intl::number::Notation::Compact) {
split_compact_affix_parts(&mut parts);
}
let sd = self
.realm
.get_property(handle, "signDisplay")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_default();
if n.is_nan()
&& sd == "always"
&& !parts
.iter()
.any(|(t, _)| *t == "minusSign" || *t == "plusSign")
{
parts.insert(0, ("plusSign", String::from("+")));
}
if matches!(sd.as_str(), "negative" | "never" | "exceptZero")
&& parts.iter().any(|(t, _)| *t == "minusSign")
&& !parts.iter().any(|(t, v)| {
matches!(*t, "integer" | "fraction") && v.chars().any(|c| c != '0')
})
&& !parts.iter().any(|(t, _)| matches!(*t, "nan" | "infinity"))
{
parts.retain(|(t, _)| *t != "minusSign");
}
let accounting = self
.realm
.get_property(handle, "currencySign")
.map(|v| self.realm.to_display_string(v))
.as_deref()
== Some("accounting");
if accounting
&& accounting_uses_parens(&locale)
&& parts.iter().any(|(t, _)| *t == "minusSign")
{
parts.retain(|(t, _)| *t != "minusSign");
parts.insert(0, ("literal", String::from("(")));
parts.push(("literal", 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()];
}
let notation = self
.realm
.get_property(handle, "notation")
.map(|v| self.realm.to_display_string(v))
.unwrap_or_default();
let mut suffix_parts: Vec<(&'static str, String)> = Vec::new();
if style == "unit" || notation == "compact" {
let num_end = s
.find(|c: char| !(c.is_ascii_digit() || c == ',' || c == '.'))
.unwrap_or(s.len());
let suffix = &s[num_end..];
if !suffix.is_empty() {
let part_kind = if style == "unit" { "unit" } else { "compact" };
let sep_end = suffix
.find(|c: char| !c.is_whitespace())
.unwrap_or(suffix.len());
if sep_end > 0 {
suffix_parts.push(("literal", String::from(&suffix[..sep_end])));
}
if sep_end < suffix.len() {
suffix_parts.push((part_kind, String::from(&suffix[sep_end..])));
}
}
s = &s[..num_end];
}
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.extend(suffix_parts);
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();
if let Some(td) = self.realm.temporal_at(oh)
&& td.kind == crate::temporal_iso::TemporalKind::Duration
{
let d = &td.duration;
return Ok([
d.years as f64,
d.months as f64,
d.weeks as f64,
d.days as f64,
d.hours as f64,
d.minutes as f64,
d.seconds as f64,
d.milliseconds as f64,
d.microseconds as f64,
d.nanoseconds as f64,
]);
}
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);
for idx in 0..units.len() {
let unit = units[idx];
let singular: &'static str = duration_singular(unit);
let mut value = duration[idx];
if value == 0.0 {
value = 0.0;
}
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>,
attributes: 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 attributes: Vec<String> = 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 {
attributes.push(String::from(parts[i]));
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 private = singleton == "x";
let mut buf = alloc::vec![String::from(singleton)];
i += 1;
while i < parts.len() && (private || parts[i].len() != 1) {
buf.push(String::from(parts[i]));
i += 1;
}
other_ext.push(buf.join("-"));
}
}
attributes.sort();
ParsedLocale {
language,
script,
region,
variants,
attributes,
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));
let mut exts: Vec<String> = self.other_ext.clone();
if !kw.is_empty() || !self.attributes.is_empty() {
let mut u = String::from("u");
for a in &self.attributes {
u.push('-');
u.push_str(a);
}
for (k, v) in &kw {
u.push('-');
u.push_str(k);
if !v.is_empty() && v != "true" {
u.push('-');
u.push_str(v);
}
}
exts.push(u);
}
exts.sort_by_key(|e| {
let s = e.as_bytes().first().copied().unwrap_or(b'~');
(s == b'x', s)
});
for e in &exts {
out.push('-');
out.push_str(e);
}
out
}
}
fn is_supported_numbering_system(ns: &str) -> bool {
is_known_numbering_system(ns)
}
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
}
#[cfg(all(test, feature = "intl"))]
mod decimal_round_tests {
use super::intl_decimal_round;
use intl::number::RoundingMode;
fn fmt(n: f64, keep_frac: usize, sig: Option<usize>, inc: u32, mode: RoundingMode) -> f64 {
intl_decimal_round(n, keep_frac, sig, inc, mode)
}
#[test]
fn shortest_decimal_boundary() {
assert_eq!(fmt(1.15, 3, Some(2), 1, RoundingMode::HalfExpand), 1.2);
assert_eq!(fmt(1.15, 3, Some(2), 1, RoundingMode::HalfEven), 1.2);
assert_eq!(fmt(1.15, 3, Some(2), 1, RoundingMode::HalfCeil), 1.2);
assert_eq!(fmt(-1.15, 3, Some(2), 1, RoundingMode::HalfFloor), -1.2);
assert_eq!(
fmt(123.445, 3, Some(5), 1, RoundingMode::HalfExpand),
123.45
);
}
#[test]
fn rounding_increment_direct() {
assert_eq!(fmt(1.25, 1, None, 2, RoundingMode::HalfExpand), 1.2);
assert_eq!(fmt(1.0750, 2, None, 5, RoundingMode::HalfExpand), 1.1);
assert_eq!(fmt(1.15, 2, None, 10, RoundingMode::HalfExpand), 1.2);
assert_eq!(fmt(1.20, 2, None, 20, RoundingMode::HalfExpand), 1.2);
assert_eq!(fmt(1.5000, 2, None, 25, RoundingMode::HalfExpand), 1.5);
assert_eq!(fmt(1.2500, 3, None, 250, RoundingMode::HalfExpand), 1.25);
}
#[test]
fn plain_fraction_rounding() {
assert_eq!(fmt(1.005, 2, None, 1, RoundingMode::HalfExpand), 1.01);
assert_eq!(fmt(2.5, 0, None, 1, RoundingMode::HalfEven), 2.0);
assert_eq!(fmt(3.5, 0, None, 1, RoundingMode::HalfEven), 4.0);
assert_eq!(fmt(0.0, 2, None, 1, RoundingMode::HalfExpand), 0.0);
}
}
#[cfg(test)]
mod resolution_helper_tests {
use super::*;
#[test]
fn split_u_keyword_hc() {
assert_eq!(
split_u_keyword("en-US-u-hc-h23", "hc"),
(String::from("en-US"), Some(String::from("h23")))
);
assert_eq!(
split_u_keyword("en-u-ca-gregory-hc-h11-nu-arab", "hc"),
(
String::from("en-u-ca-gregory-nu-arab"),
Some(String::from("h11"))
)
);
assert_eq!(
split_u_keyword("en-US", "hc"),
(String::from("en-US"), None)
);
assert_eq!(
split_u_keyword("de-u-hc-h24", "hc"),
(String::from("de"), Some(String::from("h24")))
);
}
#[test]
fn calendar_canonicalization() {
assert_eq!(canonicalize_calendar("islamicc"), "islamic-civil");
assert_eq!(canonicalize_calendar("ISO8601"), "iso8601");
assert_eq!(canonicalize_calendar("ethiopic-amete-alem"), "ethioaa");
assert_eq!(canonicalize_calendar("islamic"), "islamic-civil");
assert_eq!(canonicalize_calendar("islamic-rgsa"), "islamic-civil");
assert_eq!(canonicalize_calendar("gregory"), "gregory");
}
#[test]
fn resolve_ca_option_and_extension() {
assert_eq!(
resolve_ca_key("en", "en-u-ca-iso8601", Some("invalid")),
(String::from("iso8601"), String::from("-ca-iso8601"))
);
assert_eq!(
resolve_ca_key("en", "en-u-ca-gregory", Some("iso8601")),
(String::from("iso8601"), String::new())
);
assert_eq!(
resolve_ca_key("en", "en-u-ca-invalid", Some("invalid2")),
(String::from("gregory"), String::new())
);
}
#[test]
fn resolve_nu_rejects_generic_aliases() {
assert_eq!(
resolve_nu_key("ja-JP", "ja-JP-u-nu-native", None),
(String::from("latn"), String::new())
);
assert_eq!(
resolve_nu_key("en", "en", Some("finance")),
(String::from("latn"), String::new())
);
assert_eq!(
resolve_nu_key("en", "en-u-nu-arab", None),
(String::from("arab"), String::from("-nu-arab"))
);
}
}
#[cfg(all(test, feature = "intl"))]
mod exact_decimal_tests {
use super::*;
use intl::number::RoundingMode;
fn digits(v: &[u8]) -> alloc::vec::Vec<u8> {
v.to_vec()
}
#[test]
fn raw_fixed_rounds_half_expand() {
let (i, f) = to_raw_fixed(
false,
digits(&[1]),
digits(&[6, 2, 5]),
2,
RoundingMode::HalfExpand,
);
assert_eq!((i, f), (digits(&[1]), digits(&[6, 3])));
}
#[test]
fn raw_precision_rounds_to_sig_digits() {
let (i, f) = to_raw_precision(
false,
digits(&[1]),
digits(&[2, 3, 4]),
3,
RoundingMode::HalfExpand,
);
assert_eq!((i, f), (digits(&[1]), digits(&[2, 3])));
}
#[test]
fn round_up_modes() {
assert!(exact_round_up(&[5], RoundingMode::HalfExpand, false));
assert!(!exact_round_up(&[4, 9], RoundingMode::HalfExpand, false));
assert!(!exact_round_up(&[5], RoundingMode::Trunc, false));
assert!(exact_round_up(&[1], RoundingMode::Expand, false));
}
#[test]
fn group_thousands() {
assert_eq!(group_thousands_sep("100000", ","), "100,000");
assert_eq!(
group_thousands_sep("987654321987654321", ","),
"987,654,321,987,654,321"
);
assert_eq!(group_thousands_sep("12", ","), "12");
}
}
#[cfg(all(test, feature = "intl"))]
mod alt_calendar_format_tests {
use crate::nbexec::temporal_calendar::iso_to_fields;
use crate::temporal_iso::IsoDate;
use intl::datetime::{DateStyle, format_islamic_date, format_persian_date};
#[test]
fn islamic_month_fields_and_names() {
let iso = IsoDate {
year: 2024,
month: 3,
day: 26,
};
let f = iso_to_fields("islamic-tbla", iso);
assert_eq!(f.month, 9, "Ramadan is the 9th Islamic month");
assert_eq!(f.year, 1445);
let long = format_islamic_date("en", f.year, f.month, f.day, DateStyle::Long);
assert!(long.contains("Ramadan"), "long includes month name: {long}");
let short = format_islamic_date("en", f.year, f.month, f.day, DateStyle::Short);
assert!(
!short.contains("Ramadan"),
"short uses a numeric month: {short}"
);
}
#[test]
fn persian_month_fields_and_names() {
let iso = IsoDate {
year: 2024,
month: 3,
day: 26,
};
let f = iso_to_fields("persian", iso);
assert_eq!(f.month, 1, "Farvardin is the 1st Persian month");
assert_eq!(f.year, 1403);
let long = format_persian_date("en", f.year, f.month, f.day, DateStyle::Long);
assert!(
long.contains("Farvardin"),
"long includes month name: {long}"
);
let short = format_persian_date("en", f.year, f.month, f.day, DateStyle::Short);
assert!(
!short.contains("Farvardin"),
"short uses a numeric month: {short}"
);
}
}