boa/builtins/number/
mod.rs

1//! This module implements the global `Number` object.
2//!
3//! The `Number` JavaScript object is a wrapper object allowing you to work with numerical values.
4//! A `Number` object is created using the `Number()` constructor. A primitive type object number is created using the `Number()` **function**.
5//!
6//! The JavaScript `Number` type is double-precision 64-bit binary format IEEE 754 value. In more recent implementations,
7//! JavaScript also supports integers with arbitrary precision using the BigInt type.
8//!
9//! More information:
10//!  - [ECMAScript reference][spec]
11//!  - [MDN documentation][mdn]
12//!
13//! [spec]: https://tc39.es/ecma262/#sec-number-object
14//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number
15
16use super::string::is_trimmable_whitespace;
17use super::{function::make_builtin_fn, JsArgs};
18use crate::context::StandardObjects;
19use crate::{
20    builtins::BuiltIn,
21    object::{internal_methods::get_prototype_from_constructor, ConstructorBuilder, ObjectData},
22    property::Attribute,
23    value::{AbstractRelation, IntegerOrInfinity, JsValue},
24    BoaProfiler, Context, JsResult,
25};
26use num_traits::{float::FloatCore, Num};
27
28mod conversions;
29
30pub(crate) use conversions::{f64_to_int32, f64_to_uint32};
31
32#[cfg(test)]
33mod tests;
34
35const BUF_SIZE: usize = 2200;
36
37/// `Number` implementation.
38#[derive(Debug, Clone, Copy)]
39pub(crate) struct Number;
40
41/// Maximum number of arguments expected to the builtin parseInt() function.
42const PARSE_INT_MAX_ARG_COUNT: usize = 2;
43
44/// Maximum number of arguments expected to the builtin parseFloat() function.
45const PARSE_FLOAT_MAX_ARG_COUNT: usize = 1;
46
47impl BuiltIn for Number {
48    const NAME: &'static str = "Number";
49
50    fn attribute() -> Attribute {
51        Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE
52    }
53
54    fn init(context: &mut Context) -> (&'static str, JsValue, Attribute) {
55        let _timer = BoaProfiler::global().start_event(Self::NAME, "init");
56
57        let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT;
58        let number_object = ConstructorBuilder::with_standard_object(
59            context,
60            Self::constructor,
61            context.standard_objects().number_object().clone(),
62        )
63        .name(Self::NAME)
64        .length(Self::LENGTH)
65        .static_property("EPSILON", f64::EPSILON, attribute)
66        .static_property("MAX_SAFE_INTEGER", Self::MAX_SAFE_INTEGER, attribute)
67        .static_property("MIN_SAFE_INTEGER", Self::MIN_SAFE_INTEGER, attribute)
68        .static_property("MAX_VALUE", Self::MAX_VALUE, attribute)
69        .static_property("MIN_VALUE", Self::MIN_VALUE, attribute)
70        .static_property("NEGATIVE_INFINITY", f64::NEG_INFINITY, attribute)
71        .static_property("POSITIVE_INFINITY", f64::INFINITY, attribute)
72        .static_property("NaN", f64::NAN, attribute)
73        .method(Self::to_exponential, "toExponential", 1)
74        .method(Self::to_fixed, "toFixed", 1)
75        .method(Self::to_locale_string, "toLocaleString", 0)
76        .method(Self::to_precision, "toPrecision", 1)
77        .method(Self::to_string, "toString", 1)
78        .method(Self::value_of, "valueOf", 0)
79        .static_method(Self::number_is_finite, "isFinite", 1)
80        .static_method(Self::number_is_nan, "isNaN", 1)
81        .static_method(Self::is_safe_integer, "isSafeInteger", 1)
82        .static_method(Self::number_is_integer, "isInteger", 1)
83        .build();
84
85        let global = context.global_object();
86        make_builtin_fn(
87            Self::parse_int,
88            "parseInt",
89            &global,
90            PARSE_INT_MAX_ARG_COUNT,
91            context,
92        );
93        make_builtin_fn(
94            Self::parse_float,
95            "parseFloat",
96            &global,
97            PARSE_FLOAT_MAX_ARG_COUNT,
98            context,
99        );
100        make_builtin_fn(Self::global_is_finite, "isFinite", &global, 1, context);
101        make_builtin_fn(Self::global_is_nan, "isNaN", &global, 1, context);
102
103        (Self::NAME, number_object.into(), Self::attribute())
104    }
105}
106
107impl Number {
108    /// The amount of arguments this function object takes.
109    pub(crate) const LENGTH: usize = 1;
110
111    /// The `Number.MAX_SAFE_INTEGER` constant represents the maximum safe integer in JavaScript (`2^53 - 1`).
112    ///
113    /// /// More information:
114    ///  - [ECMAScript reference][spec]
115    ///  - [MDN documentation][mdn]
116    ///
117    /// [spec]: https://tc39.es/ecma262/#sec-number.max_safe_integer
118    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER
119    pub(crate) const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991_f64;
120
121    /// The `Number.MIN_SAFE_INTEGER` constant represents the minimum safe integer in JavaScript (`-(253 - 1)`).
122    ///
123    /// More information:
124    ///  - [ECMAScript reference][spec]
125    ///  - [MDN documentation][mdn]
126    ///
127    /// [spec]: https://tc39.es/ecma262/#sec-number.min_safe_integer
128    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER
129    pub(crate) const MIN_SAFE_INTEGER: f64 = -9_007_199_254_740_991_f64;
130
131    /// The `Number.MAX_VALUE` property represents the maximum numeric value representable in JavaScript.
132    ///
133    /// The `MAX_VALUE` property has a value of approximately `1.79E+308`, or `2^1024`.
134    /// Values larger than `MAX_VALUE` are represented as `Infinity`.
135    ///
136    /// More information:
137    ///  - [ECMAScript reference][spec]
138    ///  - [MDN documentation][mdn]
139    ///
140    /// [spec]: https://tc39.es/ecma262/#sec-number.max_value
141    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_VALUE
142    pub(crate) const MAX_VALUE: f64 = f64::MAX;
143
144    /// The `Number.MIN_VALUE` property represents the smallest positive numeric value representable in JavaScript.
145    ///
146    /// The `MIN_VALUE` property is the number closest to `0`, not the most negative number, that JavaScript can represent.
147    /// It has a value of approximately `5e-324`. Values smaller than `MIN_VALUE` ("underflow values") are converted to `0`.
148    ///
149    /// More information:
150    ///  - [ECMAScript reference][spec]
151    ///  - [MDN documentation][mdn]
152    ///
153    /// [spec]: https://tc39.es/ecma262/#sec-number.min_value
154    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_VALUE
155    pub(crate) const MIN_VALUE: f64 = f64::MIN_POSITIVE;
156
157    /// `Number( value )`
158    pub(crate) fn constructor(
159        new_target: &JsValue,
160        args: &[JsValue],
161        context: &mut Context,
162    ) -> JsResult<JsValue> {
163        let data = match args.get(0) {
164            Some(value) => value.to_numeric_number(context)?,
165            None => 0.0,
166        };
167        if new_target.is_undefined() {
168            return Ok(JsValue::new(data));
169        }
170        let prototype =
171            get_prototype_from_constructor(new_target, StandardObjects::number_object, context)?;
172        let this = JsValue::new_object(context);
173        this.as_object()
174            .expect("this should be an object")
175            .set_prototype_instance(prototype.into());
176        this.set_data(ObjectData::number(data));
177
178        Ok(this)
179    }
180
181    /// This function returns a `JsResult` of the number `Value`.
182    ///
183    /// If the `Value` is a `Number` primitive of `Number` object the number is returned.
184    /// Otherwise an `TypeError` is thrown.
185    ///
186    /// More information:
187    ///  - [ECMAScript reference][spec]
188    ///
189    /// [spec]: https://tc39.es/ecma262/#sec-thisnumbervalue
190    fn this_number_value(value: &JsValue, context: &mut Context) -> JsResult<f64> {
191        match *value {
192            JsValue::Integer(integer) => return Ok(f64::from(integer)),
193            JsValue::Rational(rational) => return Ok(rational),
194            JsValue::Object(ref object) => {
195                if let Some(number) = object.borrow().as_number() {
196                    return Ok(number);
197                }
198            }
199            _ => {}
200        }
201
202        Err(context.construct_type_error("'this' is not a number"))
203    }
204
205    /// Helper function that formats a float as a ES6-style exponential number string.
206    fn num_to_exponential(n: f64) -> String {
207        match n.abs() {
208            x if x > 1.0 => format!("{:e}", n).replace("e", "e+"),
209            x if x == 0.0 => format!("{:e}", n).replace("e", "e+"),
210            _ => format!("{:e}", n),
211        }
212    }
213
214    /// `Number.prototype.toExponential( [fractionDigits] )`
215    ///
216    /// The `toExponential()` method returns a string representing the Number object in exponential notation.
217    ///
218    /// More information:
219    ///  - [ECMAScript reference][spec]
220    ///  - [MDN documentation][mdn]
221    ///
222    /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.toexponential
223    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toExponential
224    #[allow(clippy::wrong_self_convention)]
225    pub(crate) fn to_exponential(
226        this: &JsValue,
227        _: &[JsValue],
228        context: &mut Context,
229    ) -> JsResult<JsValue> {
230        let this_num = Self::this_number_value(this, context)?;
231        let this_str_num = Self::num_to_exponential(this_num);
232        Ok(JsValue::new(this_str_num))
233    }
234
235    /// `Number.prototype.toFixed( [digits] )`
236    ///
237    /// The `toFixed()` method formats a number using fixed-point notation
238    ///
239    /// More information:
240    ///  - [ECMAScript reference][spec]
241    ///  - [MDN documentation][mdn]
242    ///
243    /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tofixed
244    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed
245    #[allow(clippy::wrong_self_convention)]
246    pub(crate) fn to_fixed(
247        this: &JsValue,
248        args: &[JsValue],
249        context: &mut Context,
250    ) -> JsResult<JsValue> {
251        let this_num = Self::this_number_value(this, context)?;
252        let precision = match args.get(0) {
253            Some(n) => match n.to_integer(context)? as i32 {
254                x if x > 0 => n.to_integer(context)? as usize,
255                _ => 0,
256            },
257            None => 0,
258        };
259        let this_fixed_num = format!("{:.*}", precision, this_num);
260        Ok(JsValue::new(this_fixed_num))
261    }
262
263    /// `Number.prototype.toLocaleString( [locales [, options]] )`
264    ///
265    /// The `toLocaleString()` method returns a string with a language-sensitive representation of this number.
266    ///
267    /// Note that while this technically conforms to the Ecma standard, it does no actual
268    /// internationalization logic.
269    ///
270    /// More information:
271    ///  - [ECMAScript reference][spec]
272    ///  - [MDN documentation][mdn]
273    ///
274    /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tolocalestring
275    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString
276    #[allow(clippy::wrong_self_convention)]
277    pub(crate) fn to_locale_string(
278        this: &JsValue,
279        _: &[JsValue],
280        context: &mut Context,
281    ) -> JsResult<JsValue> {
282        let this_num = Self::this_number_value(this, context)?;
283        let this_str_num = format!("{}", this_num);
284        Ok(JsValue::new(this_str_num))
285    }
286
287    /// flt_str_to_exp - used in to_precision
288    ///
289    /// This function traverses a string representing a number,
290    /// returning the floored log10 of this number.
291    ///
292    fn flt_str_to_exp(flt: &str) -> i32 {
293        let mut non_zero_encountered = false;
294        let mut dot_encountered = false;
295        for (i, c) in flt.chars().enumerate() {
296            if c == '.' {
297                if non_zero_encountered {
298                    return (i as i32) - 1;
299                }
300                dot_encountered = true;
301            } else if c != '0' {
302                if dot_encountered {
303                    return 1 - (i as i32);
304                }
305                non_zero_encountered = true;
306            }
307        }
308        (flt.len() as i32) - 1
309    }
310
311    /// round_to_precision - used in to_precision
312    ///
313    /// This procedure has two roles:
314    /// - If there are enough or more than enough digits in the
315    ///   string to show the required precision, the number
316    ///   represented by these digits is rounded using string
317    ///   manipulation.
318    /// - Else, zeroes are appended to the string.
319    /// - Additionnally, sometimes the exponent was wrongly computed and
320    ///   while up-rounding we find that we need an extra digit. When this
321    ///   happens, we return true so that the calling context can adjust
322    ///   the exponent. The string is kept at an exact length of `precision`.
323    ///
324    /// When this procedure returns, `digits` is exactly `precision` long.
325    ///
326    fn round_to_precision(digits: &mut String, precision: usize) -> bool {
327        if digits.len() > precision {
328            let to_round = digits.split_off(precision);
329            let mut digit = digits.pop().unwrap() as u8;
330            if let Some(first) = to_round.chars().next() {
331                if first > '4' {
332                    digit += 1;
333                }
334            }
335
336            if digit as char == ':' {
337                // ':' is '9' + 1
338                // need to propagate the increment backward
339                let mut replacement = String::from("0");
340                let mut propagated = false;
341                for c in digits.chars().rev() {
342                    let d = match (c, propagated) {
343                        ('0'..='8', false) => (c as u8 + 1) as char,
344                        (_, false) => '0',
345                        (_, true) => c,
346                    };
347                    replacement.push(d);
348                    if d != '0' {
349                        propagated = true;
350                    }
351                }
352                digits.clear();
353                let replacement = if !propagated {
354                    digits.push('1');
355                    &replacement.as_str()[1..]
356                } else {
357                    replacement.as_str()
358                };
359                for c in replacement.chars().rev() {
360                    digits.push(c)
361                }
362                !propagated
363            } else {
364                digits.push(digit as char);
365                false
366            }
367        } else {
368            digits.push_str(&"0".repeat(precision - digits.len()));
369            false
370        }
371    }
372
373    /// `Number.prototype.toPrecision( [precision] )`
374    ///
375    /// The `toPrecision()` method returns a string representing the Number object to the specified precision.
376    ///
377    /// More information:
378    ///  - [ECMAScript reference][spec]
379    ///  - [MDN documentation][mdn]
380    ///
381    /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.toprecision
382    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision
383    #[allow(clippy::wrong_self_convention)]
384    pub(crate) fn to_precision(
385        this: &JsValue,
386        args: &[JsValue],
387        context: &mut Context,
388    ) -> JsResult<JsValue> {
389        let precision = args.get_or_undefined(0);
390
391        // 1 & 6
392        let mut this_num = Self::this_number_value(this, context)?;
393        // 2
394        if precision.is_undefined() {
395            return Self::to_string(this, &[], context);
396        }
397
398        // 3
399        let precision = precision.to_integer_or_infinity(context)?;
400
401        // 4
402        if !this_num.is_finite() {
403            return Self::to_string(this, &[], context);
404        }
405
406        let precision = match precision {
407            IntegerOrInfinity::Integer(x) if (1..=100).contains(&x) => x as usize,
408            _ => {
409                // 5
410                return context.throw_range_error(
411                    "precision must be an integer at least 1 and no greater than 100",
412                );
413            }
414        };
415        let precision_i32 = precision as i32;
416
417        // 7
418        let mut prefix = String::new(); // spec: 's'
419        let mut suffix: String; // spec: 'm'
420        let mut exponent: i32; // spec: 'e'
421
422        // 8
423        if this_num < 0.0 {
424            prefix.push('-');
425            this_num = -this_num;
426        }
427
428        // 9
429        if this_num == 0.0 {
430            suffix = "0".repeat(precision);
431            exponent = 0;
432        // 10
433        } else {
434            // Due to f64 limitations, this part differs a bit from the spec,
435            // but has the same effect. It manipulates the string constructed
436            // by `format`: digits with an optional dot between two of them.
437            suffix = format!("{:.100}", this_num);
438
439            // a: getting an exponent
440            exponent = Self::flt_str_to_exp(&suffix);
441            // b: getting relevant digits only
442            if exponent < 0 {
443                suffix = suffix.split_off((1 - exponent) as usize);
444            } else if let Some(n) = suffix.find('.') {
445                suffix.remove(n);
446            }
447            // impl: having exactly `precision` digits in `suffix`
448            if Self::round_to_precision(&mut suffix, precision) {
449                exponent += 1;
450            }
451
452            // c: switching to scientific notation
453            let great_exp = exponent >= precision_i32;
454            if exponent < -6 || great_exp {
455                // ii
456                if precision > 1 {
457                    suffix.insert(1, '.');
458                }
459                // vi
460                suffix.push('e');
461                // iii
462                if great_exp {
463                    suffix.push('+');
464                }
465                // iv, v
466                suffix.push_str(&exponent.to_string());
467
468                return Ok(JsValue::new(prefix + &suffix));
469            }
470        }
471
472        // 11
473        let e_inc = exponent + 1;
474        if e_inc == precision_i32 {
475            return Ok(JsValue::new(prefix + &suffix));
476        }
477
478        // 12
479        if exponent >= 0 {
480            suffix.insert(e_inc as usize, '.');
481        // 13
482        } else {
483            prefix.push('0');
484            prefix.push('.');
485            prefix.push_str(&"0".repeat(-e_inc as usize));
486        }
487
488        // 14
489        Ok(JsValue::new(prefix + &suffix))
490    }
491
492    // https://golang.org/src/math/nextafter.go
493    #[inline]
494    fn next_after(x: f64, y: f64) -> f64 {
495        if x.is_nan() || y.is_nan() {
496            f64::NAN
497        } else if (x - y) == 0. {
498            x
499        } else if x == 0.0 {
500            f64::from_bits(1).copysign(y)
501        } else if y > x || x > 0.0 {
502            f64::from_bits(x.to_bits() + 1)
503        } else {
504            f64::from_bits(x.to_bits() - 1)
505        }
506    }
507
508    // https://chromium.googlesource.com/v8/v8/+/refs/heads/master/src/numbers/conversions.cc#1230
509    #[allow(clippy::wrong_self_convention)]
510    pub(crate) fn to_native_string_radix(mut value: f64, radix: u8) -> String {
511        assert!(radix >= 2);
512        assert!(radix <= 36);
513        assert!(value.is_finite());
514        // assert_ne!(0.0, value);
515
516        // Character array used for conversion.
517        // Temporary buffer for the result. We start with the decimal point in the
518        // middle and write to the left for the integer part and to the right for the
519        // fractional part. 1024 characters for the exponent and 52 for the mantissa
520        // either way, with additional space for sign, decimal point and string
521        // termination should be sufficient.
522        let mut buffer: [u8; BUF_SIZE] = [0; BUF_SIZE];
523        let (int_buf, frac_buf) = buffer.split_at_mut(BUF_SIZE / 2);
524        let mut fraction_cursor = 0;
525        let negative = value.is_sign_negative();
526        if negative {
527            value = -value
528        }
529        // Split the value into an integer part and a fractional part.
530        // let mut integer = value.trunc();
531        // let mut fraction = value.fract();
532        let mut integer = value.floor();
533        let mut fraction = value - integer;
534
535        // We only compute fractional digits up to the input double's precision.
536        let mut delta = 0.5 * (Self::next_after(value, f64::MAX) - value);
537        delta = Self::next_after(0.0, f64::MAX).max(delta);
538        assert!(delta > 0.0);
539        if fraction >= delta {
540            // Insert decimal point.
541            frac_buf[fraction_cursor] = b'.';
542            fraction_cursor += 1;
543            loop {
544                // Shift up by one digit.
545                fraction *= radix as f64;
546                delta *= radix as f64;
547                // Write digit.
548                let digit = fraction as u32;
549                frac_buf[fraction_cursor] =
550                    std::char::from_digit(digit, radix as u32).unwrap() as u8;
551                fraction_cursor += 1;
552                // Calculate remainder.
553                fraction -= digit as f64;
554                // Round to even.
555                if fraction + delta > 1.0
556                    && (fraction > 0.5 || (fraction - 0.5).abs() < f64::EPSILON && digit & 1 != 0)
557                {
558                    loop {
559                        // We need to back trace already written digits in case of carry-over.
560                        fraction_cursor -= 1;
561                        if fraction_cursor == 0 {
562                            //              CHECK_EQ('.', buffer[fraction_cursor]);
563                            // Carry over to the integer part.
564                            integer += 1.;
565                        } else {
566                            let c: u8 = frac_buf[fraction_cursor];
567                            // Reconstruct digit.
568                            let digit_0 = (c as char).to_digit(10).unwrap();
569                            if digit_0 + 1 >= radix as u32 {
570                                continue;
571                            }
572                            frac_buf[fraction_cursor] =
573                                std::char::from_digit(digit_0 + 1, radix as u32).unwrap() as u8;
574                            fraction_cursor += 1;
575                        }
576                        break;
577                    }
578                    break;
579                }
580                if fraction < delta {
581                    break;
582                }
583            }
584        }
585
586        // Compute integer digits. Fill unrepresented digits with zero.
587        let mut int_iter = int_buf.iter_mut().enumerate().rev(); //.rev();
588        while FloatCore::integer_decode(integer / f64::from(radix)).1 > 0 {
589            integer /= radix as f64;
590            *int_iter.next().unwrap().1 = b'0';
591        }
592
593        loop {
594            let remainder = integer % (radix as f64);
595            *int_iter.next().unwrap().1 =
596                std::char::from_digit(remainder as u32, radix as u32).unwrap() as u8;
597            integer = (integer - remainder) / radix as f64;
598            if integer <= 0f64 {
599                break;
600            }
601        }
602        // Add sign and terminate string.
603        if negative {
604            *int_iter.next().unwrap().1 = b'-';
605        }
606        assert!(fraction_cursor < BUF_SIZE);
607
608        let integer_cursor = int_iter.next().unwrap().0 + 1;
609        let fraction_cursor = fraction_cursor + BUF_SIZE / 2;
610        // dbg!("Number: {}, Radix: {}, Cursors: {}, {}", value, radix, integer_cursor, fraction_cursor);
611        String::from_utf8_lossy(&buffer[integer_cursor..fraction_cursor]).into()
612    }
613
614    #[allow(clippy::wrong_self_convention)]
615    pub(crate) fn to_native_string(x: f64) -> String {
616        let mut buffer = ryu_js::Buffer::new();
617        buffer.format(x).to_string()
618    }
619
620    /// `Number.prototype.toString( [radix] )`
621    ///
622    /// The `toString()` method returns a string representing the specified Number object.
623    ///
624    /// More information:
625    ///  - [ECMAScript reference][spec]
626    ///  - [MDN documentation][mdn]
627    ///
628    /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tostring
629    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString
630    #[allow(clippy::wrong_self_convention)]
631    pub(crate) fn to_string(
632        this: &JsValue,
633        args: &[JsValue],
634        context: &mut Context,
635    ) -> JsResult<JsValue> {
636        // 1. Let x be ? thisNumberValue(this value).
637        let x = Self::this_number_value(this, context)?;
638
639        // 2. If radix is undefined, let radixNumber be 10.
640        // 3. Else, let radixNumber be ? ToInteger(radix).
641        let radix = args
642            .get(0)
643            .map(|arg| arg.to_integer(context))
644            .transpose()?
645            .map_or(10, |radix| radix as u8);
646
647        // 4. If radixNumber < 2 or radixNumber > 36, throw a RangeError exception.
648        if !(2..=36).contains(&radix) {
649            return context
650                .throw_range_error("radix must be an integer at least 2 and no greater than 36");
651        }
652
653        // 5. If radixNumber = 10, return ! ToString(x).
654        if radix == 10 {
655            return Ok(JsValue::new(Self::to_native_string(x)));
656        }
657
658        if x == -0. {
659            return Ok(JsValue::new("0"));
660        } else if x.is_nan() {
661            return Ok(JsValue::new("NaN"));
662        } else if x.is_infinite() && x.is_sign_positive() {
663            return Ok(JsValue::new("Infinity"));
664        } else if x.is_infinite() && x.is_sign_negative() {
665            return Ok(JsValue::new("-Infinity"));
666        }
667
668        // This is a Optimization from the v8 source code to print values that can fit in a single character
669        // Since the actual num_to_string allocates a 2200 bytes buffer for actual conversion
670        // I am not sure if this part is effective as the v8 equivalent https://chromium.googlesource.com/v8/v8/+/refs/heads/master/src/builtins/number.tq#53
671        // // Fast case where the result is a one character string.
672        // if x.is_sign_positive() && x.fract() == 0.0 && x < radix_number as f64 {
673        //     return Ok(to_value(format!("{}", std::char::from_digit(x as u32, radix_number as u32).unwrap())))
674        // }
675
676        // 6. Return the String representation of this Number value using the radix specified by radixNumber.
677        Ok(JsValue::new(Self::to_native_string_radix(x, radix)))
678    }
679
680    /// `Number.prototype.toString()`
681    ///
682    /// The `valueOf()` method returns the wrapped primitive value of a Number object.
683    ///
684    /// More information:
685    ///  - [ECMAScript reference][spec]
686    ///  - [MDN documentation][mdn]
687    ///
688    /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.valueof
689    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/valueOf
690    pub(crate) fn value_of(
691        this: &JsValue,
692        _: &[JsValue],
693        context: &mut Context,
694    ) -> JsResult<JsValue> {
695        Ok(JsValue::new(Self::this_number_value(this, context)?))
696    }
697
698    /// Builtin javascript 'parseInt(str, radix)' function.
699    ///
700    /// Parses the given string as an integer using the given radix as a base.
701    ///
702    /// An argument of type Number (i.e. Integer or Rational) is also accepted in place of string.
703    ///
704    /// The radix must be an integer in the range [2, 36] inclusive.
705    ///
706    /// More information:
707    ///  - [ECMAScript reference][spec]
708    ///  - [MDN documentation][mdn]
709    ///
710    /// [spec]: https://tc39.es/ecma262/#sec-parseint-string-radix
711    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt
712    pub(crate) fn parse_int(
713        _: &JsValue,
714        args: &[JsValue],
715        context: &mut Context,
716    ) -> JsResult<JsValue> {
717        if let (Some(val), radix) = (args.get(0), args.get_or_undefined(1)) {
718            // 1. Let inputString be ? ToString(string).
719            let input_string = val.to_string(context)?;
720
721            // 2. Let S be ! TrimString(inputString, start).
722            let mut var_s = input_string.trim_start_matches(is_trimmable_whitespace);
723
724            // 3. Let sign be 1.
725            // 4. If S is not empty and the first code unit of S is the code unit 0x002D (HYPHEN-MINUS),
726            //    set sign to -1.
727            let sign = if !var_s.is_empty() && var_s.starts_with('\u{002D}') {
728                -1
729            } else {
730                1
731            };
732
733            // 5. If S is not empty and the first code unit of S is the code unit 0x002B (PLUS SIGN) or
734            //    the code unit 0x002D (HYPHEN-MINUS), remove the first code unit from S.
735            if !var_s.is_empty() {
736                var_s = var_s
737                    .strip_prefix(&['\u{002B}', '\u{002D}'][..])
738                    .unwrap_or(var_s);
739            }
740
741            // 6. Let R be ℝ(? ToInt32(radix)).
742            let mut var_r = radix.to_i32(context)?;
743
744            // 7. Let stripPrefix be true.
745            let mut strip_prefix = true;
746
747            // 8. If R ≠ 0, then
748            if var_r != 0 {
749                //     a. If R < 2 or R > 36, return NaN.
750                if !(2..=36).contains(&var_r) {
751                    return Ok(JsValue::nan());
752                }
753
754                //     b. If R ≠ 16, set stripPrefix to false.
755                if var_r != 16 {
756                    strip_prefix = false
757                }
758            } else {
759                // 9. Else,
760                //     a. Set R to 10.
761                var_r = 10;
762            }
763
764            // 10. If stripPrefix is true, then
765            //     a. If the length of S is at least 2 and the first two code units of S are either "0x" or "0X", then
766            //         i. Remove the first two code units from S.
767            //         ii. Set R to 16.
768            if strip_prefix
769                && var_s.len() >= 2
770                && (var_s.starts_with("0x") || var_s.starts_with("0X"))
771            {
772                var_s = var_s.split_at(2).1;
773
774                var_r = 16;
775            }
776
777            // 11. If S contains a code unit that is not a radix-R digit, let end be the index within S of the
778            //     first such code unit; otherwise, let end be the length of S.
779            let end = if let Some(index) = var_s.find(|c: char| !c.is_digit(var_r as u32)) {
780                index
781            } else {
782                var_s.len()
783            };
784
785            // 12. Let Z be the substring of S from 0 to end.
786            let var_z = var_s.split_at(end).0;
787
788            // 13. If Z is empty, return NaN.
789            if var_z.is_empty() {
790                return Ok(JsValue::nan());
791            }
792
793            // 14. Let mathInt be the integer value that is represented by Z in radix-R notation, using the
794            //     letters A-Z and a-z for digits with values 10 through 35. (However, if R is 10 and Z contains
795            //     more than 20 significant digits, every significant digit after the 20th may be replaced by a
796            //     0 digit, at the option of the implementation; and if R is not 2, 4, 8, 10, 16, or 32, then
797            //     mathInt may be an implementation-approximated value representing the integer value that is
798            //     represented by Z in radix-R notation.)
799            let math_int = u64::from_str_radix(var_z, var_r as u32).map_or_else(
800                |_| f64::from_str_radix(var_z, var_r as u32).expect("invalid_float_conversion"),
801                |i| i as f64,
802            );
803
804            // 15. If mathInt = 0, then
805            //     a. If sign = -1, return -0𝔽.
806            //     b. Return +0𝔽.
807            if math_int == 0_f64 {
808                if sign == -1 {
809                    return Ok(JsValue::new(-0_f64));
810                } else {
811                    return Ok(JsValue::new(0_f64));
812                }
813            }
814
815            // 16. Return 𝔽(sign × mathInt).
816            Ok(JsValue::new(f64::from(sign) * math_int))
817        } else {
818            // Not enough arguments to parseInt.
819            Ok(JsValue::nan())
820        }
821    }
822
823    /// Builtin javascript 'parseFloat(str)' function.
824    ///
825    /// Parses the given string as a floating point value.
826    ///
827    /// An argument of type Number (i.e. Integer or Rational) is also accepted in place of string.
828    ///
829    /// To improve performance an Integer type Number is returned in place of a Rational if the given
830    /// string can be parsed and stored as an Integer.
831    ///
832    /// More information:
833    ///  - [ECMAScript reference][spec]
834    ///  - [MDN documentation][mdn]
835    ///
836    /// [spec]: https://tc39.es/ecma262/#sec-parsefloat-string
837    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat
838    pub(crate) fn parse_float(
839        _: &JsValue,
840        args: &[JsValue],
841        context: &mut Context,
842    ) -> JsResult<JsValue> {
843        if let Some(val) = args.get(0) {
844            let input_string = val.to_string(context)?;
845            let s = input_string.trim_start_matches(is_trimmable_whitespace);
846            let s_prefix_lower = s.chars().take(4).collect::<String>().to_ascii_lowercase();
847
848            // TODO: write our own lexer to match syntax StrDecimalLiteral
849            if s.starts_with("Infinity") || s.starts_with("+Infinity") {
850                Ok(JsValue::new(f64::INFINITY))
851            } else if s.starts_with("-Infinity") {
852                Ok(JsValue::new(f64::NEG_INFINITY))
853            } else if s_prefix_lower.starts_with("inf")
854                || s_prefix_lower.starts_with("+inf")
855                || s_prefix_lower.starts_with("-inf")
856            {
857                // Prevent fast_float from parsing "inf", "+inf" as Infinity and "-inf" as -Infinity
858                Ok(JsValue::nan())
859            } else {
860                Ok(fast_float::parse_partial::<f64, _>(s)
861                    .map(|(f, len)| {
862                        if len > 0 {
863                            JsValue::new(f)
864                        } else {
865                            JsValue::nan()
866                        }
867                    })
868                    .unwrap_or_else(|_| JsValue::nan()))
869            }
870        } else {
871            // Not enough arguments to parseFloat.
872            Ok(JsValue::nan())
873        }
874    }
875
876    /// Builtin javascript 'isFinite(number)' function.
877    ///
878    /// Converts the argument to a number, throwing a type error if the conversion is invalid.
879    ///
880    /// If the number is NaN, +∞, or -∞ false is returned.
881    ///
882    /// Otherwise true is returned.
883    ///
884    /// More information:
885    ///  - [ECMAScript reference][spec]
886    ///  - [MDN documentation][mdn]
887    ///
888    /// [spec]: https://tc39.es/ecma262/#sec-isfinite-number
889    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isFinite
890    pub(crate) fn global_is_finite(
891        _: &JsValue,
892        args: &[JsValue],
893        context: &mut Context,
894    ) -> JsResult<JsValue> {
895        if let Some(value) = args.get(0) {
896            let number = value.to_number(context)?;
897            Ok(number.is_finite().into())
898        } else {
899            Ok(false.into())
900        }
901    }
902
903    /// Builtin javascript 'isNaN(number)' function.
904    ///
905    /// Converts the argument to a number, throwing a type error if the conversion is invalid.
906    ///
907    /// If the number is NaN true is returned.
908    ///
909    /// Otherwise false is returned.
910    ///
911    /// More information:
912    ///  - [ECMAScript reference][spec]
913    ///  - [MDN documentation][mdn]
914    ///
915    /// [spec]: https://tc39.es/ecma262/#sec-isnan-number
916    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN
917    pub(crate) fn global_is_nan(
918        _: &JsValue,
919        args: &[JsValue],
920        context: &mut Context,
921    ) -> JsResult<JsValue> {
922        if let Some(value) = args.get(0) {
923            let number = value.to_number(context)?;
924            Ok(number.is_nan().into())
925        } else {
926            Ok(true.into())
927        }
928    }
929
930    /// `Number.isFinite( number )`
931    ///
932    /// Checks if the argument is a number, returning false if it isn't.
933    ///
934    /// If the number is NaN, +∞, or -∞ false is returned.
935    ///
936    /// Otherwise true is returned.
937    ///
938    /// More information:
939    ///  - [ECMAScript reference][spec]
940    ///  - [MDN documentation][mdn]
941    ///
942    /// [spec]: https://tc39.es/ecma262/#sec-number.isfinite
943    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite
944    pub(crate) fn number_is_finite(
945        _: &JsValue,
946        args: &[JsValue],
947        _ctx: &mut Context,
948    ) -> JsResult<JsValue> {
949        Ok(JsValue::new(if let Some(val) = args.get(0) {
950            match val {
951                JsValue::Integer(_) => true,
952                JsValue::Rational(number) => number.is_finite(),
953                _ => false,
954            }
955        } else {
956            false
957        }))
958    }
959
960    /// `Number.isInteger( number )`
961    ///
962    /// Checks if the argument is an integer.
963    ///
964    /// More information:
965    ///  - [ECMAScript reference][spec]
966    ///  - [MDN documentation][mdn]
967    ///
968    /// [spec]: https://tc39.es/ecma262/#sec-number.isinteger
969    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger
970    pub(crate) fn number_is_integer(
971        _: &JsValue,
972        args: &[JsValue],
973        _ctx: &mut Context,
974    ) -> JsResult<JsValue> {
975        Ok(args.get(0).map_or(false, Self::is_integer).into())
976    }
977
978    /// `Number.isNaN( number )`
979    ///
980    /// Checks if the argument is a number, returning false if it isn't.
981    ///
982    /// If the number is NaN true is returned.
983    ///
984    /// Otherwise false is returned.
985    ///
986    /// More information:
987    ///  - [ECMAScript reference][spec]
988    ///  - [MDN documentation][mdn]
989    ///
990    /// [spec]: https://tc39.es/ecma262/#sec-isnan-number
991    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
992    pub(crate) fn number_is_nan(
993        _: &JsValue,
994        args: &[JsValue],
995        _ctx: &mut Context,
996    ) -> JsResult<JsValue> {
997        Ok(JsValue::new(if let Some(val) = args.get(0) {
998            match val {
999                JsValue::Integer(_) => false,
1000                JsValue::Rational(number) => number.is_nan(),
1001                _ => false,
1002            }
1003        } else {
1004            false
1005        }))
1006    }
1007
1008    /// `Number.isSafeInteger( number )`
1009    ///
1010    /// Checks if the argument is an integer, returning false if it isn't.
1011    ///
1012    /// If abs(number) ≤ MAX_SAFE_INTEGER true is returned.
1013    ///
1014    /// Otherwise false is returned.
1015    ///
1016    /// More information:
1017    ///  - [ECMAScript reference][spec]
1018    ///  - [MDN documentation][mdn]
1019    ///
1020    /// [spec]: https://tc39.es/ecma262/#sec-isnan-number
1021    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
1022    pub(crate) fn is_safe_integer(
1023        _: &JsValue,
1024        args: &[JsValue],
1025        _ctx: &mut Context,
1026    ) -> JsResult<JsValue> {
1027        Ok(JsValue::new(match args.get(0) {
1028            Some(JsValue::Integer(_)) => true,
1029            Some(JsValue::Rational(number)) if Self::is_float_integer(*number) => {
1030                number.abs() <= Number::MAX_SAFE_INTEGER
1031            }
1032            _ => false,
1033        }))
1034    }
1035
1036    /// Checks if the argument is a finite integer Number value.
1037    ///
1038    /// More information:
1039    ///  - [ECMAScript reference][spec]
1040    ///
1041    /// [spec]: https://tc39.es/ecma262/#sec-isinteger
1042    #[inline]
1043    pub(crate) fn is_integer(val: &JsValue) -> bool {
1044        match val {
1045            JsValue::Integer(_) => true,
1046            JsValue::Rational(number) => Number::is_float_integer(*number),
1047            _ => false,
1048        }
1049    }
1050
1051    /// Checks if the float argument is an integer.
1052    #[inline]
1053    #[allow(clippy::float_cmp)]
1054    pub(crate) fn is_float_integer(number: f64) -> bool {
1055        number.is_finite() && number.abs().floor() == number.abs()
1056    }
1057
1058    /// The abstract operation Number::equal takes arguments
1059    /// x (a Number) and y (a Number). It performs the following steps when called:
1060    ///
1061    /// <https://tc39.es/ecma262/#sec-numeric-types-number-equal>
1062    #[inline]
1063    #[allow(clippy::float_cmp)]
1064    pub(crate) fn equal(x: f64, y: f64) -> bool {
1065        x == y
1066    }
1067
1068    /// The abstract operation Number::sameValue takes arguments
1069    /// x (a Number) and y (a Number). It performs the following steps when called:
1070    ///
1071    /// <https://tc39.es/ecma262/#sec-numeric-types-number-sameValue>
1072    #[allow(clippy::float_cmp)]
1073    pub(crate) fn same_value(a: f64, b: f64) -> bool {
1074        if a.is_nan() && b.is_nan() {
1075            return true;
1076        }
1077        a == b && a.signum() == b.signum()
1078    }
1079
1080    /// The abstract operation Number::sameValueZero takes arguments
1081    /// x (a Number) and y (a Number). It performs the following steps when called:
1082    ///
1083    /// <https://tc39.es/ecma262/#sec-numeric-types-number-sameValueZero>
1084    #[inline]
1085    #[allow(clippy::float_cmp)]
1086    pub(crate) fn same_value_zero(x: f64, y: f64) -> bool {
1087        if x.is_nan() && y.is_nan() {
1088            return true;
1089        }
1090
1091        x == y
1092    }
1093
1094    #[inline]
1095    #[allow(clippy::float_cmp)]
1096    pub(crate) fn less_than(x: f64, y: f64) -> AbstractRelation {
1097        if x.is_nan() || y.is_nan() {
1098            return AbstractRelation::Undefined;
1099        }
1100        if x == y || x == 0.0 && y == -0.0 || x == -0.0 && y == 0.0 {
1101            return AbstractRelation::False;
1102        }
1103        if x.is_infinite() && x.is_sign_positive() {
1104            return AbstractRelation::False;
1105        }
1106        if y.is_infinite() && y.is_sign_positive() {
1107            return AbstractRelation::True;
1108        }
1109        if x.is_infinite() && x.is_sign_negative() {
1110            return AbstractRelation::True;
1111        }
1112        if y.is_infinite() && y.is_sign_negative() {
1113            return AbstractRelation::False;
1114        }
1115        (x < y).into()
1116    }
1117
1118    #[inline]
1119    pub(crate) fn not(x: f64) -> i32 {
1120        let x = f64_to_int32(x);
1121        !x
1122    }
1123}