Skip to content

Commit

Permalink
Implement Array.prototype.reduce (#555)
Browse files Browse the repository at this point in the history
Co-authored-by: HalidOdat <halidodat@gmail.com>
  • Loading branch information
benjaminflin and HalidOdat authored Jul 15, 2020
1 parent 690b194 commit 3e2e566
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 3 deletions.
74 changes: 71 additions & 3 deletions boa/src/builtins/array/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ impl Array {
1 if args[0].is_integer() => {
length = i32::from(&args[0]);
// TODO: It should not create an array of undefineds, but an empty array ("holy" array in V8) with length `n`.
for n in 0..length {
this.set_field(n.to_string(), Value::undefined());
}
// for n in 0..length {
// this.set_field(n.to_string(), Value::undefined());
// }
}
1 if args[0].is_double() => {
return ctx.throw_range_error("invalid array length");
Expand Down Expand Up @@ -957,6 +957,73 @@ impl Array {
Ok(Value::from(false))
}

/// `Array.prototype.reduce( callbackFn [ , initialValue ] )`
///
/// The reduce method traverses left to right starting from the first defined value in the array,
/// accumulating a value using a given callback function. It returns the accumulated value.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.reduce
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
pub(crate) fn reduce(
this: &Value,
args: &[Value],
interpreter: &mut Interpreter,
) -> ResultValue {
let this = interpreter.to_object(this)?;
let callback = match args.get(0) {
Some(value) if value.is_function() => value,
_ => return interpreter.throw_type_error("Reduce was called without a callback"),
};
let initial_value = args.get(1).cloned().unwrap_or_else(Value::undefined);
let mut length = interpreter.to_length(&this.get_field("length"))?;
if length == 0 && initial_value.is_undefined() {
return interpreter
.throw_type_error("Reduce was called on an empty array and with no initial value");
}
let mut k = 0;
let mut accumulator = if initial_value.is_undefined() {
let mut k_present = false;
while k < length {
if this.has_field(&k.to_string()) {
k_present = true;
break;
}
k += 1;
}
if !k_present {
return interpreter.throw_type_error(
"Reduce was called on an empty array and with no initial value",
);
}
let result = this.get_field(k.to_string());
k += 1;
result
} else {
initial_value
};
while k < length {
if this.has_field(&k.to_string()) {
let arguments = [
accumulator,
this.get_field(k.to_string()),
Value::from(k),
this.clone(),
];
accumulator = interpreter.call(&callback, &Value::undefined(), &arguments)?;
/* We keep track of possibly shortened length in order to prevent unnecessary iteration.
It may also be necessary to do this since shortening the array length does not
delete array elements. See: https://github.com/boa-dev/boa/issues/557 */
length = min(length, interpreter.to_length(&this.get_field("length"))?);
}
k += 1;
}
Ok(accumulator)
}

/// Initialise the `Array` object on the global object.
#[inline]
pub(crate) fn init(global: &Value) -> (&str, Value) {
Expand Down Expand Up @@ -988,6 +1055,7 @@ impl Array {
make_builtin_fn(Self::find_index, "findIndex", &prototype, 1);
make_builtin_fn(Self::slice, "slice", &prototype, 2);
make_builtin_fn(Self::some, "some", &prototype, 2);
make_builtin_fn(Self::reduce, "reduce", &prototype, 2);

let array = make_constructor_fn(
Self::NAME,
Expand Down
110 changes: 110 additions & 0 deletions boa/src/builtins/array/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,116 @@ fn some() {
assert_eq!(result, "true");
}

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

let init = r#"
var arr = [1, 2, 3, 4];
function add(acc, x) {
return acc + x;
}
function addIdx(acc, _, idx) {
return acc + idx;
}
function addLen(acc, _x, _idx, arr) {
return acc + arr.length;
}
function addResize(acc, x, idx, arr) {
if(idx == 0) {
arr.length = 3;
}
return acc + x;
}
var delArray = [1, 2, 3, 4, 5];
delete delArray[0];
delete delArray[1];
delete delArray[3];
"#;
forward(&mut engine, init);

// empty array
let result = forward(&mut engine, "[].reduce(add, 0)");
assert_eq!(result, "0");

// simple with initial value
let result = forward(&mut engine, "arr.reduce(add, 0)");
assert_eq!(result, "10");

// without initial value
let result = forward(&mut engine, "arr.reduce(add)");
assert_eq!(result, "10");

// with some items missing
let result = forward(&mut engine, "delArray.reduce(add, 0)");
assert_eq!(result, "8");

// with index
let result = forward(&mut engine, "arr.reduce(addIdx, 0)");
assert_eq!(result, "6");

// with array
let result = forward(&mut engine, "arr.reduce(addLen, 0)");
assert_eq!(result, "16");

// resizing the array as reduce progresses
let result = forward(&mut engine, "arr.reduce(addResize, 0)");
assert_eq!(result, "6");

// Empty array
let result = forward(
&mut engine,
r#"
try {
[].reduce((acc, x) => acc + x);
} catch(e) {
e.message
}
"#,
);
assert_eq!(
result,
"Reduce was called on an empty array and with no initial value"
);

// Array with no defined elements
let result = forward(
&mut engine,
r#"
try {
var arr = [0, 1];
delete arr[0];
delete arr[1];
arr.reduce((acc, x) => acc + x);
} catch(e) {
e.message
}
"#,
);
assert_eq!(
result,
"Reduce was called on an empty array and with no initial value"
);

// No callback
let result = forward(
&mut engine,
r#"
try {
arr.reduce("");
} catch(e) {
e.message
}
"#,
);
assert_eq!(result, "Reduce was called without a callback");
}

#[test]
fn call_array_constructor_with_one_argument() {
let realm = Realm::create();
Expand Down

0 comments on commit 3e2e566

Please sign in to comment.