Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

parseInt, parseFloat implementation #459

Merged
merged 14 commits into from
Jun 9, 2020
17 changes: 16 additions & 1 deletion boa/src/builtins/function/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<N>(function: NativeFunctionData, name: N, parent: &Value, length: i32)
Expand Down
135 changes: 135 additions & 0 deletions boa/src/builtins/number/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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::<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
pub(crate) fn create(global: &Value) -> Value {
let prototype = Value::new_object(Some(global));
Expand All @@ -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:
Expand Down
207 changes: 207 additions & 0 deletions boa/src/builtins/number/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}