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> {
53 let d = digits.trunc();
54 if !(0.0..=100.0).contains(&d) || digits.is_nan() {
55 return Err("toFixed digits must be between 0 and 100".to_string());
56 }
57 if !x.is_finite() || x.abs() >= 1e21 {
58 return Ok(format_number_js(x));
59 }
60 Ok(format!("{:.*}", d as usize, x))
61}
62
63pub fn to_exponential_js(x: f64, fraction_digits: f64) -> Result<String, String> {
67 if !x.is_finite() {
68 return Ok(format_number_js(x));
69 }
70 let mantissa_exp = if fraction_digits.is_nan() {
71 format!("{x:e}")
72 } else {
73 let d = fraction_digits.trunc();
74 if !(0.0..=100.0).contains(&d) {
75 return Err("toExponential digits must be between 0 and 100".to_string());
76 }
77 format!("{:.*e}", d as usize, x)
78 };
79 Ok(jsify_exponent(&mantissa_exp))
80}
81
82pub fn to_precision_js(x: f64, precision: f64) -> Result<String, String> {
85 if precision.is_nan() {
86 return Ok(format_number_js(x));
87 }
88 let p = precision.trunc();
89 if !(1.0..=100.0).contains(&p) {
90 return Err("toPrecision precision must be between 1 and 100".to_string());
91 }
92 let p = p as usize;
93 if !x.is_finite() {
94 return Ok(format_number_js(x));
95 }
96 if x == 0.0 {
97 return Ok(if p == 1 {
98 "0".to_string()
99 } else {
100 format!("{:.*}", p - 1, 0.0)
101 });
102 }
103 let sci = format!("{:.*e}", p - 1, x);
108 let (_, exponent) = sci
109 .split_once('e')
110 .expect("finite exponential formatting includes an exponent");
111 let exp: i32 = exponent.parse().expect("an f64 decimal exponent fits i32");
112 if exp < -6 || exp >= p as i32 {
113 return Ok(jsify_exponent(&sci));
114 }
115 let fraction_digits = (p as i32 - 1 - exp).max(0) as usize;
116 Ok(format!("{x:.fraction_digits$}"))
117}
118
119fn jsify_exponent(s: &str) -> String {
122 match s.find('e') {
123 Some(i) if !s[i + 1..].starts_with('-') => format!("{}e+{}", &s[..i], &s[i + 1..]),
124 _ => s.to_string(),
125 }
126}
127
128pub fn to_string_radix_js(x: f64, radix: f64) -> Result<String, String> {
134 let r = radix.trunc();
135 if !(2.0..=36.0).contains(&r) || radix.is_nan() {
136 return Err("toString radix must be between 2 and 36".to_string());
137 }
138 let r = r as u32;
139 if r == 10 || !x.is_finite() {
140 return Ok(format_number_js(x));
141 }
142 if x == 0.0 {
143 return Ok("0".to_string());
144 }
145
146 let negative = x < 0.0;
147 let magnitude = x.abs();
148 let integer = magnitude.trunc();
149 let mut fraction = magnitude - integer;
150
151 let int_digits = num_bigint::BigInt::from_f64(integer)
154 .expect("finite integer magnitude converts to BigInt")
155 .to_str_radix(r);
156 let mut out = String::new();
157 if negative {
158 out.push('-');
159 }
160 out.push_str(&int_digits);
161
162 if fraction > 0.0 {
163 out.push('.');
164 for _ in 0..32 {
165 fraction *= f64::from(r);
170 let digit = fraction.trunc() as u32;
171 out.push(
172 char::from_digit(digit, r).expect("fraction digit is below the validated radix"),
173 );
174 fraction -= fraction.trunc();
175 if fraction == 0.0 {
176 break;
177 }
178 }
179 }
180 Ok(out)
181}
182
183pub fn parse_int_with_radix(input: &str, radix: f64) -> f64 {
188 match u32::try_from(to_int32(radix)) {
189 Ok(radix @ (0 | 2..=36)) => parse_int_js(input, radix),
190 _ => f64::NAN,
191 }
192}
193
194pub fn parse_int_js(input: &str, radix: u32) -> f64 {
201 let s = input.trim_start_matches(is_js_whitespace);
202 if s.is_empty() {
203 return f64::NAN;
204 }
205 let (sign, rest) = strip_sign(s);
206 if rest.is_empty() {
207 return f64::NAN;
208 }
209
210 let (effective_radix, digits) = if radix == 0 || radix == 16 {
211 if let Some(after_prefix) = strip_hex_prefix(rest) {
212 (16u32, after_prefix)
213 } else if radix == 0 {
214 (10, rest)
215 } else {
216 (16, rest)
217 }
218 } else if (2..=36).contains(&radix) {
219 (radix, rest)
220 } else {
221 return f64::NAN;
222 };
223
224 let mut consumed = 0usize;
225 let mut value: f64 = 0.0;
226 for ch in digits.chars() {
227 let Some(digit) = ch.to_digit(effective_radix) else {
228 break;
229 };
230 value = value * (effective_radix as f64) + (digit as f64);
231 consumed += ch.len_utf8();
232 }
233 if consumed == 0 {
234 return f64::NAN;
235 }
236 sign * value
237}
238
239pub fn parse_float_js(input: &str) -> f64 {
240 let s = input.trim_start_matches(is_js_whitespace);
241 if s.is_empty() {
242 return f64::NAN;
243 }
244 let (sign, rest) = strip_sign(s);
245 if rest.is_empty() {
246 return f64::NAN;
247 }
248 if let Some(remainder) = rest.strip_prefix("Infinity") {
249 let _ = remainder;
251 return sign * f64::INFINITY;
252 }
253
254 let prefix = float_prefix(rest);
255 if prefix.is_empty() {
256 return f64::NAN;
257 }
258 if prefix == "." {
259 return f64::NAN;
260 }
261 match prefix.parse::<f64>() {
262 Ok(v) => sign * v,
263 Err(_) => f64::NAN,
264 }
265}
266
267pub fn string_to_number_js(input: &str) -> f64 {
274 let s = input.trim_matches(is_js_whitespace);
275 if s.is_empty() {
276 return 0.0;
277 }
278
279 if let Some(rest) = strip_radix_prefix(s, b'x') {
280 return parse_radix_strict(rest, 16);
281 }
282 if let Some(rest) = strip_radix_prefix(s, b'o') {
283 return parse_radix_strict(rest, 8);
284 }
285 if let Some(rest) = strip_radix_prefix(s, b'b') {
286 return parse_radix_strict(rest, 2);
287 }
288
289 let (sign, rest) = strip_sign(s);
290 if rest == "Infinity" {
291 return sign * f64::INFINITY;
292 }
293 if !rest.bytes().all(is_decimal_literal_byte) {
296 return f64::NAN;
297 }
298 match rest.parse::<f64>() {
299 Ok(v) => sign * v,
300 Err(_) => f64::NAN,
301 }
302}
303
304fn strip_radix_prefix(s: &str, marker: u8) -> Option<&str> {
305 let bytes = s.as_bytes();
306 if bytes.len() >= 2 && bytes[0] == b'0' && bytes[1] | 0x20 == marker {
307 Some(&s[2..])
308 } else {
309 None
310 }
311}
312
313fn parse_radix_strict(digits: &str, radix: u32) -> f64 {
314 if digits.is_empty() {
315 return f64::NAN;
316 }
317 let mut value = 0.0_f64;
318 for ch in digits.chars() {
319 let Some(d) = ch.to_digit(radix) else {
320 return f64::NAN;
321 };
322 value = value * f64::from(radix) + f64::from(d);
323 }
324 value
325}
326
327fn is_decimal_literal_byte(b: u8) -> bool {
328 b.is_ascii_digit() || matches!(b, b'.' | b'e' | b'E' | b'+' | b'-')
329}
330
331fn strip_sign(s: &str) -> (f64, &str) {
332 if let Some(rest) = s.strip_prefix('+') {
333 (1.0, rest)
334 } else if let Some(rest) = s.strip_prefix('-') {
335 (-1.0, rest)
336 } else {
337 (1.0, s)
338 }
339}
340
341fn strip_hex_prefix(s: &str) -> Option<&str> {
344 s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))
345}
346
347fn float_prefix(s: &str) -> &str {
348 let bytes = s.as_bytes();
349 let mut i = 0usize;
350 let n = bytes.len();
351
352 let int_start = i;
353 while i < n && bytes[i].is_ascii_digit() {
354 i += 1;
355 }
356 let int_end = i;
357
358 let mut frac_end = int_end;
359 if i < n && bytes[i] == b'.' {
360 i += 1;
361 let frac_start = i;
362 while i < n && bytes[i].is_ascii_digit() {
363 i += 1;
364 }
365 if frac_start == i && int_end == int_start {
366 return "";
368 }
369 frac_end = i;
370 } else if int_end == int_start {
371 return "";
372 }
373
374 let exp_start = i;
376 if i < n && (bytes[i] == b'e' || bytes[i] == b'E') {
377 let mut j = i + 1;
378 if j < n && (bytes[j] == b'+' || bytes[j] == b'-') {
379 j += 1;
380 }
381 let digits_start = j;
382 while j < n && bytes[j].is_ascii_digit() {
383 j += 1;
384 }
385 if j > digits_start {
386 i = j;
387 } else {
388 i = exp_start;
389 }
390 }
391
392 let end = if i > frac_end { i } else { frac_end };
393 &s[..end]
394}
395
396pub(crate) fn is_js_whitespace(value: char) -> bool {
398 matches!(value, '\u{0009}'..='\u{000D}' | '\u{0020}' | '\u{00A0}' | '\u{1680}'
399 | '\u{2000}'..='\u{200A}' | '\u{2028}' | '\u{2029}' | '\u{202F}' | '\u{205F}'
400 | '\u{3000}' | '\u{FEFF}')
401}
402
403pub(crate) fn pow_js(base: f64, exponent: f64) -> f64 {
404 if exponent.is_nan() || (base.abs() == 1.0 && exponent.is_infinite()) {
405 return f64::NAN;
406 }
407 base.powf(exponent)
408}
409
410#[cfg(test)]
411mod tests {
412 use super::{parse_float_js, parse_int_js, string_to_number_js, to_uint8};
413
414 #[test]
415 fn to_uint8_wraps_modulo_256() {
416 assert_eq!(to_uint8(7.0), 7);
417 assert_eq!(to_uint8(256.0), 0);
418 assert_eq!(to_uint8(257.9), 1);
419 assert_eq!(to_uint8(-1.0), 255);
420 assert_eq!(to_uint8(-1.5), 255);
421 assert_eq!(to_uint8(-256.0), 0);
422 assert_eq!(to_uint8(-0.5), 0);
423 assert_eq!(to_uint8(f64::NAN), 0);
424 assert_eq!(to_uint8(f64::INFINITY), 0);
425 assert_eq!(to_uint8(f64::NEG_INFINITY), 0);
426 assert_eq!(to_uint8(2f64.powi(60) + 512.0), 0);
427 }
428
429 #[test]
430 fn formatter_invariants_hold_at_float_boundaries() {
431 let below_one = f64::from_bits(1.0_f64.to_bits() - 1);
432 for radix in 2..=36 {
433 for value in [below_one, 0.5, f64::MIN_POSITIVE, f64::from_bits(1)] {
434 let text = super::to_string_radix_js(value, f64::from(radix)).unwrap();
435 if radix != 10 {
436 assert!(text.chars().all(|c| c == '.' || c.is_digit(radix)));
437 }
438 }
439 assert!(super::to_string_radix_js(f64::MAX, f64::from(radix)).is_ok());
440 }
441 assert_eq!(
442 super::to_precision_js(f64::from_bits(1), 1.0).unwrap(),
443 "5e-324"
444 );
445 assert_eq!(super::to_precision_js(f64::MAX, 1.0).unwrap(), "2e+308");
446 assert_eq!(super::to_precision_js(-0.0, 3.0).unwrap(), "0.00");
447 }
448
449 #[test]
450 fn parse_int_decimal_prefix() {
451 assert_eq!(parse_int_js("42", 10), 42.0);
452 assert_eq!(parse_int_js("42px", 10), 42.0);
453 assert_eq!(parse_int_js(" 42 ", 10), 42.0);
454 }
455
456 #[test]
457 fn parse_int_negative_and_positive() {
458 assert_eq!(parse_int_js("-42", 10), -42.0);
459 assert_eq!(parse_int_js("+42", 10), 42.0);
460 }
461
462 #[test]
463 fn parse_int_hex_with_explicit_radix_16() {
464 assert_eq!(parse_int_js("0xff", 16), 255.0);
465 assert_eq!(parse_int_js("0XFF", 16), 255.0);
466 assert_eq!(parse_int_js("ff", 16), 255.0);
467 }
468
469 #[test]
470 fn parse_int_radix_zero_auto_detects_hex() {
471 assert_eq!(parse_int_js("0xff", 0), 255.0);
472 assert_eq!(parse_int_js("010", 0), 10.0); assert_eq!(parse_int_js("42", 0), 42.0);
474 }
475
476 #[test]
477 fn parse_int_no_digits_is_nan() {
478 assert!(parse_int_js("abc", 10).is_nan());
479 assert!(parse_int_js("", 10).is_nan());
480 assert!(parse_int_js(" ", 10).is_nan());
481 assert!(parse_int_js("0xZZ", 16).is_nan());
482 }
483
484 #[test]
485 fn parse_int_invalid_radix_is_nan() {
486 assert!(parse_int_js("42", 1).is_nan());
487 assert!(parse_int_js("42", 37).is_nan());
488 }
489
490 #[test]
491 fn parse_float_basic() {
492 assert_eq!(parse_float_js("1.5"), 1.5);
493 assert_eq!(parse_float_js("1.5x"), 1.5);
494 assert_eq!(parse_float_js(".5"), 0.5);
495 assert_eq!(parse_float_js("42"), 42.0);
496 }
497
498 #[test]
499 fn parse_float_negative_and_positive() {
500 assert_eq!(parse_float_js("-1.5"), -1.5);
501 assert_eq!(parse_float_js("+1.5"), 1.5);
502 }
503
504 #[test]
505 fn parse_float_exponent() {
506 assert_eq!(parse_float_js("1e3"), 1000.0);
507 assert_eq!(parse_float_js("1.5e2"), 150.0);
508 assert_eq!(parse_float_js("1.5e+2"), 150.0);
509 assert_eq!(parse_float_js("1.5e-2"), 0.015);
510 }
511
512 #[test]
513 fn parse_float_infinity() {
514 assert_eq!(parse_float_js("Infinity"), f64::INFINITY);
515 assert_eq!(parse_float_js("-Infinity"), f64::NEG_INFINITY);
516 }
517
518 #[test]
519 fn parse_float_no_digits_is_nan() {
520 assert!(parse_float_js("abc").is_nan());
521 assert!(parse_float_js(".").is_nan());
522 assert!(parse_float_js("").is_nan());
523 assert!(parse_float_js(" ").is_nan());
524 }
525
526 #[test]
527 fn parse_float_skips_whitespace() {
528 assert_eq!(parse_float_js(" 1.5 "), 1.5);
529 }
530
531 #[test]
532 fn parse_float_partial_exponent_keeps_mantissa() {
533 assert_eq!(parse_float_js("1.5e"), 1.5);
534 }
535
536 #[test]
537 fn number_decimal_and_scientific() {
538 assert_eq!(string_to_number_js("123"), 123.0);
539 assert_eq!(string_to_number_js("3.5"), 3.5);
540 assert_eq!(string_to_number_js("1e3"), 1000.0);
541 assert_eq!(string_to_number_js(".5"), 0.5);
542 assert_eq!(string_to_number_js("-42"), -42.0);
543 assert_eq!(string_to_number_js(" 7 "), 7.0);
544 }
545
546 #[test]
547 fn number_radix_prefixes() {
548 assert_eq!(string_to_number_js("0xff"), 255.0);
549 assert_eq!(string_to_number_js("0XFF"), 255.0);
550 assert_eq!(string_to_number_js("0o17"), 15.0);
551 assert_eq!(string_to_number_js("0O17"), 15.0);
552 assert_eq!(string_to_number_js("0b101"), 5.0);
553 assert_eq!(string_to_number_js("0B101"), 5.0);
554 assert_eq!(string_to_number_js(" 0xff "), 255.0);
555 }
556
557 #[test]
558 fn number_empty_is_zero() {
559 assert_eq!(string_to_number_js(""), 0.0);
560 assert_eq!(string_to_number_js(" "), 0.0);
561 }
562
563 #[test]
564 fn number_infinity() {
565 assert_eq!(string_to_number_js("Infinity"), f64::INFINITY);
566 assert_eq!(string_to_number_js("+Infinity"), f64::INFINITY);
567 assert_eq!(string_to_number_js("-Infinity"), f64::NEG_INFINITY);
568 }
569
570 #[test]
571 fn number_rejects_trailing_junk_and_bad_radix() {
572 assert!(string_to_number_js("12px").is_nan());
573 assert!(string_to_number_js("0xZZ").is_nan());
574 assert!(string_to_number_js("0x").is_nan());
575 assert!(string_to_number_js("0b12").is_nan());
576 assert!(string_to_number_js("0xff.5").is_nan());
577 }
578
579 #[test]
580 fn number_rejects_signed_radix_literals() {
581 assert!(string_to_number_js("-0xff").is_nan());
583 assert!(string_to_number_js("+0b101").is_nan());
584 }
585
586 #[test]
587 fn number_rejects_rust_only_float_spellings() {
588 assert!(string_to_number_js("inf").is_nan());
590 assert!(string_to_number_js("infinity").is_nan());
591 assert!(string_to_number_js("nan").is_nan());
592 assert!(string_to_number_js("NaN").is_nan());
593 }
594}