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}