1use crate::host::{self, with_host};
19use fusevm::Value;
20
21const CURRENCIES: &[(&str, &str, usize, bool)] = &[
28 ("AFN", "AFN", 0, true),
29 ("ALL", "ALL", 0, true),
30 ("AUD", "A$", 2, false),
31 ("BHD", "BHD", 3, true),
32 ("BIF", "BIF", 0, true),
33 ("BRL", "R$", 2, false),
34 ("CAD", "CA$", 2, false),
35 ("CLP", "CLP", 0, true),
36 ("CNY", "CN¥", 2, false),
37 ("COP", "COP", 0, true),
38 ("DJF", "DJF", 0, true),
39 ("EUR", "€", 2, false),
40 ("GBP", "£", 2, false),
41 ("GNF", "GNF", 0, true),
42 ("HKD", "HK$", 2, false),
43 ("HUF", "HUF", 0, true),
44 ("IDR", "IDR", 0, true),
45 ("ILS", "₪", 2, false),
46 ("INR", "₹", 2, false),
47 ("IQD", "IQD", 0, true),
48 ("IRR", "IRR", 0, true),
49 ("ISK", "ISK", 0, true),
50 ("JOD", "JOD", 3, true),
51 ("JPY", "¥", 0, false),
52 ("KMF", "KMF", 0, true),
53 ("KPW", "KPW", 0, true),
54 ("KRW", "₩", 0, false),
55 ("KWD", "KWD", 3, true),
56 ("LAK", "LAK", 0, true),
57 ("LBP", "LBP", 0, true),
58 ("LYD", "LYD", 3, true),
59 ("MGA", "MGA", 0, true),
60 ("MMK", "MMK", 0, true),
61 ("MXN", "MX$", 2, false),
62 ("NZD", "NZ$", 2, false),
63 ("OMR", "OMR", 3, true),
64 ("PHP", "₱", 2, false),
65 ("PKR", "PKR", 0, true),
66 ("PYG", "PYG", 0, true),
67 ("RWF", "RWF", 0, true),
68 ("SLL", "SLL", 0, true),
69 ("SOS", "SOS", 0, true),
70 ("SYP", "SYP", 0, true),
71 ("TND", "TND", 3, true),
72 ("TWD", "NT$", 2, false),
73 ("UGX", "UGX", 0, true),
74 ("USD", "$", 2, false),
75 ("VND", "₫", 0, false),
76 ("VUV", "VUV", 0, true),
77 ("XAF", "FCFA", 0, true),
78 ("XCD", "EC$", 2, false),
79 ("XCG", "Cg.", 2, true),
80 ("XOF", "F\u{202f}CFA", 0, true),
81 ("XPF", "CFPF", 0, true),
82 ("YER", "YER", 0, true),
83];
84
85pub enum Num<'a> {
87 Float(f64),
88 BigInt(&'a str, bool),
90}
91
92enum Style {
93 Decimal,
94 Percent,
95 Currency(String),
97}
98
99#[derive(PartialEq)]
100enum SignDisplay {
101 Auto,
102 Always,
103 ExceptZero,
104 Negative,
105 Never,
106}
107
108enum Rounding {
109 Fraction { min: usize, max: usize },
110 Significant { min: usize, max: usize },
111}
112
113#[derive(PartialEq)]
114enum Grouping {
115 Always,
116 Min2,
117 Off,
118}
119
120struct Format {
121 style: Style,
122 sign: SignDisplay,
123 min_int: usize,
124 rounding: Rounding,
125 grouping: Grouping,
126}
127
128struct Decimal {
132 digits: Vec<u8>,
133 point: i64,
134}
135
136impl Decimal {
137 fn from_f64(x: f64) -> Decimal {
139 if x == 0.0 {
140 return Decimal {
141 digits: Vec::new(),
142 point: 0,
143 };
144 }
145 let s = format!("{x:e}");
147 let (mant, exp) = s.split_once('e').expect("{:e} has an exponent");
148 let exp: i64 = exp.parse().expect("integer exponent");
149 let digits: Vec<u8> = mant
150 .bytes()
151 .filter(u8::is_ascii_digit)
152 .map(|b| b - b'0')
153 .collect();
154 let mut d = Decimal {
155 digits,
156 point: exp + 1,
157 };
158 d.trim();
159 d
160 }
161
162 fn from_digits(s: &str) -> Decimal {
163 let digits: Vec<u8> = s.bytes().map(|b| b - b'0').collect();
164 let mut d = Decimal {
165 point: digits.len() as i64,
166 digits,
167 };
168 d.trim();
169 d
170 }
171
172 fn trim(&mut self) {
174 let lead = self.digits.iter().take_while(|&&b| b == 0).count();
175 self.digits.drain(..lead);
176 self.point -= lead as i64;
177 while self.digits.last() == Some(&0) {
178 self.digits.pop();
179 }
180 if self.digits.is_empty() {
181 self.point = 0;
182 }
183 }
184
185 fn round_to(&mut self, keep: i64) {
187 let n = self.digits.len() as i64;
188 if keep >= n {
189 return;
190 }
191 if keep < 0 {
192 self.digits.clear();
193 self.point = 0;
194 return;
195 }
196 let up = self.digits[keep as usize] >= 5;
197 self.digits.truncate(keep as usize);
198 if up {
199 let mut i = self.digits.len();
200 loop {
201 if i == 0 {
202 self.digits.insert(0, 1);
203 self.point += 1;
204 break;
205 }
206 i -= 1;
207 if self.digits[i] == 9 {
208 self.digits[i] = 0;
209 } else {
210 self.digits[i] += 1;
211 break;
212 }
213 }
214 }
215 self.trim();
216 }
217
218 fn times_100(&mut self) {
220 if !self.digits.is_empty() {
221 self.point += 2;
222 }
223 }
224
225 fn split(&self) -> (String, String) {
227 let digit = |i: i64| -> char {
228 if i >= 0 && (i as usize) < self.digits.len() {
229 (b'0' + self.digits[i as usize]) as char
230 } else {
231 '0'
232 }
233 };
234 let int: String = if self.point > 0 {
235 (0..self.point).map(digit).collect()
236 } else {
237 String::new()
238 };
239 let frac_len = self.digits.len() as i64 - self.point;
240 let frac: String = (self.point..self.point + frac_len.max(0))
241 .map(digit)
242 .collect();
243 (int, frac)
244 }
245}
246
247fn out_of_range(value: &str, name: &str) -> String {
249 host::range_error(&format!(
250 "Value {value} out of range for Number.prototype.toLocaleString options property {name}"
251 ))
252}
253
254fn enum_option(opts: &Value, name: &str, allowed: &[&str]) -> Result<Option<String>, String> {
256 match string_option(opts, name)? {
257 Some(s) if !allowed.contains(&s.as_str()) => Err(out_of_range(&s, name)),
258 other => Ok(other),
259 }
260}
261
262fn digit_option(opts: &Value, name: &str, lo: f64, hi: f64) -> Result<Option<usize>, String> {
265 let v = crate::builtins::get_property(opts, name)?;
266 if matches!(v, Value::Undef) {
267 return Ok(None);
268 }
269 let n = host::to_number_value(&v)?;
270 if n.is_nan() || n < lo || n > hi {
271 return Err(host::range_error(&format!("{name} value is out of range.")));
272 }
273 Ok(Some(n.floor() as usize))
274}
275
276fn string_option(opts: &Value, name: &str) -> Result<Option<String>, String> {
278 let v = crate::builtins::get_property(opts, name)?;
279 if matches!(v, Value::Undef) {
280 return Ok(None);
281 }
282 Ok(Some(to_str(&v)?))
283}
284
285fn read_format(opts: &Value) -> Result<Format, String> {
286 if with_host(|h| h.is_null(opts)) {
287 return Err(host::type_error(
288 "Number.prototype.toLocaleString called on null or undefined",
289 ));
290 }
291 let is_object = matches!(opts, Value::Obj(_)) && !with_host(|h| host::is_primitive(h, opts));
294 if !is_object {
295 return Ok(Format {
296 style: Style::Decimal,
297 sign: SignDisplay::Auto,
298 min_int: 1,
299 rounding: Rounding::Fraction { min: 0, max: 3 },
300 grouping: Grouping::Always,
301 });
302 }
303 let style_name = enum_option(opts, "style", &["decimal", "percent", "currency", "unit"])?
307 .unwrap_or_else(|| "decimal".into());
308 let currency = string_option(opts, "currency")?;
310 if let Some(code) = ¤cy {
311 if code.len() != 3 || !code.bytes().all(|b| b.is_ascii_alphabetic()) {
312 return Err(host::range_error(&format!(
313 "Invalid currency code : {code}"
314 )));
315 }
316 }
317 let display = enum_option(
318 opts,
319 "currencyDisplay",
320 &["code", "symbol", "narrowSymbol", "name"],
321 )?;
322 let (style, currency_digits) = match style_name.as_str() {
323 "percent" => (Style::Percent, None),
324 "currency" => {
325 let Some(code) = currency else {
326 return Err(host::type_error(
327 "Currency code is required with currency style.",
328 ));
329 };
330 let code = code.to_ascii_uppercase();
331 let known = CURRENCIES.iter().find(|(c, ..)| *c == code);
332 let digits = known.map(|(_, _, d, _)| *d).unwrap_or(2);
333 let prefix = match (display.as_deref(), known) {
336 (Some("code"), _) | (_, None) => format!("{code}\u{a0}"),
337 (_, Some((_, sym, _, spaced))) => {
338 let gap = if *spaced { "\u{a0}" } else { "" };
339 format!("{sym}{gap}")
340 }
341 };
342 (Style::Currency(prefix), Some(digits))
343 }
344 _ => (Style::Decimal, None),
346 };
347 let min_int = digit_option(opts, "minimumIntegerDigits", 1.0, 21.0)?.unwrap_or(1);
348 let min_frac = digit_option(opts, "minimumFractionDigits", 0.0, 100.0)?;
349 let max_frac = digit_option(opts, "maximumFractionDigits", 0.0, 100.0)?;
350 let min_sig = digit_option(opts, "minimumSignificantDigits", 1.0, 21.0)?;
352 let max_sig = digit_option(opts, "maximumSignificantDigits", 1.0, 21.0)?;
353 let rounding = if min_sig.is_some() || max_sig.is_some() {
354 let min = min_sig.unwrap_or(1);
355 let max = max_sig.unwrap_or(21);
356 if min > max {
357 return Err(host::range_error(
358 "maximumSignificantDigits value is out of range.",
359 ));
360 }
361 Rounding::Significant { min, max }
362 } else {
363 let (min_default, max_default) = match (&style, currency_digits) {
364 (_, Some(d)) => (d, d),
365 (Style::Percent, _) => (0, 0),
366 _ => (0, 3),
367 };
368 let (min, max) = match (min_frac, max_frac) {
369 (None, None) => (min_default, max_default),
370 (Some(mn), None) => (mn, mn.max(max_default)),
371 (None, Some(mx)) => (min_default.min(mx), mx),
372 (Some(mn), Some(mx)) => {
373 if mn > mx {
374 return Err(host::range_error(
375 "maximumFractionDigits value is out of range.",
376 ));
377 }
378 (mn, mx)
379 }
380 };
381 Rounding::Fraction { min, max }
382 };
383 let grouping_v = crate::builtins::get_property(opts, "useGrouping")?;
387 let grouping = match grouping_v {
388 Value::Undef | Value::Bool(true) => Grouping::Always,
389 ref v if !with_host(|h| h.truthy(v)) => Grouping::Off,
390 ref v => match to_str(v)?.as_str() {
391 "min2" => Grouping::Min2,
392 "always" | "auto" | "true" | "false" => Grouping::Always,
393 other => return Err(out_of_range(other, "useGrouping")),
394 },
395 };
396 let sign = match enum_option(
397 opts,
398 "signDisplay",
399 &["auto", "never", "always", "exceptZero", "negative"],
400 )?
401 .as_deref()
402 {
403 Some("never") => SignDisplay::Never,
404 Some("always") => SignDisplay::Always,
405 Some("exceptZero") => SignDisplay::ExceptZero,
406 Some("negative") => SignDisplay::Negative,
407 _ => SignDisplay::Auto,
408 };
409 Ok(Format {
410 style,
411 sign,
412 min_int,
413 rounding,
414 grouping,
415 })
416}
417
418fn group(int: &str, grouping: &Grouping) -> String {
419 if *grouping == Grouping::Off || (*grouping == Grouping::Min2 && int.len() < 5) {
420 return int.to_string();
421 }
422 let mut out = String::with_capacity(int.len() + int.len() / 3);
423 for (i, c) in int.chars().enumerate() {
424 if i > 0 && (int.len() - i) % 3 == 0 {
425 out.push(',');
426 }
427 out.push(c);
428 }
429 out
430}
431
432pub fn to_locale_string(x: Num, opts: &Value) -> Result<String, String> {
434 let fmt = read_format(opts)?;
435 let (neg, mut dec, special) = match x {
436 Num::Float(f) if f.is_nan() => (false, Decimal::from_digits(""), Some("NaN")),
437 Num::Float(f) if f.is_infinite() => (f < 0.0, Decimal::from_digits(""), Some("∞")),
438 Num::Float(f) => (f.is_sign_negative(), Decimal::from_f64(f.abs()), None),
439 Num::BigInt(digits, neg) => (neg, Decimal::from_digits(digits), None),
440 };
441 let body = if let Some(text) = special {
442 text.to_string()
443 } else {
444 if matches!(fmt.style, Style::Percent) {
445 dec.times_100();
446 }
447 let (min_frac_shown, min_sig) = match fmt.rounding {
448 Rounding::Fraction { min, max } => {
449 dec.round_to(dec.point + max as i64);
450 (min, None)
451 }
452 Rounding::Significant { min, max } => {
453 dec.round_to(max as i64);
454 (0, Some(min))
455 }
456 };
457 let (mut int, mut frac) = dec.split();
458 if int.len() < fmt.min_int {
459 int = format!("{}{int}", "0".repeat(fmt.min_int - int.len()));
460 }
461 let mut min_frac = min_frac_shown;
462 if let Some(min_sig) = min_sig {
463 let sig_int = int.trim_start_matches('0').len();
466 let lead_frac_zeros = if sig_int == 0 {
467 frac.bytes().take_while(|&b| b == b'0').count()
468 } else {
469 0
470 };
471 let have = (sig_int + frac.len() - lead_frac_zeros).max(1);
472 min_frac = frac.len() + min_sig.saturating_sub(have);
473 }
474 while frac.len() < min_frac {
475 frac.push('0');
476 }
477 let grouped = group(&int, &fmt.grouping);
478 if frac.is_empty() {
479 grouped
480 } else {
481 format!("{grouped}.{frac}")
482 }
483 };
484 let zero = special.is_none() && dec.digits.is_empty();
487 let nan = special == Some("NaN");
488 let sign = match fmt.sign {
489 SignDisplay::Never => "",
490 SignDisplay::ExceptZero if zero || nan => "",
491 SignDisplay::Negative if zero => "",
492 _ if neg => "-",
493 SignDisplay::Always | SignDisplay::ExceptZero => "+",
494 _ => "",
495 };
496 Ok(match fmt.style {
497 Style::Decimal => format!("{sign}{body}"),
498 Style::Percent => format!("{sign}{body}%"),
499 Style::Currency(prefix) => format!("{sign}{prefix}{body}"),
500 })
501}
502
503fn to_str(v: &Value) -> Result<String, String> {
505 let s = host::to_string_value(v)?;
506 Ok(with_host(|h| h.str_of(&s)))
507}