Browse Source

parseInt, parseFloat implementation (#459)

* Added documentation to make_builtin_fn

* Simple impl of parseInt()

* Impl rest of parse_int

* Fixed handling of strings starting 0x

* Made NaN return as per js spec

* Rework to improve clarity

* parseFloat impl, added tests

* Addressed comments to PR

* Removed f64 import

* Fixed handling of too many/few arguments to parseInt/Float

Co-authored-by: HalidOdat <halidodat@gmail.com>
pull/469/head
Paul Lancaster 5 years ago committed by GitHub
parent
commit
a78934d424
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      boa/src/builtins/function/mod.rs
  2. 135
      boa/src/builtins/number/mod.rs
  3. 207
      boa/src/builtins/number/tests.rs

17
boa/src/builtins/function/mod.rs

@ -429,7 +429,22 @@ pub fn make_constructor_fn(
constructor_val 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. /// If no length is provided, the length will be set to 0.
pub fn make_builtin_fn<N>(function: NativeFunctionData, name: N, parent: &Value, length: i32) pub fn make_builtin_fn<N>(function: NativeFunctionData, name: N, parent: &Value, length: i32)

135
boa/src/builtins/number/mod.rs

@ -37,6 +37,12 @@ const BUF_SIZE: usize = 2200;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub(crate) struct Number; 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 { impl Number {
/// Helper function that converts a Value to a Number. /// Helper function that converts a Value to a Number.
#[allow(clippy::wrong_self_convention)] #[allow(clippy::wrong_self_convention)]
@ -405,6 +411,122 @@ impl Number {
Ok(Self::to_number(this)) 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::<i32>() {
// 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::<f64>() {
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 /// Create a new `Number` object
pub(crate) fn create(global: &Value) -> Value { pub(crate) fn create(global: &Value) -> Value {
let prototype = Value::new_object(Some(global)); 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::to_string, "toString", &prototype, 1);
make_builtin_fn(Self::value_of, "valueOf", &prototype, 0); 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); let number = make_constructor_fn("Number", 1, Self::make_number, global, prototype, true);
// Constants from: // Constants from:

207
boa/src/builtins/number/tests.rs

@ -470,3 +470,210 @@ fn number_constants() {
.unwrap() .unwrap()
.is_null_or_undefined()); .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");
}

Loading…
Cancel
Save