Skip to main content

chrono_node/core/utils/
js.rs

1//! JavaScript string and number built-ins that upstream code relies on.
2//!
3//! Use these instead of Rust equivalents when porting: they differ in edge cases
4//! (`parseInt("12px")` is 12; `" x\u{FEFF}".trim()` removes the BOM; `substring` counts UTF-16 units).
5
6/// JS `WhiteSpace` + `LineTerminator`, as used by `String.prototype.trim`.
7pub fn is_js_whitespace(c: char) -> bool {
8    matches!(
9        c,
10        '\t' | '\n' | '\u{B}' | '\u{C}' | '\r' | ' ' | '\u{A0}' | '\u{1680}' | '\u{2000}'
11            ..='\u{200A}' | '\u{2028}' | '\u{2029}' | '\u{202F}' | '\u{205F}' | '\u{3000}' | '\u{FEFF}'
12    )
13}
14
15/// `s.trim()`.
16pub fn js_trim(s: &str) -> &str {
17    s.trim_matches(is_js_whitespace)
18}
19
20/// `parseInt(s)` (radix 10). `None` is `NaN`. Saturates instead of losing precision past `i64`.
21pub fn js_parse_int(s: &str) -> Option<i64> {
22    let s = s.trim_start_matches(is_js_whitespace);
23    let (negative, digits) = match s.as_bytes().first() {
24        Some(b'-') => (true, &s[1..]),
25        Some(b'+') => (false, &s[1..]),
26        _ => (false, s),
27    };
28    let end = digits.bytes().position(|b| !b.is_ascii_digit()).unwrap_or(digits.len());
29    if end == 0 {
30        return None;
31    }
32    let mut value: i64 = 0;
33    for b in digits[..end].bytes() {
34        value = value.saturating_mul(10).saturating_add((b - b'0') as i64);
35    }
36    Some(if negative { -value } else { value })
37}
38
39/// `parseFloat(s)`. Returns `NaN` if no number prefix.
40pub fn js_parse_float(s: &str) -> f64 {
41    let s = s.trim_start_matches(is_js_whitespace);
42    let bytes = s.as_bytes();
43    let mut i = 0;
44    if matches!(bytes.first(), Some(b'+') | Some(b'-')) {
45        i = 1;
46    }
47    if s[i..].starts_with("Infinity") {
48        return if bytes[0] == b'-' {
49            f64::NEG_INFINITY
50        } else {
51            f64::INFINITY
52        };
53    }
54    let int_start = i;
55    while i < bytes.len() && bytes[i].is_ascii_digit() {
56        i += 1;
57    }
58    let mut digits = i - int_start;
59    if i < bytes.len() && bytes[i] == b'.' {
60        let frac_start = i + 1;
61        let mut j = frac_start;
62        while j < bytes.len() && bytes[j].is_ascii_digit() {
63            j += 1;
64        }
65        digits += j - frac_start;
66        if digits > 0 {
67            i = j;
68        }
69    }
70    if digits == 0 {
71        return f64::NAN;
72    }
73    if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
74        let mut j = i + 1;
75        if j < bytes.len() && (bytes[j] == b'+' || bytes[j] == b'-') {
76            j += 1;
77        }
78        let exp_start = j;
79        while j < bytes.len() && bytes[j].is_ascii_digit() {
80            j += 1;
81        }
82        if j > exp_start {
83            i = j;
84        }
85    }
86    s[..i].parse().unwrap_or(f64::NAN)
87}
88
89/// Byte offset of the `units`-th UTF-16 code unit in `s` (clamped to the end, and moved forward
90/// to a char boundary if it falls inside a surrogate pair).
91pub fn utf16_to_byte_offset(s: &str, units: usize) -> usize {
92    let mut seen = 0;
93    for (byte, c) in s.char_indices() {
94        if seen >= units {
95            return byte;
96        }
97        seen += c.len_utf16();
98    }
99    s.len()
100}
101
102/// `s.substring(start)` where `start` counts UTF-16 units.
103pub fn js_substring_from(s: &str, start_utf16: usize) -> &str {
104    &s[utf16_to_byte_offset(s, start_utf16)..]
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn parse_int_like_js() {
113        assert_eq!(js_parse_int("12px"), Some(12));
114        assert_eq!(js_parse_int("  -7"), Some(-7));
115        assert_eq!(js_parse_int("x1"), None);
116        assert_eq!(js_parse_int("007"), Some(7));
117    }
118
119    #[test]
120    fn parse_float_like_js() {
121        assert_eq!(js_parse_float("2.5 days"), 2.5);
122        assert_eq!(js_parse_float(".5"), 0.5);
123        assert_eq!(js_parse_float("3."), 3.0);
124        assert_eq!(js_parse_float("1e3x"), 1000.0);
125        assert_eq!(js_parse_float("1ex"), 1.0);
126        assert!(js_parse_float("few").is_nan());
127        assert!(js_parse_float(".").is_nan());
128    }
129
130    #[test]
131    fn trim_and_substring() {
132        assert_eq!(js_trim("\u{FEFF} a \u{3000}"), "a");
133        assert_eq!(js_substring_from("😀ab", 2), "ab");
134        assert_eq!(js_substring_from("ab", 5), "");
135    }
136}