Skip to content

Commit c19c168

Browse files
authored
Add an Object.groupBy TypeScript goody (#519)
Closes #363.
1 parent 5e2da1e commit c19c168

4 files changed

Lines changed: 270 additions & 0 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { describe, expect, it } from "@jest/globals";
2+
3+
delete (Object as unknown as Record<string, unknown>).groupBy;
4+
// eslint-disable-next-line import-x/first -- This has to happen after we delete the built-in implementation.
5+
import "./index.ts";
6+
7+
describe("Object.groupBy", () => {
8+
it("groups elements by the result of the callback function", () => {
9+
const words = ["zero", "one", "two", "three", "four", "five", "six"];
10+
const result = Object.groupBy(words, (word) => String(word.length));
11+
12+
expect(result).toStrictEqual({
13+
"4": ["zero", "four", "five"],
14+
"3": ["one", "two", "six"],
15+
"5": ["three"],
16+
});
17+
});
18+
19+
it("allows duplicate elements", () => {
20+
const numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9];
21+
const result = Object.groupBy(numbers, (num) =>
22+
num % 2 === 0 ? "even" : "odd",
23+
);
24+
25+
expect(result).toStrictEqual({
26+
odd: [3, 1, 1, 5, 9, 5, 3, 5, 9, 7, 9],
27+
even: [4, 2, 6, 8],
28+
});
29+
});
30+
31+
it("respects key order", () => {
32+
const words = [
33+
"zero",
34+
"one",
35+
"two",
36+
"three",
37+
"four",
38+
"five",
39+
"six",
40+
"seven",
41+
"eight",
42+
"nine",
43+
"ten",
44+
];
45+
const result = Object.groupBy(
46+
words,
47+
(word) => word.replace(/[aeiou]/gi, "").length + "!",
48+
);
49+
50+
expect(Object.keys(result)).toStrictEqual(["2!", "1!", "3!"]);
51+
});
52+
53+
it.each([
54+
[],
55+
[].values(),
56+
new Set().values(),
57+
new Map().entries(),
58+
""[Symbol.iterator](),
59+
(function* () {})(),
60+
])("handles empty iterables", (iterable) => {
61+
const result = Object.groupBy(iterable, String);
62+
63+
expect(result).toStrictEqual({});
64+
});
65+
66+
it("can use the index in the callback function", () => {
67+
const letters = ["a", "b", "c", "d"];
68+
const result = Object.groupBy(letters, (_, index) =>
69+
index % 2 === 0 ? "even" : "odd",
70+
);
71+
72+
expect(result).toStrictEqual({
73+
even: ["a", "c"],
74+
odd: ["b", "d"],
75+
});
76+
});
77+
78+
it("works with a Set", () => {
79+
const set = new Set(["apple", "banana", "cherry", "apple"]);
80+
const result = Object.groupBy(set, (fruit) => fruit[1]);
81+
82+
expect(result).toStrictEqual({
83+
p: ["apple"],
84+
a: ["banana"],
85+
h: ["cherry"],
86+
});
87+
});
88+
89+
it("works with a Map", () => {
90+
const map = new Map([
91+
["a", 1],
92+
["b", 2],
93+
["c", 3],
94+
]);
95+
const result = Object.groupBy(map, ([, value]) =>
96+
value % 2 === 0 ? "even" : "odd",
97+
);
98+
99+
expect(result).toStrictEqual({
100+
even: [["b", 2]],
101+
odd: [
102+
["a", 1],
103+
["c", 3],
104+
],
105+
});
106+
});
107+
108+
it("works with a generator", () => {
109+
const generator = (function* () {
110+
yield 3;
111+
yield 1;
112+
yield 4;
113+
})();
114+
const result = Object.groupBy(generator, (num) =>
115+
num % 2 === 0 ? "even" : "odd",
116+
);
117+
118+
expect(result).toStrictEqual({
119+
odd: [3, 1],
120+
even: [4],
121+
});
122+
});
123+
124+
it("works with a custom iterable", () => {
125+
const iterable = {
126+
[Symbol.iterator]() {
127+
let count = 0;
128+
return {
129+
next: () =>
130+
count < 3 ? { value: ++count } : { done: true, value: undefined },
131+
};
132+
},
133+
} as Iterable<number>;
134+
135+
const result = Object.groupBy(iterable, (num) =>
136+
num % 2 === 0 ? "even" : "odd",
137+
);
138+
139+
expect(result).toStrictEqual({
140+
odd: [1, 3],
141+
even: [2],
142+
});
143+
});
144+
145+
it("throws for non-iterable arguments", () => {
146+
expect(() => {
147+
// @ts-expect-error Invalid argument.
148+
Object.groupBy(123, String);
149+
}).toThrow(TypeError);
150+
});
151+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
declare global {
2+
interface ObjectConstructor {
3+
groupBy<V>(
4+
iterable: Iterable<V>,
5+
callbackFn: (value: V, index: number) => string,
6+
): Partial<Record<string, V[]>>;
7+
}
8+
}
9+
10+
Object.groupBy ??= function <V>(
11+
iterable: Iterable<V>,
12+
callbackFn: (value: V, index: number) => string,
13+
): Partial<Record<string, V[]>> {
14+
const groups: Partial<Record<string, V[]>> = {};
15+
16+
let index = 0;
17+
for (const value of iterable) {
18+
const key = callbackFn(value, index++);
19+
(groups[key] ??= []).push(value);
20+
}
21+
22+
return groups;
23+
};
24+
25+
// Needed to fix the error "Augmentations for the global scope can only be directly nested in external modules or ambient module declarations. ts(2669)"
26+
// See: https://stackoverflow.com/questions/57132428/augmentations-for-the-global-scope-can-only-be-directly-nested-in-external-modul
27+
export {};

workspaces/adventure-pack/src/app/__tests__/__snapshots__/equip-test.ts.snap

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2289,6 +2289,26 @@ Object.getUnsafe = function (obj, properties) {
22892289
/////////////////////////// END ADVENTURE PACK CODE ////////////////////////////"
22902290
`;
22912291

2292+
exports[`App can equip single goody: JavaScript Object.groupBy 1`] = `
2293+
"////////////////////////// BEGIN ADVENTURE PACK CODE ///////////////////////////
2294+
// Adventure Pack commit fake-commit-hash
2295+
// Running at: https://example.com/
2296+
2297+
Object.groupBy ??= function (iterable, callbackFn) {
2298+
const groups = {};
2299+
2300+
let index = 0;
2301+
for (const value of iterable) {
2302+
const key = callbackFn(value, index++);
2303+
(groups[key] ??= []).push(value);
2304+
}
2305+
2306+
return groups;
2307+
};
2308+
2309+
/////////////////////////// END ADVENTURE PACK CODE ////////////////////////////"
2310+
`;
2311+
22922312
exports[`App can equip single goody: JavaScript Object.prototype.entries 1`] = `
22932313
"////////////////////////// BEGIN ADVENTURE PACK CODE ///////////////////////////
22942314
// Adventure Pack commit fake-commit-hash
@@ -4923,6 +4943,38 @@ Object.getUnsafe = function (
49234943
/////////////////////////// END ADVENTURE PACK CODE ////////////////////////////"
49244944
`;
49254945

4946+
exports[`App can equip single goody: TypeScript Object.groupBy 1`] = `
4947+
"////////////////////////// BEGIN ADVENTURE PACK CODE ///////////////////////////
4948+
// Adventure Pack commit fake-commit-hash
4949+
// Running at: https://example.com/
4950+
4951+
declare global {
4952+
interface ObjectConstructor {
4953+
groupBy<V>(
4954+
iterable: Iterable<V>,
4955+
callbackFn: (value: V, index: number) => string,
4956+
): Partial<Record<string, V[]>>;
4957+
}
4958+
}
4959+
4960+
Object.groupBy ??= function <V>(
4961+
iterable: Iterable<V>,
4962+
callbackFn: (value: V, index: number) => string,
4963+
): Partial<Record<string, V[]>> {
4964+
const groups: Partial<Record<string, V[]>> = {};
4965+
4966+
let index = 0;
4967+
for (const value of iterable) {
4968+
const key = callbackFn(value, index++);
4969+
(groups[key] ??= []).push(value);
4970+
}
4971+
4972+
return groups;
4973+
};
4974+
4975+
/////////////////////////// END ADVENTURE PACK CODE ////////////////////////////"
4976+
`;
4977+
49264978
exports[`App can equip single goody: TypeScript Object.prototype.entries 1`] = `
49274979
"////////////////////////// BEGIN ADVENTURE PACK CODE ///////////////////////////
49284980
// Adventure Pack commit fake-commit-hash

workspaces/adventure-pack/src/app/__tests__/__snapshots__/render-test.ts.snap

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,6 +1263,20 @@ exports[`App can render goody: JavaScript Object.getUnsafe 1`] = `
12631263
};"
12641264
`;
12651265
1266+
exports[`App can render goody: JavaScript Object.groupBy 1`] = `
1267+
"Object.groupBy ??= function (iterable, callbackFn) {
1268+
const groups = {};
1269+
1270+
let index = 0;
1271+
for (const value of iterable) {
1272+
const key = callbackFn(value, index++);
1273+
(groups[key] ??= []).push(value);
1274+
}
1275+
1276+
return groups;
1277+
};"
1278+
`;
1279+
12661280
exports[`App can render goody: JavaScript Object.prototype.entries 1`] = `
12671281
"import "Iterator.prototype.map";
12681282
import "Object.prototype.keys";
@@ -2830,6 +2844,32 @@ Object.getUnsafe = function (
28302844
};"
28312845
`;
28322846
2847+
exports[`App can render goody: TypeScript Object.groupBy 1`] = `
2848+
"declare global {
2849+
interface ObjectConstructor {
2850+
groupBy<V>(
2851+
iterable: Iterable<V>,
2852+
callbackFn: (value: V, index: number) => string,
2853+
): Partial<Record<string, V[]>>;
2854+
}
2855+
}
2856+
2857+
Object.groupBy ??= function <V>(
2858+
iterable: Iterable<V>,
2859+
callbackFn: (value: V, index: number) => string,
2860+
): Partial<Record<string, V[]>> {
2861+
const groups: Partial<Record<string, V[]>> = {};
2862+
2863+
let index = 0;
2864+
for (const value of iterable) {
2865+
const key = callbackFn(value, index++);
2866+
(groups[key] ??= []).push(value);
2867+
}
2868+
2869+
return groups;
2870+
};"
2871+
`;
2872+
28332873
exports[`App can render goody: TypeScript Object.prototype.entries 1`] = `
28342874
"import "Iterator.prototype.map";
28352875
import "Object.prototype.keys";

0 commit comments

Comments
 (0)