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
15 changes: 15 additions & 0 deletions boa/src/builtins/function/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,21 @@ pub fn make_constructor_fn(

/// Macro to create a new member function of a prototype.
Lan2u marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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)
where
Expand Down
111 changes: 110 additions & 1 deletion boa/src/builtins/number/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@ use crate::{
BoaProfiler,
};
use num_traits::float::FloatCore;
use std::{borrow::Borrow, ops::Deref};
use std::{borrow::Borrow, f64, ops::Deref, str::FromStr};

const BUF_SIZE: usize = 2200;

/// `Number` implementation.
#[derive(Debug, Clone, Copy)]
pub(crate) struct Number;

const PARSE_INT_MAX_ARG_COUNT: usize = 2;
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 +408,99 @@ impl Number {
Ok(Self::to_number(this))
}

pub(crate) fn parse_int(
_this: &mut Value,
args: &[Value],
_ctx: &mut Interpreter,
) -> ResultValue {
if args.len() > PARSE_INT_MAX_ARG_COUNT {
// Too many arguments.
return Err(Value::undefined());
}
Razican marked this conversation as resolved.
Show resolved Hide resolved

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.
Err(Value::undefined())
Razican marked this conversation as resolved.
Show resolved Hide resolved
}
}

pub(crate) fn parse_float(
_this: &mut Value,
args: &[Value],
_ctx: &mut Interpreter,
) -> ResultValue {
if args.len() > PARSE_FLOAT_MAX_ARG_COUNT {
// Too many arguments.
return Err(Value::undefined());
}
Razican marked this conversation as resolved.
Show resolved Hide resolved

if let Some(val) = args.get(0) {
match val.data() {
ValueData::String(s) => {
if let Ok(f) = f64::from_str(s) {
Razican marked this conversation as resolved.
Show resolved Hide resolved
Ok(Value::rational(f))
} else {
// String can't be parsed.
Ok(Value::from(f64::NAN))
}
}
ValueData::Integer(i) => Ok(Value::rational(*i as f64)),
Lan2u marked this conversation as resolved.
Show resolved Hide resolved
ValueData::Rational(f) => Ok(Value::rational(*f)),
_ => {
// Wrong argument type to parseFloat.
Ok(Value::from(f64::NAN))
}
}
} else {
// Not enough arguments to parseFloat.
Err(Value::undefined())
Razican marked this conversation as resolved.
Show resolved Hide resolved
}
}

/// Create a new `Number` object
pub(crate) fn create(global: &Value) -> Value {
let prototype = Value::new_object(Some(global));
Expand All @@ -417,6 +513,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
208 changes: 208 additions & 0 deletions boa/src/builtins/number/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,3 +470,211 @@ 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");
}

#[test]
fn parse_int_no_args() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);

assert_eq!(&forward(&mut engine, "parseInt()"), "Error: undefined");
}

#[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)"),
"Error: undefined"
);
}

#[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");
}

#[test]
fn parse_float_no_args() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);

assert_eq!(&forward(&mut engine, "parseFloat()"), "Error: undefined");
}

#[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)"),
"Error: undefined"
);
}