Skip to main content

swc_ecma_utils/
number.rs

1/// <https://tc39.es/ecma262/#sec-numeric-types-number-tostring>
2pub trait ToJsString {
3    fn to_js_string(&self) -> String;
4}
5
6impl ToJsString for f64 {
7    fn to_js_string(&self) -> String {
8        let mut buffer = dragonbox_ecma::Buffer::new();
9        buffer.format(*self).to_string()
10    }
11}
12
13/// Parses the canonical non-negative decimal representation of a `usize`.
14///
15/// This does not apply numeric coercion, so distinct ECMAScript property keys
16/// such as `"01"`, `"1.0"`, and `"+1"` are not reinterpreted as the index `1`.
17pub fn parse_canonical_index(property_key: &str) -> Option<usize> {
18    let bytes = property_key.as_bytes();
19    let is_canonical = match bytes {
20        [b'0'] => true,
21        [b'1'..=b'9', rest @ ..] => rest.iter().all(u8::is_ascii_digit),
22        _ => false,
23    };
24
25    if !is_canonical {
26        return None;
27    }
28
29    property_key.parse().ok()
30}
31
32#[derive(Debug, Clone, Copy, PartialEq)]
33pub struct JsNumber(f64);
34
35impl From<f64> for JsNumber {
36    fn from(v: f64) -> Self {
37        JsNumber(v)
38    }
39}
40
41impl From<JsNumber> for f64 {
42    fn from(v: JsNumber) -> Self {
43        v.0
44    }
45}
46
47impl std::ops::Deref for JsNumber {
48    type Target = f64;
49
50    fn deref(&self) -> &Self::Target {
51        &self.0
52    }
53}
54
55impl JsNumber {
56    /// Applies ECMAScript `ToUint16` conversion.
57    ///
58    /// <https://tc39.es/ecma262/#sec-touint16>
59    pub fn to_uint16(self) -> u16 {
60        self.as_uint32() as u16
61    }
62
63    // https://tc39.es/ecma262/#sec-toint32
64    fn as_int32(&self) -> i32 {
65        self.as_uint32() as i32
66    }
67
68    // https://tc39.es/ecma262/#sec-touint32
69    fn as_uint32(&self) -> u32 {
70        if !self.0.is_finite() {
71            return 0;
72        }
73
74        // pow(2, 32) = 4294967296
75        self.0.trunc().rem_euclid(4294967296.0) as u32
76    }
77}
78
79// JsNumber + JsNumber
80impl std::ops::Add<JsNumber> for JsNumber {
81    type Output = JsNumber;
82
83    fn add(self, rhs: JsNumber) -> Self::Output {
84        JsNumber(self.0 + rhs.0)
85    }
86}
87
88// JsNumber - JsNumber
89impl std::ops::Sub<JsNumber> for JsNumber {
90    type Output = JsNumber;
91
92    fn sub(self, rhs: JsNumber) -> Self::Output {
93        JsNumber(self.0 - rhs.0)
94    }
95}
96
97// JsNumber * JsNumber
98impl std::ops::Mul<JsNumber> for JsNumber {
99    type Output = JsNumber;
100
101    fn mul(self, rhs: JsNumber) -> Self::Output {
102        JsNumber(self.0 * rhs.0)
103    }
104}
105
106// JsNumber / JsNumber
107impl std::ops::Div<JsNumber> for JsNumber {
108    type Output = JsNumber;
109
110    fn div(self, rhs: JsNumber) -> Self::Output {
111        JsNumber(self.0 / rhs.0)
112    }
113}
114
115// JsNumber % JsNumber
116impl std::ops::Rem<JsNumber> for JsNumber {
117    type Output = JsNumber;
118
119    fn rem(self, rhs: JsNumber) -> Self::Output {
120        JsNumber(self.0 % rhs.0)
121    }
122}
123
124// JsNumber ** JsNumber
125impl JsNumber {
126    pub fn pow(self, rhs: JsNumber) -> JsNumber {
127        // https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-exponentiate
128        // https://github.com/rust-lang/rust/issues/60468
129        if rhs.0.is_nan() {
130            return JsNumber(f64::NAN);
131        }
132
133        if self.0.abs() == 1f64 && rhs.0.is_infinite() {
134            return JsNumber(f64::NAN);
135        }
136
137        JsNumber(self.0.powf(rhs.0))
138    }
139}
140
141// JsNumber << JsNumber
142// https://tc39.es/ecma262/#sec-numeric-types-number-leftShift
143impl std::ops::Shl<JsNumber> for JsNumber {
144    type Output = JsNumber;
145
146    fn shl(self, rhs: JsNumber) -> Self::Output {
147        JsNumber(self.as_int32().wrapping_shl(rhs.as_uint32()) as f64)
148    }
149}
150
151// JsNumber >> JsNumber
152// https://tc39.es/ecma262/#sec-numeric-types-number-signedRightShift
153impl std::ops::Shr<JsNumber> for JsNumber {
154    type Output = JsNumber;
155
156    fn shr(self, rhs: JsNumber) -> Self::Output {
157        JsNumber((self.as_int32()).wrapping_shr(rhs.as_uint32()) as f64)
158    }
159}
160
161// JsNumber >>> JsNumber
162// https://tc39.es/ecma262/#sec-numeric-types-number-unsignedRightShift
163impl JsNumber {
164    pub fn unsigned_shr(self, rhs: JsNumber) -> JsNumber {
165        JsNumber((self.as_uint32()).wrapping_shr(rhs.as_uint32()) as f64)
166    }
167}
168
169// JsNumber | JsNumber
170// https://tc39.es/ecma262/#sec-numberbitwiseop
171impl std::ops::BitOr<JsNumber> for JsNumber {
172    type Output = JsNumber;
173
174    fn bitor(self, rhs: JsNumber) -> Self::Output {
175        JsNumber((self.as_int32() | rhs.as_int32()) as f64)
176    }
177}
178
179// JsNumber & JsNumber
180// https://tc39.es/ecma262/#sec-numberbitwiseop
181impl std::ops::BitAnd<JsNumber> for JsNumber {
182    type Output = JsNumber;
183
184    fn bitand(self, rhs: JsNumber) -> Self::Output {
185        JsNumber((self.as_int32() & rhs.as_int32()) as f64)
186    }
187}
188
189// JsNumber ^ JsNumber
190// https://tc39.es/ecma262/#sec-numberbitwiseop
191impl std::ops::BitXor<JsNumber> for JsNumber {
192    type Output = JsNumber;
193
194    fn bitxor(self, rhs: JsNumber) -> Self::Output {
195        JsNumber((self.as_int32() ^ rhs.as_int32()) as f64)
196    }
197}
198
199// - JsNumber
200impl std::ops::Neg for JsNumber {
201    type Output = JsNumber;
202
203    fn neg(self) -> Self::Output {
204        JsNumber(-self.0)
205    }
206}
207
208// ~ JsNumber
209impl std::ops::Not for JsNumber {
210    type Output = JsNumber;
211
212    fn not(self) -> Self::Output {
213        JsNumber(!(self.as_int32()) as f64)
214    }
215}
216
217fn clz(s: &str) -> usize {
218    s.as_bytes().iter().take_while(|&&c| c == b'0').count()
219}
220
221pub fn minify_number(num: f64, detect_dot: &mut bool) -> String {
222    // ddddd -> 0xhhhh
223    // len(0xhhhh) == len(ddddd)
224    // 10000000 <= num <= 0xffffff
225    'hex: {
226        if num.fract() == 0.0 && num.abs() <= u64::MAX as f64 {
227            let int = num.abs() as u64;
228
229            if int < 10000000 {
230                break 'hex;
231            }
232
233            // use scientific notation
234            if int % 1000 == 0 {
235                break 'hex;
236            }
237
238            *detect_dot = false;
239            return format!(
240                "{}{:#x}",
241                if num.is_sign_negative() { "-" } else { "" },
242                int
243            );
244        }
245    }
246
247    let mut num = num.to_string();
248
249    if num.contains(".") {
250        *detect_dot = false;
251    }
252
253    if let Some(num) = num.strip_prefix("0.") {
254        let cnt = clz(num);
255        if cnt > 2 {
256            return format!("{}e-{}", &num[cnt..], num.len());
257        }
258        return format!(".{num}");
259    }
260
261    if let Some(num) = num.strip_prefix("-0.") {
262        let cnt = clz(num);
263        if cnt > 2 {
264            return format!("-{}e-{}", &num[cnt..], num.len());
265        }
266        return format!("-.{num}");
267    }
268
269    if num.ends_with("000") {
270        *detect_dot = false;
271
272        let cnt = num
273            .as_bytes()
274            .iter()
275            .rev()
276            .skip(3)
277            .take_while(|&&c| c == b'0')
278            .count()
279            + 3;
280
281        num.truncate(num.len() - cnt);
282        num.push('e');
283        num.push_str(&cnt.to_string());
284    }
285
286    num
287}
288
289#[cfg(test)]
290mod test_js_number {
291    use super::*;
292
293    #[test]
294    fn test_parse_canonical_index() {
295        assert_eq!(parse_canonical_index("0"), Some(0));
296        assert_eq!(parse_canonical_index("42"), Some(42));
297        assert_eq!(parse_canonical_index(""), None);
298        assert_eq!(parse_canonical_index("01"), None);
299        assert_eq!(parse_canonical_index("1.0"), None);
300        assert_eq!(parse_canonical_index("+1"), None);
301        assert_eq!(parse_canonical_index("-0"), None);
302    }
303
304    #[test]
305    fn test_as_int32() {
306        assert_eq!(JsNumber(f64::NAN).as_int32(), 0);
307        assert_eq!(JsNumber(0.0).as_int32(), 0);
308        assert_eq!(JsNumber(-0.0).as_int32(), 0);
309        assert_eq!(JsNumber(f64::INFINITY).as_int32(), 0);
310        assert_eq!(JsNumber(f64::NEG_INFINITY).as_int32(), 0);
311    }
312
313    #[test]
314    fn test_as_uint32() {
315        assert_eq!(JsNumber(f64::NAN).as_uint32(), 0);
316        assert_eq!(JsNumber(0.0).as_uint32(), 0);
317        assert_eq!(JsNumber(-0.0).as_uint32(), 0);
318        assert_eq!(JsNumber(f64::INFINITY).as_uint32(), 0);
319        assert_eq!(JsNumber(f64::NEG_INFINITY).as_uint32(), 0);
320        assert_eq!(JsNumber(-8.0).as_uint32(), 4294967288);
321    }
322
323    #[test]
324    fn test_to_uint16() {
325        assert_eq!(JsNumber(65.9).to_uint16(), 65);
326        assert_eq!(JsNumber(-1.0).to_uint16(), 65535);
327        assert_eq!(JsNumber(-65535.9).to_uint16(), 1);
328        assert_eq!(JsNumber(65536.0).to_uint16(), 0);
329        assert_eq!(JsNumber(65537.0).to_uint16(), 1);
330        assert_eq!(JsNumber(f64::NAN).to_uint16(), 0);
331        assert_eq!(JsNumber(f64::INFINITY).to_uint16(), 0);
332        assert_eq!(JsNumber(f64::NEG_INFINITY).to_uint16(), 0);
333        assert_eq!(JsNumber(4294967361.0).to_uint16(), 65);
334        assert_eq!(JsNumber(-4294967231.0).to_uint16(), 65);
335    }
336
337    #[test]
338    fn test_add() {
339        assert_eq!(JsNumber(1.0) + JsNumber(2.0), JsNumber(3.0));
340
341        assert!((JsNumber(1.0) + JsNumber(f64::NAN)).is_nan());
342        assert!((JsNumber(f64::NAN) + JsNumber(1.0)).is_nan());
343        assert!((JsNumber(f64::NAN) + JsNumber(f64::NAN)).is_nan());
344        assert!((JsNumber(f64::INFINITY) + JsNumber(f64::NEG_INFINITY)).is_nan());
345        assert!((JsNumber(f64::NEG_INFINITY) + JsNumber(f64::INFINITY)).is_nan());
346
347        assert_eq!(
348            JsNumber(f64::INFINITY) + JsNumber(1.0),
349            JsNumber(f64::INFINITY)
350        );
351        assert_eq!(
352            JsNumber(f64::NEG_INFINITY) + JsNumber(1.0),
353            JsNumber(f64::NEG_INFINITY)
354        );
355        assert_eq!(JsNumber(-0.0) + JsNumber(0.0), JsNumber(-0.0));
356    }
357}