Skip to content

Commit f129d5e

Browse files
Merge pull request #1 from jhonsnow456/feat/collections
feat(collections): add groupBy, shuffle, range, difference, flatten, countBy
2 parents ad7c87b + 0933eb9 commit f129d5e

14 files changed

Lines changed: 513 additions & 0 deletions

collections/count_by.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
// This module is browser compatible.
3+
4+
/**
5+
* Counts the occurrences of each key returned by the given function.
6+
*
7+
* @typeParam T The type of the array elements
8+
* @typeParam K The type of the keys
9+
*
10+
* @param array The array to count
11+
* @param keyFn The function that returns the key for each element
12+
*
13+
* @returns An object mapping keys to their counts
14+
*
15+
* @example Basic usage
16+
* ```ts
17+
* import { countBy } from "@std/collections/count-by";
18+
* import { assertEquals } from "@std/assert";
19+
*
20+
* const pets = [
21+
* { type: "dog", name: "Fido" },
22+
* { type: "cat", name: "Whiskers" },
23+
* { type: "dog", name: "Rover" },
24+
* ];
25+
*
26+
* const counts = countBy(pets, (pet) => pet.type);
27+
* assertEquals(counts, { dog: 2, cat: 1 });
28+
* ```
29+
*/
30+
export function countBy<T, K extends PropertyKey>(
31+
array: readonly T[],
32+
keyFn: (element: T) => K,
33+
): Record<K, number> {
34+
const result = {} as Record<K, number>;
35+
for (const element of array) {
36+
const key = keyFn(element);
37+
if (Object.prototype.hasOwnProperty.call(result, key)) {
38+
result[key]!++;
39+
} else {
40+
result[key] = 1;
41+
}
42+
}
43+
return result;
44+
}

collections/count_by_test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
import { assertEquals } from "@std/assert";
3+
import { countBy } from "./count_by.ts";
4+
5+
Deno.test({
6+
name: "countBy() counts elements by key",
7+
fn() {
8+
const pets = [
9+
{ type: "dog", name: "Fido" },
10+
{ type: "cat", name: "Whiskers" },
11+
{ type: "dog", name: "Rover" },
12+
];
13+
assertEquals(countBy(pets, (pet) => pet.type), { dog: 2, cat: 1 });
14+
},
15+
});
16+
17+
Deno.test({
18+
name: "countBy() handles empty array",
19+
fn() {
20+
assertEquals(countBy([], (x: number) => x % 2), {});
21+
},
22+
});
23+
24+
Deno.test({
25+
name: "countBy() counts numbers by parity",
26+
fn() {
27+
assertEquals(
28+
countBy([1, 2, 3, 4, 5, 6], (n) => n % 2 === 0 ? "even" : "odd"),
29+
{ odd: 3, even: 3 },
30+
);
31+
},
32+
});

collections/deno.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"./associate-by": "./associate_by.ts",
88
"./associate-with": "./associate_with.ts",
99
"./chunk": "./chunk.ts",
10+
"./count-by": "./count_by.ts",
1011
"./deep-merge": "./deep_merge.ts",
12+
"./difference": "./difference.ts",
1113
"./distinct": "./distinct.ts",
1214
"./distinct-by": "./distinct_by.ts",
1315
"./drop-last-while": "./drop_last_while.ts",
@@ -16,6 +18,8 @@
1618
"./filter-keys": "./filter_keys.ts",
1719
"./filter-values": "./filter_values.ts",
1820
"./find-single": "./find_single.ts",
21+
"./flatten": "./flatten.ts",
22+
"./group-by": "./group_by.ts",
1923
"./first-not-nullish-of": "./first_not_nullish_of.ts",
2024
"./includes-value": "./includes_value.ts",
2125
"./interleave": "./interleave.ts",
@@ -38,9 +42,11 @@
3842
"./partition-entries": "./partition_entries.ts",
3943
"./permutations": "./permutations.ts",
4044
"./pick": "./pick.ts",
45+
"./range": "./range.ts",
4146
"./reduce-groups": "./reduce_groups.ts",
4247
"./running-reduce": "./running_reduce.ts",
4348
"./sample": "./sample.ts",
49+
"./shuffle": "./shuffle.ts",
4450
"./sliding-windows": "./sliding_windows.ts",
4551
"./sort-by": "./sort_by.ts",
4652
"./sum-of": "./sum_of.ts",

collections/difference.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
// This module is browser compatible.
3+
4+
/**
5+
* Returns an array of elements that are in the first array but not in the
6+
* second.
7+
*
8+
* @typeParam T The type of the array elements
9+
*
10+
* @param a The array to check for elements
11+
* @param b The array whose elements should be excluded
12+
*
13+
* @returns A new array with elements from `a` not in `b`
14+
*
15+
* @example Basic usage
16+
* ```ts
17+
* import { difference } from "@std/collections/difference";
18+
* import { assertEquals } from "@std/assert";
19+
*
20+
* const a = [1, 2, 3, 4];
21+
* const b = [2, 4];
22+
*
23+
* assertEquals(difference(a, b), [1, 3]);
24+
* ```
25+
*/
26+
export function difference<T>(a: readonly T[], b: readonly T[]): T[] {
27+
const exclude = new Set(b);
28+
return a.filter((item) => !exclude.has(item));
29+
}

collections/difference_test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
import { assertEquals } from "@std/assert";
3+
import { difference } from "./difference.ts";
4+
5+
Deno.test({
6+
name: "difference() returns elements in a not in b",
7+
fn() {
8+
assertEquals(difference([1, 2, 3, 4], [2, 4]), [1, 3]);
9+
},
10+
});
11+
12+
Deno.test({
13+
name: "difference() returns empty array when all elements are excluded",
14+
fn() {
15+
assertEquals(difference([1, 2], [1, 2]), []);
16+
},
17+
});
18+
19+
Deno.test({
20+
name: "difference() returns all elements when no overlap",
21+
fn() {
22+
assertEquals(difference([1, 2], [3, 4]), [1, 2]);
23+
},
24+
});
25+
26+
Deno.test({
27+
name: "difference() handles empty arrays",
28+
fn() {
29+
assertEquals(difference([], [1, 2]), []);
30+
assertEquals(difference([1, 2], []), [1, 2]);
31+
},
32+
});
33+
34+
Deno.test({
35+
name: "difference() handles duplicates in input",
36+
fn() {
37+
assertEquals(difference([1, 1, 2, 2, 3], [2]), [1, 1, 3]);
38+
},
39+
});

collections/flatten.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
// This module is browser compatible.
3+
4+
/**
5+
* Flattens an array of arrays into a single array (one level deep).
6+
*
7+
* @typeParam T The type of the inner array elements
8+
*
9+
* @param array The array of arrays to flatten
10+
*
11+
* @returns A new flattened array
12+
*
13+
* @example Basic usage
14+
* ```ts
15+
* import { flatten } from "@std/collections/flatten";
16+
* import { assertEquals } from "@std/assert";
17+
*
18+
* const nested = [[1, 2], [3, 4], [5]];
19+
* assertEquals(flatten(nested), [1, 2, 3, 4, 5]);
20+
* ```
21+
*/
22+
export function flatten<T>(array: readonly (readonly T[])[]): T[] {
23+
const result: T[] = [];
24+
for (const inner of array) {
25+
for (const element of inner) {
26+
result.push(element);
27+
}
28+
}
29+
return result;
30+
}

collections/flatten_test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
import { assertEquals } from "@std/assert";
3+
import { flatten } from "./flatten.ts";
4+
5+
Deno.test({
6+
name: "flatten() flattens nested arrays one level",
7+
fn() {
8+
assertEquals(
9+
flatten([[1, 2], [3, 4], [5]]),
10+
[1, 2, 3, 4, 5],
11+
);
12+
},
13+
});
14+
15+
Deno.test({
16+
name: "flatten() handles empty subarrays",
17+
fn() {
18+
assertEquals(flatten([[1], [], [2]]), [1, 2]);
19+
},
20+
});
21+
22+
Deno.test({
23+
name: "flatten() handles empty outer array",
24+
fn() {
25+
assertEquals(flatten([]), []);
26+
},
27+
});
28+
29+
Deno.test({
30+
name: "flatten() does not deep flatten",
31+
fn() {
32+
const result = flatten([[[1]], [[2]]]);
33+
assertEquals(result, [[1], [2]]);
34+
},
35+
});

collections/group_by.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
// This module is browser compatible.
3+
4+
/**
5+
* Groups elements of an array by a key returned by the given function.
6+
*
7+
* @typeParam T The type of the array elements
8+
* @typeParam K The type of the group keys
9+
*
10+
* @param array The array to group
11+
* @param keyFn The function that returns the key for each element
12+
*
13+
* @returns An object mapping keys to arrays of elements
14+
*
15+
* @example Basic usage
16+
* ```ts
17+
* import { groupBy } from "@std/collections/group-by";
18+
* import { assertEquals } from "@std/assert";
19+
*
20+
* const pets = [
21+
* { type: "dog", name: "Fido" },
22+
* { type: "cat", name: "Whiskers" },
23+
* { type: "dog", name: "Rover" },
24+
* ];
25+
*
26+
* const grouped = groupBy(pets, (pet) => pet.type);
27+
* assertEquals(grouped, {
28+
* dog: [{ type: "dog", name: "Fido" }, { type: "dog", name: "Rover" }],
29+
* cat: [{ type: "cat", name: "Whiskers" }],
30+
* });
31+
* ```
32+
*/
33+
export function groupBy<T, K extends PropertyKey>(
34+
array: readonly T[],
35+
keyFn: (element: T) => K,
36+
): Record<K, T[]> {
37+
const result = {} as Record<K, T[]>;
38+
for (const element of array) {
39+
const key = keyFn(element);
40+
if (Object.prototype.hasOwnProperty.call(result, key)) {
41+
result[key]!.push(element);
42+
} else {
43+
result[key] = [element];
44+
}
45+
}
46+
return result;
47+
}

collections/group_by_test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
import { assertEquals } from "@std/assert";
3+
import { groupBy } from "./group_by.ts";
4+
5+
Deno.test({
6+
name: "groupBy() groups elements by key function",
7+
fn() {
8+
const pets = [
9+
{ type: "dog", name: "Fido" },
10+
{ type: "cat", name: "Whiskers" },
11+
{ type: "dog", name: "Rover" },
12+
];
13+
const grouped = groupBy(pets, (pet) => pet.type);
14+
assertEquals(grouped, {
15+
dog: [
16+
{ type: "dog", name: "Fido" },
17+
{ type: "dog", name: "Rover" },
18+
],
19+
cat: [{ type: "cat", name: "Whiskers" }],
20+
});
21+
},
22+
});
23+
24+
Deno.test({
25+
name: "groupBy() handles empty array",
26+
fn() {
27+
assertEquals(groupBy([], (x: number) => x % 2), {});
28+
},
29+
});
30+
31+
Deno.test({
32+
name: "groupBy() groups numbers by parity",
33+
fn() {
34+
const result = groupBy([1, 2, 3, 4, 5, 6], (n) => n % 2 === 0 ? "even" : "odd");
35+
assertEquals(result, { odd: [1, 3, 5], even: [2, 4, 6] });
36+
},
37+
});

collections/mod.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,20 @@ export * from "./aggregate_groups.ts";
3232
export * from "./associate_by.ts";
3333
export * from "./associate_with.ts";
3434
export * from "./chunk.ts";
35+
export * from "./count_by.ts";
3536
export * from "./deep_merge.ts";
37+
export * from "./difference.ts";
3638
export * from "./distinct.ts";
3739
export * from "./distinct_by.ts";
3840
export * from "./drop_last_while.ts";
3941
export * from "./drop_while.ts";
4042
export * from "./filter_entries.ts";
4143
export * from "./filter_keys.ts";
4244
export * from "./filter_values.ts";
45+
export * from "./flatten.ts";
4346
export * from "./find_single.ts";
4447
export * from "./first_not_nullish_of.ts";
48+
export * from "./group_by.ts";
4549
export * from "./includes_value.ts";
4650
export * from "./interleave.ts";
4751
export * from "./intersect.ts";
@@ -63,9 +67,11 @@ export * from "./partition.ts";
6367
export * from "./partition_entries.ts";
6468
export * from "./permutations.ts";
6569
export * from "./pick.ts";
70+
export * from "./range.ts";
6671
export * from "./reduce_groups.ts";
6772
export * from "./running_reduce.ts";
6873
export * from "./sample.ts";
74+
export * from "./shuffle.ts";
6975
export * from "./sliding_windows.ts";
7076
export * from "./sort_by.ts";
7177
export * from "./sum_of.ts";

0 commit comments

Comments
 (0)