diff --git a/boa/src/builtins/function/mod.rs b/boa/src/builtins/function/mod.rs index 5e7a5ae973..ae4575c1c6 100644 --- a/boa/src/builtins/function/mod.rs +++ b/boa/src/builtins/function/mod.rs @@ -429,7 +429,22 @@ pub fn make_constructor_fn( constructor_val } -/// Macro to create a new member function of a prototype. +/// Creates a new member function of a `Object` or `prototype`. +/// +/// A function registered using this macro can then be called from Javascript using: +/// +/// parent.name() +/// +/// See the javascript 'Number.toString()' as an example. +/// +/// # Arguments +/// function: The function to register as a built in function. +/// name: The name of the function (how it will be called but without the ()). +/// parent: The object to register the function on, if the global object is used then the function is instead called as name() +/// without requiring the parent, see parseInt() as an example. +/// length: As described at https://tc39.es/ecma262/#sec-function-instances-length, The value of the "length" property is an integer that +/// indicates the typical number of arguments expected by the function. However, the language permits the function to be invoked with +/// some other number of arguments. /// /// If no length is provided, the length will be set to 0. pub fn make_builtin_fn(function: NativeFunctionData, name: N, parent: &Value, length: i32) diff --git a/boa/src/builtins/number/mod.rs b/boa/src/builtins/number/mod.rs index 86ee9d4344..b2cd26b5f7 100644 --- a/boa/src/builtins/number/mod.rs +++ b/boa/src/builtins/number/mod.rs @@ -37,6 +37,12 @@ const BUF_SIZE: usize = 2200; #[derive(Debug, Clone, Copy)] pub(crate) struct Number; +/// Maximum number of arguments expected to the builtin parseInt() function. +const PARSE_INT_MAX_ARG_COUNT: usize = 2; + +/// Maximum number of arguments expected to the builtin parseFloat() function. +const PARSE_FLOAT_MAX_ARG_COUNT: usize = 1; + impl Number { /// Helper function that converts a Value to a Number. #[allow(clippy::wrong_self_convention)] @@ -405,6 +411,122 @@ impl Number { Ok(Self::to_number(this)) } + /// Builtin javascript 'parseInt(str, radix)' function. + /// + /// Parses the given string as an integer using the given radix as a base. + /// + /// An argument of type Number (i.e. Integer or Rational) is also accepted in place of string. + /// + /// The radix must be an integer in the range [2, 36] inclusive. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-parseint-string-radix + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt + pub(crate) fn parse_int( + _this: &mut Value, + args: &[Value], + _ctx: &mut Interpreter, + ) -> ResultValue { + if let (Some(val), r) = (args.get(0), args.get(1)) { + let mut radix = if let Some(rx) = r { + if let ValueData::Integer(i) = rx.data() { + *i as u32 + } else { + // Handling a second argument that isn't an integer but was provided so cannot be defaulted. + return Ok(Value::from(f64::NAN)); + } + } else { + // No second argument provided therefore radix is unknown + 0 + }; + + match val.data() { + ValueData::String(s) => { + // Attempt to infer radix from given string. + + if radix == 0 { + if s.starts_with("0x") || s.starts_with("0X") { + if let Ok(i) = i32::from_str_radix(&s[2..], 16) { + return Ok(Value::integer(i)); + } else { + // String can't be parsed. + return Ok(Value::from(f64::NAN)); + } + } else { + radix = 10 + }; + } + + if let Ok(i) = i32::from_str_radix(s, radix) { + Ok(Value::integer(i)) + } else { + // String can't be parsed. + Ok(Value::from(f64::NAN)) + } + } + ValueData::Integer(i) => Ok(Value::integer(*i)), + ValueData::Rational(f) => Ok(Value::integer(*f as i32)), + _ => { + // Wrong argument type to parseInt. + Ok(Value::from(f64::NAN)) + } + } + } else { + // Not enough arguments to parseInt. + Ok(Value::from(f64::NAN)) + } + } + + /// Builtin javascript 'parseFloat(str)' function. + /// + /// Parses the given string as a floating point value. + /// + /// An argument of type Number (i.e. Integer or Rational) is also accepted in place of string. + /// + /// To improve performance an Integer type Number is returned in place of a Rational if the given + /// string can be parsed and stored as an Integer. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-parsefloat-string + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat + pub(crate) fn parse_float( + _this: &mut Value, + args: &[Value], + _ctx: &mut Interpreter, + ) -> ResultValue { + if let Some(val) = args.get(0) { + match val.data() { + ValueData::String(s) => { + if let Ok(i) = s.parse::() { + // Attempt to parse an integer first so that it can be stored as an integer + // to improve performance + Ok(Value::integer(i)) + } else if let Ok(f) = s.parse::() { + Ok(Value::rational(f)) + } else { + // String can't be parsed. + Ok(Value::from(f64::NAN)) + } + } + ValueData::Integer(i) => Ok(Value::integer(*i)), + ValueData::Rational(f) => Ok(Value::rational(*f)), + _ => { + // Wrong argument type to parseFloat. + Ok(Value::from(f64::NAN)) + } + } + } else { + // Not enough arguments to parseFloat. + Ok(Value::from(f64::NAN)) + } + } + /// Create a new `Number` object pub(crate) fn create(global: &Value) -> Value { let prototype = Value::new_object(Some(global)); @@ -417,6 +539,19 @@ impl Number { make_builtin_fn(Self::to_string, "toString", &prototype, 1); make_builtin_fn(Self::value_of, "valueOf", &prototype, 0); + make_builtin_fn( + Self::parse_int, + "parseInt", + global, + PARSE_INT_MAX_ARG_COUNT as i32, + ); + make_builtin_fn( + Self::parse_float, + "parseFloat", + global, + PARSE_FLOAT_MAX_ARG_COUNT as i32, + ); + let number = make_constructor_fn("Number", 1, Self::make_number, global, prototype, true); // Constants from: diff --git a/boa/src/builtins/number/tests.rs b/boa/src/builtins/number/tests.rs index 090f9f180e..c1a310764c 100644 --- a/boa/src/builtins/number/tests.rs +++ b/boa/src/builtins/number/tests.rs @@ -470,3 +470,210 @@ fn number_constants() { .unwrap() .is_null_or_undefined()); } + +#[test] +fn parse_int_simple() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseInt(\"6\")"), "6"); +} + +#[test] +fn parse_int_negative() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseInt(\"-9\")"), "-9"); +} + +#[test] +fn parse_int_already_int() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseInt(100)"), "100"); +} + +#[test] +fn parse_int_float() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseInt(100.5)"), "100"); +} + +#[test] +fn parse_int_float_str() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseInt(\"100.5\")"), "NaN"); +} + +#[test] +fn parse_int_inferred_hex() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseInt(\"0xA\")"), "10"); +} + +/// This test demonstrates that this version of parseInt treats strings starting with 0 to be parsed with +/// a radix 10 if no radix is specified. Some alternative implementations default to a radix of 8. +#[test] +fn parse_int_zero_start() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseInt(\"018\")"), "18"); +} + +#[test] +fn parse_int_varying_radix() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + let base_str = "1000"; + + for radix in 2..36 { + let expected = i32::from_str_radix(base_str, radix).unwrap(); + + assert_eq!( + forward( + &mut engine, + &format!("parseInt(\"{}\", {} )", base_str, radix) + ), + expected.to_string() + ); + } +} + +#[test] +fn parse_int_negative_varying_radix() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + let base_str = "-1000"; + + for radix in 2..36 { + let expected = i32::from_str_radix(base_str, radix).unwrap(); + + assert_eq!( + forward( + &mut engine, + &format!("parseInt(\"{}\", {} )", base_str, radix) + ), + expected.to_string() + ); + } +} + +#[test] +fn parse_int_malformed_str() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseInt(\"hello\")"), "NaN"); +} + +#[test] +fn parse_int_undefined() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseInt(undefined)"), "NaN"); +} + +/// Shows that no arguments to parseInt is treated the same as if undefined was +/// passed as the first argument. +#[test] +fn parse_int_no_args() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseInt()"), "NaN"); +} + +/// Shows that extra arguments to parseInt are ignored. +#[test] +fn parse_int_too_many_args() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseInt(\"100\", 10, 10)"), "100"); +} + +#[test] +fn parse_float_simple() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseFloat(\"6.5\")"), "6.5"); +} + +#[test] +fn parse_float_int() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseFloat(10)"), "10"); +} + +#[test] +fn parse_float_int_str() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseFloat(\"8\")"), "8"); +} + +#[test] +fn parse_float_already_float() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseFloat(17.5)"), "17.5"); +} + +#[test] +fn parse_float_negative() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseFloat(\"-99.7\")"), "-99.7"); +} + +#[test] +fn parse_float_malformed_str() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseFloat(\"hello\")"), "NaN"); +} + +#[test] +fn parse_float_undefined() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseFloat(undefined)"), "NaN"); +} + +/// No arguments to parseFloat is treated the same as passing undefined as the first argument. +#[test] +fn parse_float_no_args() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseFloat()"), "NaN"); +} + +/// Shows that the parseFloat function ignores extra arguments. +#[test] +fn parse_float_too_many_args() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + assert_eq!(&forward(&mut engine, "parseFloat(\"100.5\", 10)"), "100.5"); +}