Skip to content

feat(collections): stabilize Iterable input for chunk, dropLastWhile, dropWhile, intersect, sample, slidingWindows, sortBy, takeLastWhile, takeWhile, and withoutAll #6644

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

Merged
merged 14 commits into from
May 9, 2025
Merged
35 changes: 26 additions & 9 deletions collections/chunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
// This module is browser compatible.

/**
* Splits the given array into chunks of the given size and returns them.
* Splits the given array into an array of chunks of the given size and returns them.
*
* @typeParam T Type of the elements in the input array.
* @typeParam T The type of the elements in the iterable.
*
* @param array The array to split into chunks.
* @param iterable The iterable to take elements from.
* @param size The size of the chunks. This must be a positive integer.
*
* @returns An array of chunks of the given size.
Expand Down Expand Up @@ -37,20 +37,37 @@
* );
* ```
*/
export function chunk<T>(array: readonly T[], size: number): T[][] {
export function chunk<T>(
iterable: Iterable<T>,
size: number,
): T[][] {
if (size <= 0 || !Number.isInteger(size)) {
throw new RangeError(
`Expected size to be an integer greater than 0 but found ${size}`,
);
}

const result: T[][] = [];
let index = 0;

while (index < array.length) {
result.push(array.slice(index, index + size));
index += size;
// Faster path
if (Array.isArray(iterable)) {
let index = 0;
while (index < iterable.length) {
result.push(iterable.slice(index, index + size));
index += size;
}
return result;
}

let chunk: T[] = [];
for (const item of iterable) {
chunk.push(item);
if (chunk.length === size) {
result.push(chunk);
chunk = [];
}
}
if (chunk.length > 0) {
result.push(chunk);
}
return result;
}
112 changes: 111 additions & 1 deletion collections/chunk_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function chunkTest<I>(
const testArray = [1, 2, 3, 4, 5, 6];

Deno.test({
name: "chunk() handles no mutation",
name: "chunk() does not mutate the input",
fn() {
const array = [1, 2, 3, 4];
chunk(array, 2);
Expand Down Expand Up @@ -120,3 +120,113 @@ Deno.test({
);
},
});

Deno.test("chunk() handles a generator", () => {
function* gen() {
yield "a";
yield "b";
yield "c";
yield "d";
}
assertEquals(chunk(gen(), 1), [["a"], ["b"], ["c"], ["d"]], "size = 1");
assertEquals(chunk(gen(), 2), [["a", "b"], ["c", "d"]], "size = 2");
assertEquals(chunk(gen(), 3), [["a", "b", "c"], ["d"]], "size = 3");
assertEquals(chunk(gen(), 4), [["a", "b", "c", "d"]], "size = gen.length");
assertEquals(chunk(gen(), 5), [["a", "b", "c", "d"]], "size > gen.length");
});

Deno.test("chunk() handles a string", () => {
assertEquals(chunk("abcdefg", 4), [
["a", "b", "c", "d"],
["e", "f", "g"],
]);
});

Deno.test("chunk() handles a Set", () => {
const set = new Set([1, 2, 3, 4, 5, 6]);
assertEquals(chunk(set, 2), [
[1, 2],
[3, 4],
[5, 6],
]);
});

Deno.test("chunk() handles a Map", () => {
const map = new Map([
["a", 1],
["b", 2],
["c", 3],
["d", 4],
["e", 5],
["f", 6],
]);
assertEquals(chunk(map, 2), [
[
["a", 1],
["b", 2],
],
[
["c", 3],
["d", 4],
],
[
["e", 5],
["f", 6],
],
]);
});

Deno.test("chunk() handles user-defined iterable", () => {
class MyIterable {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
yield 6;
}
}
assertEquals(chunk(new MyIterable(), 2), [
[1, 2],
[3, 4],
[5, 6],
]);
});

Deno.test("chunk() handles a TypedArrays", () => {
const typedArrays = [
Uint8Array,
Uint8ClampedArray,
Uint16Array,
Uint32Array,
Int8Array,
Int16Array,
Int32Array,
Float32Array,
Float64Array,
];
for (const TypedArray of typedArrays) {
const array = new TypedArray([1, 2, 3, 4, 5, 6]);
assertEquals(chunk(array, 2), [
[1, 2],
[3, 4],
[5, 6],
]);
}
});

Deno.test("chunk() handles an array with empty slots", () => {
// Regression test for chunk that only allowed Array instances instead of any Iterable.
// This is a special case where an array is filled with empty slots which is a different kind of nothing than null or undefined
// Typed arrays are not affected, as they are filled with 0 instead of empty slots
const arr = new Array(4);
arr[2] = 3;

const expectedSecondChunk = new Array(2);
expectedSecondChunk[0] = 3;
assertEquals(chunk(arr, 2), [
new Array(2),
expectedSecondChunk,
]);
});
10 changes: 0 additions & 10 deletions collections/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,7 @@
"./take-last-while": "./take_last_while.ts",
"./take-while": "./take_while.ts",
"./union": "./union.ts",
"./unstable-chunk": "./unstable_chunk.ts",
"./unstable-cycle": "./unstable_cycle.ts",
"./unstable-drop-while": "./unstable_drop_while.ts",
"./unstable-drop-last-while": "./unstable_drop_last_while.ts",
"./unstable-intersect": "./unstable_intersect.ts",
"./unstable-sample": "./unstable_sample.ts",
"./unstable-sliding-windows": "./unstable_sliding_windows.ts",
"./unstable-sort-by": "./unstable_sort_by.ts",
"./unstable-take-last-while": "./unstable_take_last_while.ts",
"./unstable-take-while": "./unstable_take_while.ts",
"./unstable-without-all": "./unstable_without_all.ts",
"./unzip": "./unzip.ts",
"./without-all": "./without_all.ts",
"./zip": "./zip.ts"
Expand Down
20 changes: 11 additions & 9 deletions collections/drop_last_while.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
// This module is browser compatible.

/**
* Returns a new array that drops all elements in the given collection until the
* Returns an array that drops all elements in the given iterable until the
* last element that does not match the given predicate.
*
* @typeParam T The type of the elements in the input array.
* @typeParam T The type of the elements in the input iterable.
*
* @param array The array to drop elements from.
* @param iterable The iterable to drop elements from.
* @param predicate The function to test each element for a condition.
*
* @returns A new array that drops all elements until the last element that does
* @returns An array that drops all elements until the last element that does
* not match the given predicate.
*
* @example Basic usage
Expand All @@ -26,11 +26,13 @@
* ```
*/
export function dropLastWhile<T>(
array: readonly T[],
iterable: Iterable<T>,
predicate: (el: T) => boolean,
): T[] {
let offset = array.length;
while (0 < offset && predicate(array[offset - 1] as T)) offset--;

return array.slice(0, offset);
const array = Array.isArray(iterable) ? iterable : Array.from(iterable);
let offset = array.length - 1;
while (offset >= 0 && predicate(array[offset]!)) {
offset--;
}
return array.slice(0, offset + 1);
}
36 changes: 33 additions & 3 deletions collections/drop_last_while_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ Deno.test("dropLastWhile() handles num array", () => {
assertEquals(actual, [20]);
});

Deno.test("dropLastWhile() handles no mutation", () => {
Deno.test("dropLastWhile() does not mutate the input array", () => {
const array = [1, 2, 3, 4, 5, 6];

const actual = dropLastWhile(array, (i) => i > 4);

assertEquals(actual, [1, 2, 3, 4]);
assertEquals(array, [1, 2, 3, 4, 5, 6]);
});
Expand Down Expand Up @@ -50,3 +48,35 @@ Deno.test("dropLastWhile() returns empty array when all elements get dropped", (

assertEquals(actual, []);
});

Deno.test("dropLastWhile() handles a string", () => {
const values = "hello there world";
const actual = dropLastWhile(values, (i) => i !== " ");
assertEquals(actual, "hello there ".split(""));
});

Deno.test("dropLastWhile() handles a Set", () => {
const values = new Set([20, 33, 44]);
const actual = dropLastWhile(values, (i) => i > 30);
assertEquals(actual, [20]);
});

Deno.test("dropLastWhile() handles a Map", () => {
const values = new Map([
["a", 20],
["b", 33],
["c", 44],
]);
const actual = dropLastWhile(values, ([_k, v]) => v > 30);
assertEquals(actual, [["a", 20]]);
});

Deno.test("dropLastWhile() handles a generator", () => {
function* gen() {
yield 20;
yield 33;
yield 44;
}
const actual = dropLastWhile(gen(), (i) => i > 30);
assertEquals(actual, [20]);
});
32 changes: 20 additions & 12 deletions collections/drop_while.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
// This module is browser compatible.

/**
* Returns a new array that drops all elements in the given collection until the
* Returns an array that drops all elements in the given iterable until the
* first element that does not match the given predicate.
*
* @typeParam T The type of the elements in the input array.
* @typeParam T The type of the elements in the input iterable.
*
* @param array The array to drop elements from.
* @param iterable The iterable to drop elements from.
* @param predicate The function to test each element for a condition.
*
* @returns A new array that drops all elements until the first element that
* @returns An array that drops all elements until the first element that
* does not match the given predicate.
*
* @example Basic usage
Expand All @@ -25,15 +25,23 @@
* ```
*/
export function dropWhile<T>(
array: readonly T[],
iterable: Iterable<T>,
predicate: (el: T) => boolean,
): T[] {
let offset = 0;
const length = array.length;

while (length > offset && predicate(array[offset] as T)) {
offset++;
if (Array.isArray(iterable)) {
const idx = iterable.findIndex((el) => !predicate(el));
if (idx === -1) {
return [];
}
return iterable.slice(idx);
}

return array.slice(offset, length);
const array: T[] = [];
let found = false;
for (const item of iterable) {
if (found || !predicate(item)) {
found = true;
array.push(item);
}
}
return array;
}
Loading
Loading