1use num_traits::FromPrimitive;
4
5pub(crate) fn to_uint32(value: f64) -> u32 {
7 if value == 0.0 || !value.is_finite() {
8 return 0;
9 }
10 value.trunc().rem_euclid(4_294_967_296.0) as u32
11}
12
13pub(crate) fn to_int32(value: f64) -> i32 {
14 to_uint32(value) as i32
15}
16
17pub(crate) fn to_uint16(value: f64) -> u16 {
19 to_uint32(value) as u16
20}
21
22pub(crate) fn to_uint8(value: f64) -> u8 {
24 to_uint32(value) as u8
25}
26
27pub fn format_number_js(n: f64) -> String {
30 if n.is_nan() {
31 "NaN".to_string()
32 } else if n.is_infinite() {
33 if n > 0.0 {
34 "Infinity".to_string()
35 } else {
36 "-Infinity".to_string()
37 }
38 } else if n == 0.0 {
39 "0".to_string()
40 } else if n.abs() >= 1e21 || n.abs() < 1e-6 {
41 jsify_exponent(&format!("{n:e}"))
42 } else {
43 n.to_string()
44 }
45}
46
47pub fn to_fixed_js(x: f64, digits: f64) -> Result<String, String> {
49 let d = digits.trunc();
50 if !(0.0..=100.0).contains(&d) || digits.is_nan() {
51 return Err("toFixed digits must be between 0 and 100".to_string());
52 }
53 if !x.is_finite() || x.abs() >= 1e21 {
54 return Ok(format_number_js(x));
55 }
56 let digits = d as usize;
57 let magnitude = fixed_ties_away_from_zero(x.abs(), digits);
58 Ok(if x < 0.0 {
61 format!("-{magnitude}")
62 } else {
63 magnitude
64 })
65}
66
67const EXACT_FRACTION_DIGITS: usize = 1074;
70
71fn fixed_ties_away_from_zero(x: f64, digits: usize) -> String {
76 let rounded = format!("{x:.digits$}");
77 let one_more = digits + 1;
80 if !format!("{x:.one_more$}").ends_with('5') {
81 return rounded;
82 }
83 let exact = format!("{x:.EXACT_FRACTION_DIGITS$}");
84 let Some(point) = exact.find('.') else {
85 return rounded;
86 };
87 let fraction_end = point + 1 + digits;
88 let Some(rest) = exact.get(fraction_end..) else {
89 return rounded;
90 };
91 let is_tie = rest
92 .strip_prefix('5')
93 .is_some_and(|tail| tail.bytes().all(|b| b == b'0'));
94 if !is_tie {
95 return rounded;
96 }
97 let kept_end = if digits == 0 { point } else { fraction_end };
98 increment_last_digit(&exact[..kept_end])
99}
100
101fn increment_last_digit(number: &str) -> String {
104 let mut digits = number.to_owned().into_bytes();
105 for byte in digits.iter_mut().rev() {
106 match *byte {
107 b'.' => continue,
108 b'9' => *byte = b'0',
109 _ => {
110 *byte += 1;
111 return ascii_text(digits);
112 }
113 }
114 }
115 digits.insert(0, b'1');
116 ascii_text(digits)
117}
118
119fn ascii_text(bytes: Vec<u8>) -> String {
121 bytes.into_iter().map(char::from).collect()
122}
123
124pub fn to_exponential_js(x: f64, fraction_digits: f64) -> Result<String, String> {
128 if !x.is_finite() {
129 return Ok(format_number_js(x));
130 }
131 let mantissa_exp = if fraction_digits.is_nan() {
132 format!("{x:e}")
133 } else {
134 let d = fraction_digits.trunc();
135 if !(0.0..=100.0).contains(&d) {
136 return Err("toExponential digits must be between 0 and 100".to_string());
137 }
138 format!("{:.*e}", d as usize, x)
139 };
140 Ok(jsify_exponent(&mantissa_exp))
141}
142
143pub fn to_precision_js(x: f64, precision: f64) -> Result<String, String> {
146 if precision.is_nan() {
147 return Ok(format_number_js(x));
148 }
149 let p = precision.trunc();
150 if !(1.0..=100.0).contains(&p) {
151 return Err("toPrecision precision must be between 1 and 100".to_string());
152 }
153 let p = p as usize;
154 if !x.is_finite() {
155 return Ok(format_number_js(x));
156 }
157 if x == 0.0 {
158 return Ok(if p == 1 {
159 "0".to_string()
160 } else {
161 format!("{:.*}", p - 1, 0.0)
162 });
163 }
164 let sci = format!("{:.*e}", p - 1, x);
169 let (_, exponent) = sci
170 .split_once('e')
171 .expect("finite exponential formatting includes an exponent");
172 let exp: i32 = exponent.parse().expect("an f64 decimal exponent fits i32");
173 if exp < -6 || exp >= p as i32 {
174 return Ok(jsify_exponent(&sci));
175 }
176 let fraction_digits = (p as i32 - 1 - exp).max(0) as usize;
177 Ok(format!("{x:.fraction_digits$}"))
178}
179
180fn jsify_exponent(s: &str) -> String {
183 match s.find('e') {
184 Some(i) if !s[i + 1..].starts_with('-') => format!("{}e+{}", &s[..i], &s[i + 1..]),
185 _ => s.to_string(),
186 }
187}
188
189pub fn to_string_radix_js(x: f64, radix: f64) -> Result<String, String> {
195 let r = radix.trunc();
196 if !(2.0..=36.0).contains(&r) || radix.is_nan() {
197 return Err("toString radix must be between 2 and 36".to_string());
198 }
199 let r = r as u32;
200 if r == 10 || !x.is_finite() {
201 return Ok(format_number_js(x));
202 }
203 if x == 0.0 {
204 return Ok("0".to_string());
205 }
206
207 let negative = x < 0.0;
208 let magnitude = x.abs();
209 let integer = magnitude.trunc();
210 let mut fraction = magnitude - integer;
211
212 let int_digits = num_bigint::BigInt::from_f64(integer)
215 .expect("finite integer magnitude converts to BigInt")
216 .to_str_radix(r);
217 let mut out = String::new();
218 if negative {
219 out.push('-');
220 }
221 out.push_str(&int_digits);
222
223 if fraction > 0.0 {
224 out.push('.');
225 for _ in 0..32 {
226 fraction *= f64::from(r);
231 let digit = fraction.trunc() as u32;
232 out.push(
233 char::from_digit(digit, r).expect("fraction digit is below the validated radix"),
234 );
235 fraction -= fraction.trunc();
236 if fraction == 0.0 {
237 break;
238 }
239 }
240 }
241 Ok(out)
242}
243
244pub fn parse_int_with_radix(input: &str, radix: f64) -> f64 {
249 match u32::try_from(to_int32(radix)) {
250 Ok(radix @ (0 | 2..=36)) => parse_int_js(input, radix),
251 _ => f64::NAN,
252 }
253}
254
255pub fn parse_int_js(input: &str, radix: u32) -> f64 {
262 let s = input.trim_start_matches(is_js_whitespace);
263 if s.is_empty() {
264 return f64::NAN;
265 }
266 let (sign, rest) = strip_sign(s);
267 if rest.is_empty() {
268 return f64::NAN;
269 }
270
271 let (effective_radix, digits) = if radix == 0 || radix == 16 {
272 if let Some(after_prefix) = strip_hex_prefix(rest) {
273 (16u32, after_prefix)
274 } else if radix == 0 {
275 (10, rest)
276 } else {
277 (16, rest)
278 }
279 } else if (2..=36).contains(&radix) {
280 (radix, rest)
281 } else {
282 return f64::NAN;
283 };
284
285 let mut consumed = 0usize;
286 let mut value: f64 = 0.0;
287 for ch in digits.chars() {
288 let Some(digit) = ch.to_digit(effective_radix) else {
289 break;
290 };
291 value = value * (effective_radix as f64) + (digit as f64);
292 consumed += ch.len_utf8();
293 }
294 if consumed == 0 {
295 return f64::NAN;
296 }
297 sign * value
298}
299
300pub fn parse_float_js(input: &str) -> f64 {
301 let s = input.trim_start_matches(is_js_whitespace);
302 if s.is_empty() {
303 return f64::NAN;
304 }
305 let (sign, rest) = strip_sign(s);
306 if rest.is_empty() {
307 return f64::NAN;
308 }
309 if let Some(remainder) = rest.strip_prefix("Infinity") {
310 let _ = remainder;
312 return sign * f64::INFINITY;
313 }
314
315 let prefix = float_prefix(rest);
316 if prefix.is_empty() {
317 return f64::NAN;
318 }
319 if prefix == "." {
320 return f64::NAN;
321 }
322 match prefix.parse::<f64>() {
323 Ok(v) => sign * v,
324 Err(_) => f64::NAN,
325 }
326}
327
328pub fn string_to_number_js(input: &str) -> f64 {
335 let s = input.trim_matches(is_js_whitespace);
336 if s.is_empty() {
337 return 0.0;
338 }
339
340 if let Some(rest) = strip_radix_prefix(s, b'x') {
341 return parse_radix_strict(rest, 16);
342 }
343 if let Some(rest) = strip_radix_prefix(s, b'o') {
344 return parse_radix_strict(rest, 8);
345 }
346 if let Some(rest) = strip_radix_prefix(s, b'b') {
347 return parse_radix_strict(rest, 2);
348 }
349
350 let (sign, rest) = strip_sign(s);
351 if rest == "Infinity" {
352 return sign * f64::INFINITY;
353 }
354 if !rest.bytes().all(is_decimal_literal_byte) {
357 return f64::NAN;
358 }
359 match rest.parse::<f64>() {
360 Ok(v) => sign * v,
361 Err(_) => f64::NAN,
362 }
363}
364
365fn strip_radix_prefix(s: &str, marker: u8) -> Option<&str> {
366 let bytes = s.as_bytes();
367 if bytes.len() >= 2 && bytes[0] == b'0' && bytes[1] | 0x20 == marker {
368 Some(&s[2..])
369 } else {
370 None
371 }
372}
373
374fn parse_radix_strict(digits: &str, radix: u32) -> f64 {
375 if digits.is_empty() {
376 return f64::NAN;
377 }
378 let mut value = 0.0_f64;
379 for ch in digits.chars() {
380 let Some(d) = ch.to_digit(radix) else {
381 return f64::NAN;
382 };
383 value = value * f64::from(radix) + f64::from(d);
384 }
385 value
386}
387
388fn is_decimal_literal_byte(b: u8) -> bool {
389 b.is_ascii_digit() || matches!(b, b'.' | b'e' | b'E' | b'+' | b'-')
390}
391
392fn strip_sign(s: &str) -> (f64, &str) {
393 if let Some(rest) = s.strip_prefix('+') {
394 (1.0, rest)
395 } else if let Some(rest) = s.strip_prefix('-') {
396 (-1.0, rest)
397 } else {
398 (1.0, s)
399 }
400}
401
402fn strip_hex_prefix(s: &str) -> Option<&str> {
405 s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))
406}
407
408fn float_prefix(s: &str) -> &str {
409 let bytes = s.as_bytes();
410 let mut i = 0usize;
411 let n = bytes.len();
412
413 let int_start = i;
414 while i < n && bytes[i].is_ascii_digit() {
415 i += 1;
416 }
417 let int_end = i;
418
419 let mut frac_end = int_end;
420 if i < n && bytes[i] == b'.' {
421 i += 1;
422 let frac_start = i;
423 while i < n && bytes[i].is_ascii_digit() {
424 i += 1;
425 }
426 if frac_start == i && int_end == int_start {
427 return "";
429 }
430 frac_end = i;
431 } else if int_end == int_start {
432 return "";
433 }
434
435 let exp_start = i;
437 if i < n && (bytes[i] == b'e' || bytes[i] == b'E') {
438 let mut j = i + 1;
439 if j < n && (bytes[j] == b'+' || bytes[j] == b'-') {
440 j += 1;
441 }
442 let digits_start = j;
443 while j < n && bytes[j].is_ascii_digit() {
444 j += 1;
445 }
446 if j > digits_start {
447 i = j;
448 } else {
449 i = exp_start;
450 }
451 }
452
453 let end = if i > frac_end { i } else { frac_end };
454 &s[..end]
455}
456
457pub(crate) fn is_js_whitespace(value: char) -> bool {
459 matches!(value, '\u{0009}'..='\u{000D}' | '\u{0020}' | '\u{00A0}' | '\u{1680}'
460 | '\u{2000}'..='\u{200A}' | '\u{2028}' | '\u{2029}' | '\u{202F}' | '\u{205F}'
461 | '\u{3000}' | '\u{FEFF}')
462}
463
464pub(crate) fn pow_js(base: f64, exponent: f64) -> f64 {
465 if exponent.is_nan() || (base.abs() == 1.0 && exponent.is_infinite()) {
466 return f64::NAN;
467 }
468 base.powf(exponent)
469}
470
471#[cfg(test)]
472mod tests {
473 use super::{parse_float_js, parse_int_js, string_to_number_js, to_fixed_js, to_uint8};
474
475 #[test]
476 fn to_fixed_matches_node_including_exact_ties() {
477 let cases: [(f64, f64, &str); 22] = [
479 (2.25, 1.0, "2.3"),
480 (0.125, 2.0, "0.13"),
481 (2.5, 0.0, "3"),
482 (-2.25, 1.0, "-2.3"),
483 (0.5, 0.0, "1"),
484 (1.5, 0.0, "2"),
485 (-0.5, 0.0, "-1"),
486 (9.5, 0.0, "10"),
487 (99.995, 2.0, "100.00"),
488 (0.0625, 3.0, "0.063"),
489 (1.005, 2.0, "1.00"),
490 (1.45, 1.0, "1.4"),
491 (8.345, 2.0, "8.35"),
492 (-0.0001, 2.0, "-0.00"),
493 (-0.0, 2.0, "0.00"),
494 (0.0, 0.0, "0"),
495 (123.456, 10.0, "123.4560000000"),
496 (0.1, 20.0, "0.10000000000000000555"),
497 (5e-324, 2.0, "0.00"),
498 (100000000000000070000.0, 1.0, "100000000000000065536.0"),
499 (0.15, 1.0, "0.1"),
500 (1e-7, 8.0, "0.00000010"),
501 ];
502 for (x, digits, expected) in cases {
503 assert_eq!(
504 to_fixed_js(x, digits).unwrap(),
505 expected,
506 "({x}).toFixed({digits})"
507 );
508 }
509 }
510
511 #[test]
512 fn to_uint8_wraps_modulo_256() {
513 assert_eq!(to_uint8(7.0), 7);
514 assert_eq!(to_uint8(256.0), 0);
515 assert_eq!(to_uint8(257.9), 1);
516 assert_eq!(to_uint8(-1.0), 255);
517 assert_eq!(to_uint8(-1.5), 255);
518 assert_eq!(to_uint8(-256.0), 0);
519 assert_eq!(to_uint8(-0.5), 0);
520 assert_eq!(to_uint8(f64::NAN), 0);
521 assert_eq!(to_uint8(f64::INFINITY), 0);
522 assert_eq!(to_uint8(f64::NEG_INFINITY), 0);
523 assert_eq!(to_uint8(2f64.powi(60) + 512.0), 0);
524 }
525
526 #[test]
527 fn formatter_invariants_hold_at_float_boundaries() {
528 let below_one = f64::from_bits(1.0_f64.to_bits() - 1);
529 for radix in 2..=36 {
530 for value in [below_one, 0.5, f64::MIN_POSITIVE, f64::from_bits(1)] {
531 let text = super::to_string_radix_js(value, f64::from(radix)).unwrap();
532 if radix != 10 {
533 assert!(text.chars().all(|c| c == '.' || c.is_digit(radix)));
534 }
535 }
536 assert!(super::to_string_radix_js(f64::MAX, f64::from(radix)).is_ok());
537 }
538 assert_eq!(
539 super::to_precision_js(f64::from_bits(1), 1.0).unwrap(),
540 "5e-324"
541 );
542 assert_eq!(super::to_precision_js(f64::MAX, 1.0).unwrap(), "2e+308");
543 assert_eq!(super::to_precision_js(-0.0, 3.0).unwrap(), "0.00");
544 }
545
546 #[test]
547 fn parse_int_decimal_prefix() {
548 assert_eq!(parse_int_js("42", 10), 42.0);
549 assert_eq!(parse_int_js("42px", 10), 42.0);
550 assert_eq!(parse_int_js(" 42 ", 10), 42.0);
551 }
552
553 #[test]
554 fn parse_int_negative_and_positive() {
555 assert_eq!(parse_int_js("-42", 10), -42.0);
556 assert_eq!(parse_int_js("+42", 10), 42.0);
557 }
558
559 #[test]
560 fn parse_int_hex_with_explicit_radix_16() {
561 assert_eq!(parse_int_js("0xff", 16), 255.0);
562 assert_eq!(parse_int_js("0XFF", 16), 255.0);
563 assert_eq!(parse_int_js("ff", 16), 255.0);
564 }
565
566 #[test]
567 fn parse_int_radix_zero_auto_detects_hex() {
568 assert_eq!(parse_int_js("0xff", 0), 255.0);
569 assert_eq!(parse_int_js("010", 0), 10.0); assert_eq!(parse_int_js("42", 0), 42.0);
571 }
572
573 #[test]
574 fn parse_int_no_digits_is_nan() {
575 assert!(parse_int_js("abc", 10).is_nan());
576 assert!(parse_int_js("", 10).is_nan());
577 assert!(parse_int_js(" ", 10).is_nan());
578 assert!(parse_int_js("0xZZ", 16).is_nan());
579 }
580
581 #[test]
582 fn parse_int_invalid_radix_is_nan() {
583 assert!(parse_int_js("42", 1).is_nan());
584 assert!(parse_int_js("42", 37).is_nan());
585 }
586
587 #[test]
588 fn parse_float_basic() {
589 assert_eq!(parse_float_js("1.5"), 1.5);
590 assert_eq!(parse_float_js("1.5x"), 1.5);
591 assert_eq!(parse_float_js(".5"), 0.5);
592 assert_eq!(parse_float_js("42"), 42.0);
593 }
594
595 #[test]
596 fn parse_float_negative_and_positive() {
597 assert_eq!(parse_float_js("-1.5"), -1.5);
598 assert_eq!(parse_float_js("+1.5"), 1.5);
599 }
600
601 #[test]
602 fn parse_float_exponent() {
603 assert_eq!(parse_float_js("1e3"), 1000.0);
604 assert_eq!(parse_float_js("1.5e2"), 150.0);
605 assert_eq!(parse_float_js("1.5e+2"), 150.0);
606 assert_eq!(parse_float_js("1.5e-2"), 0.015);
607 }
608
609 #[test]
610 fn parse_float_infinity() {
611 assert_eq!(parse_float_js("Infinity"), f64::INFINITY);
612 assert_eq!(parse_float_js("-Infinity"), f64::NEG_INFINITY);
613 }
614
615 #[test]
616 fn parse_float_no_digits_is_nan() {
617 assert!(parse_float_js("abc").is_nan());
618 assert!(parse_float_js(".").is_nan());
619 assert!(parse_float_js("").is_nan());
620 assert!(parse_float_js(" ").is_nan());
621 }
622
623 #[test]
624 fn parse_float_skips_whitespace() {
625 assert_eq!(parse_float_js(" 1.5 "), 1.5);
626 }
627
628 #[test]
629 fn parse_float_partial_exponent_keeps_mantissa() {
630 assert_eq!(parse_float_js("1.5e"), 1.5);
631 }
632
633 #[test]
634 fn number_decimal_and_scientific() {
635 assert_eq!(string_to_number_js("123"), 123.0);
636 assert_eq!(string_to_number_js("3.5"), 3.5);
637 assert_eq!(string_to_number_js("1e3"), 1000.0);
638 assert_eq!(string_to_number_js(".5"), 0.5);
639 assert_eq!(string_to_number_js("-42"), -42.0);
640 assert_eq!(string_to_number_js(" 7 "), 7.0);
641 }
642
643 #[test]
644 fn number_radix_prefixes() {
645 assert_eq!(string_to_number_js("0xff"), 255.0);
646 assert_eq!(string_to_number_js("0XFF"), 255.0);
647 assert_eq!(string_to_number_js("0o17"), 15.0);
648 assert_eq!(string_to_number_js("0O17"), 15.0);
649 assert_eq!(string_to_number_js("0b101"), 5.0);
650 assert_eq!(string_to_number_js("0B101"), 5.0);
651 assert_eq!(string_to_number_js(" 0xff "), 255.0);
652 }
653
654 #[test]
655 fn number_empty_is_zero() {
656 assert_eq!(string_to_number_js(""), 0.0);
657 assert_eq!(string_to_number_js(" "), 0.0);
658 }
659
660 #[test]
661 fn number_infinity() {
662 assert_eq!(string_to_number_js("Infinity"), f64::INFINITY);
663 assert_eq!(string_to_number_js("+Infinity"), f64::INFINITY);
664 assert_eq!(string_to_number_js("-Infinity"), f64::NEG_INFINITY);
665 }
666
667 #[test]
668 fn number_rejects_trailing_junk_and_bad_radix() {
669 assert!(string_to_number_js("12px").is_nan());
670 assert!(string_to_number_js("0xZZ").is_nan());
671 assert!(string_to_number_js("0x").is_nan());
672 assert!(string_to_number_js("0b12").is_nan());
673 assert!(string_to_number_js("0xff.5").is_nan());
674 }
675
676 #[test]
677 fn number_rejects_signed_radix_literals() {
678 assert!(string_to_number_js("-0xff").is_nan());
680 assert!(string_to_number_js("+0b101").is_nan());
681 }
682
683 #[test]
684 fn number_rejects_rust_only_float_spellings() {
685 assert!(string_to_number_js("inf").is_nan());
687 assert!(string_to_number_js("infinity").is_nan());
688 assert!(string_to_number_js("nan").is_nan());
689 assert!(string_to_number_js("NaN").is_nan());
690 }
691}