Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion core/engine/src/builtins/iterable/iterator_prototype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ impl IntrinsicObject for Iterator {
);

#[cfg(feature = "experimental")]
let builder = builder.static_method(Self::includes, js_string!("includes"), 1);
let builder = builder
.static_method(Self::includes, js_string!("includes"), 1)
.static_method(Self::join, js_string!("join"), 1);

builder.build();
}
Expand Down Expand Up @@ -511,6 +513,74 @@ impl Iterator {
Ok(false.into())
}

/// `Iterator.prototype.join ( [ separator ] )`
///
/// The `join()` method of `Iterator` instances returns a new string by concatenating
/// all of the elements in this iterator, separated by commas or a specified separator string.
/// If the iterator has only one item, then that item will be returned without using the separator.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/proposal-iterator-join/
#[cfg(feature = "experimental")]
fn join(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 2. If O is not an Object, throw a TypeError exception.
let o = this
.as_object()
.ok_or_else(|| js_error!(TypeError: "Iterator.prototype.join called on non-object"))?;

// 3. Let iterated be the Iterator Record { [[Iterator]]: O, [[NextMethod]]: undefined, [[Done]]: false }.
let iterated = IteratorRecord::new(o.clone(), JsValue::undefined());

// 4. If separator is undefined, let sep be ",".
// Else,
// a. Let sep be Completion(ToString(separator)).
// b. ? IfAbruptCloseIterator(sep, iterated).
let separator = args.get_or_undefined(0);
let sep = if separator.is_undefined() {
js_string!(",")
} else {
let sep = separator.to_string(context);
if_abrupt_close_iterator!(sep, iterated, context)
};

// 5. Set iterated to ? GetIteratorDirect(O).
let mut iterated = get_iterator_direct(&o, context)?;

// 6. Let R be the empty String.
// 7. Let first be true.
let mut r = crate::string::CommonJsStringBuilder::new();
let mut first = true;

// 8. Repeat,
while let Some(value) = iterated.step_value(context)? {
// a. Let value be ? IteratorStepValue(iterated).
// b. If value is done, return R.
// c. If first is true, set first to false.
// d. Else, set R to the string-concatenation of R and sep.
if first {
first = false;
} else {
r.push(sep.clone());
}

// e. If value is neither undefined nor null, then
if !value.is_null_or_undefined() {
// i. Let S be Completion(ToString(value)).
// ii. ? IfAbruptCloseIterator(S, iterated).
// iii. Set R to the string-concatenation of R and S.
let s = value.to_string(context);
let s = if_abrupt_close_iterator!(s, iterated, context);
r.push(s);
}
}

// 9. Return R.
Ok(r.build().into())
}

/// `Iterator.prototype.reduce ( reducer [ , initialValue ] )`
///
/// More information:
Expand Down
64 changes: 64 additions & 0 deletions core/engine/src/builtins/iterable/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -502,3 +502,67 @@ fn iterator_includes_errors() {
),
]);
}

#[test]
#[cfg(feature = "experimental")]
fn iterator_join_basic() {
run_test_actions([
TestAction::run("const gen = () => Iterator.from(['a', 'b', 'c']);"),
TestAction::assert_eq("gen().join()", js_str!("a,b,c")),
TestAction::assert_eq("gen().join('-')", js_str!("a-b-c")),
TestAction::assert_eq("gen().join('')", js_str!("abc")),
TestAction::assert_eq("gen().join('---')", js_str!("a---b---c")),
TestAction::assert_eq("Iterator.from([]).join()", js_str!("")),
TestAction::assert_eq("Iterator.from(['single']).join()", js_str!("single")),
TestAction::assert_eq("Iterator.from(['single']).join('-')", js_str!("single")),
]);
}

#[test]
#[cfg(feature = "experimental")]
fn iterator_join_generator() {
run_test_actions([
TestAction::run("function* gen() { yield 1; yield 2; yield 3; }"),
TestAction::assert_eq("gen().join()", js_str!("1,2,3")),
TestAction::assert_eq("gen().join(' + ')", js_str!("1 + 2 + 3")),
TestAction::assert_eq("gen().take(2).join()", js_str!("1,2")),
TestAction::assert_eq("gen().drop(1).join()", js_str!("2,3")),
]);
}

#[test]
#[cfg(feature = "experimental")]
fn iterator_join_nullish_elements() {
run_test_actions([
TestAction::assert_eq(
"Iterator.from(['one', null, 'two', undefined]).join()",
js_str!("one,,two,"),
),
TestAction::assert_eq(
"Iterator.from(['one', null, 'two', undefined, 'three']).join('-')",
js_str!("one--two--three"),
),
]);
}

#[test]
#[cfg(feature = "experimental")]
fn iterator_join_errors() {
run_test_actions([
TestAction::assert_native_error(
"Iterator.prototype.join.call(null)",
JsNativeErrorKind::Type,
"Iterator.prototype.join called on non-object",
),
TestAction::assert_native_error(
"Iterator.prototype.join.call(undefined)",
JsNativeErrorKind::Type,
"Iterator.prototype.join called on non-object",
),
TestAction::assert_native_error(
"Iterator.prototype.join.call(123)",
JsNativeErrorKind::Type,
"Iterator.prototype.join called on non-object",
),
]);
}
4 changes: 0 additions & 4 deletions test262_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ features = [
# https://github.com/tc39/proposal-iterator-chunking
"iterator-chunking",

# Iterator Join
# https://github.com/tc39/proposal-iterator-join
"Iterator.prototype.join",

# Error Stack Accessor
# https://github.com/tc39/proposal-error-stack-accessor
"error-stack-accessor",
Expand Down
Loading