diff --git a/workspaces/adventure-pack/goodies/typescript/Object.groupBy/index.test.ts b/workspaces/adventure-pack/goodies/typescript/Object.groupBy/index.test.ts new file mode 100644 index 00000000..0816ade1 --- /dev/null +++ b/workspaces/adventure-pack/goodies/typescript/Object.groupBy/index.test.ts @@ -0,0 +1,151 @@ +import { describe, expect, it } from "@jest/globals"; + +delete (Object as unknown as Record).groupBy; +// eslint-disable-next-line import-x/first -- This has to happen after we delete the built-in implementation. +import "./index.ts"; + +describe("Object.groupBy", () => { + it("groups elements by the result of the callback function", () => { + const words = ["zero", "one", "two", "three", "four", "five", "six"]; + const result = Object.groupBy(words, (word) => String(word.length)); + + expect(result).toStrictEqual({ + "4": ["zero", "four", "five"], + "3": ["one", "two", "six"], + "5": ["three"], + }); + }); + + it("allows duplicate elements", () => { + const numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]; + const result = Object.groupBy(numbers, (num) => + num % 2 === 0 ? "even" : "odd", + ); + + expect(result).toStrictEqual({ + odd: [3, 1, 1, 5, 9, 5, 3, 5, 9, 7, 9], + even: [4, 2, 6, 8], + }); + }); + + it("respects key order", () => { + const words = [ + "zero", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "ten", + ]; + const result = Object.groupBy( + words, + (word) => word.replace(/[aeiou]/gi, "").length + "!", + ); + + expect(Object.keys(result)).toStrictEqual(["2!", "1!", "3!"]); + }); + + it.each([ + [], + [].values(), + new Set().values(), + new Map().entries(), + ""[Symbol.iterator](), + (function* () {})(), + ])("handles empty iterables", (iterable) => { + const result = Object.groupBy(iterable, String); + + expect(result).toStrictEqual({}); + }); + + it("can use the index in the callback function", () => { + const letters = ["a", "b", "c", "d"]; + const result = Object.groupBy(letters, (_, index) => + index % 2 === 0 ? "even" : "odd", + ); + + expect(result).toStrictEqual({ + even: ["a", "c"], + odd: ["b", "d"], + }); + }); + + it("works with a Set", () => { + const set = new Set(["apple", "banana", "cherry", "apple"]); + const result = Object.groupBy(set, (fruit) => fruit[1]); + + expect(result).toStrictEqual({ + p: ["apple"], + a: ["banana"], + h: ["cherry"], + }); + }); + + it("works with a Map", () => { + const map = new Map([ + ["a", 1], + ["b", 2], + ["c", 3], + ]); + const result = Object.groupBy(map, ([, value]) => + value % 2 === 0 ? "even" : "odd", + ); + + expect(result).toStrictEqual({ + even: [["b", 2]], + odd: [ + ["a", 1], + ["c", 3], + ], + }); + }); + + it("works with a generator", () => { + const generator = (function* () { + yield 3; + yield 1; + yield 4; + })(); + const result = Object.groupBy(generator, (num) => + num % 2 === 0 ? "even" : "odd", + ); + + expect(result).toStrictEqual({ + odd: [3, 1], + even: [4], + }); + }); + + it("works with a custom iterable", () => { + const iterable = { + [Symbol.iterator]() { + let count = 0; + return { + next: () => + count < 3 ? { value: ++count } : { done: true, value: undefined }, + }; + }, + } as Iterable; + + const result = Object.groupBy(iterable, (num) => + num % 2 === 0 ? "even" : "odd", + ); + + expect(result).toStrictEqual({ + odd: [1, 3], + even: [2], + }); + }); + + it("throws for non-iterable arguments", () => { + expect(() => { + // @ts-expect-error Invalid argument. + Object.groupBy(123, String); + }).toThrow(TypeError); + }); +}); diff --git a/workspaces/adventure-pack/goodies/typescript/Object.groupBy/index.ts b/workspaces/adventure-pack/goodies/typescript/Object.groupBy/index.ts new file mode 100644 index 00000000..8c4369e3 --- /dev/null +++ b/workspaces/adventure-pack/goodies/typescript/Object.groupBy/index.ts @@ -0,0 +1,27 @@ +declare global { + interface ObjectConstructor { + groupBy( + iterable: Iterable, + callbackFn: (value: V, index: number) => string, + ): Partial>; + } +} + +Object.groupBy ??= function ( + iterable: Iterable, + callbackFn: (value: V, index: number) => string, +): Partial> { + const groups: Partial> = {}; + + let index = 0; + for (const value of iterable) { + const key = callbackFn(value, index++); + (groups[key] ??= []).push(value); + } + + return groups; +}; + +// Needed to fix the error "Augmentations for the global scope can only be directly nested in external modules or ambient module declarations. ts(2669)" +// See: https://stackoverflow.com/questions/57132428/augmentations-for-the-global-scope-can-only-be-directly-nested-in-external-modul +export {}; diff --git a/workspaces/adventure-pack/src/app/__tests__/__snapshots__/equip-test.ts.snap b/workspaces/adventure-pack/src/app/__tests__/__snapshots__/equip-test.ts.snap index f7a3641e..974d53d3 100644 --- a/workspaces/adventure-pack/src/app/__tests__/__snapshots__/equip-test.ts.snap +++ b/workspaces/adventure-pack/src/app/__tests__/__snapshots__/equip-test.ts.snap @@ -2289,6 +2289,26 @@ Object.getUnsafe = function (obj, properties) { /////////////////////////// END ADVENTURE PACK CODE ////////////////////////////" `; +exports[`App can equip single goody: JavaScript Object.groupBy 1`] = ` +"////////////////////////// BEGIN ADVENTURE PACK CODE /////////////////////////// +// Adventure Pack commit fake-commit-hash +// Running at: https://example.com/ + +Object.groupBy ??= function (iterable, callbackFn) { + const groups = {}; + + let index = 0; + for (const value of iterable) { + const key = callbackFn(value, index++); + (groups[key] ??= []).push(value); + } + + return groups; +}; + +/////////////////////////// END ADVENTURE PACK CODE ////////////////////////////" +`; + exports[`App can equip single goody: JavaScript Object.prototype.entries 1`] = ` "////////////////////////// BEGIN ADVENTURE PACK CODE /////////////////////////// // Adventure Pack commit fake-commit-hash @@ -4898,6 +4918,38 @@ Object.getUnsafe = function ( /////////////////////////// END ADVENTURE PACK CODE ////////////////////////////" `; +exports[`App can equip single goody: TypeScript Object.groupBy 1`] = ` +"////////////////////////// BEGIN ADVENTURE PACK CODE /////////////////////////// +// Adventure Pack commit fake-commit-hash +// Running at: https://example.com/ + +declare global { + interface ObjectConstructor { + groupBy( + iterable: Iterable, + callbackFn: (value: V, index: number) => string, + ): Partial>; + } +} + +Object.groupBy ??= function ( + iterable: Iterable, + callbackFn: (value: V, index: number) => string, +): Partial> { + const groups: Partial> = {}; + + let index = 0; + for (const value of iterable) { + const key = callbackFn(value, index++); + (groups[key] ??= []).push(value); + } + + return groups; +}; + +/////////////////////////// END ADVENTURE PACK CODE ////////////////////////////" +`; + exports[`App can equip single goody: TypeScript Object.prototype.entries 1`] = ` "////////////////////////// BEGIN ADVENTURE PACK CODE /////////////////////////// // Adventure Pack commit fake-commit-hash diff --git a/workspaces/adventure-pack/src/app/__tests__/__snapshots__/render-test.ts.snap b/workspaces/adventure-pack/src/app/__tests__/__snapshots__/render-test.ts.snap index f0a6e8f0..563003a2 100644 --- a/workspaces/adventure-pack/src/app/__tests__/__snapshots__/render-test.ts.snap +++ b/workspaces/adventure-pack/src/app/__tests__/__snapshots__/render-test.ts.snap @@ -1263,6 +1263,20 @@ exports[`App can render goody: JavaScript Object.getUnsafe 1`] = ` };" `; +exports[`App can render goody: JavaScript Object.groupBy 1`] = ` +"Object.groupBy ??= function (iterable, callbackFn) { + const groups = {}; + + let index = 0; + for (const value of iterable) { + const key = callbackFn(value, index++); + (groups[key] ??= []).push(value); + } + + return groups; +};" +`; + exports[`App can render goody: JavaScript Object.prototype.entries 1`] = ` "import "Iterator.prototype.map"; import "Object.prototype.keys"; @@ -2811,6 +2825,32 @@ Object.getUnsafe = function ( };" `; +exports[`App can render goody: TypeScript Object.groupBy 1`] = ` +"declare global { + interface ObjectConstructor { + groupBy( + iterable: Iterable, + callbackFn: (value: V, index: number) => string, + ): Partial>; + } +} + +Object.groupBy ??= function ( + iterable: Iterable, + callbackFn: (value: V, index: number) => string, +): Partial> { + const groups: Partial> = {}; + + let index = 0; + for (const value of iterable) { + const key = callbackFn(value, index++); + (groups[key] ??= []).push(value); + } + + return groups; +};" +`; + exports[`App can render goody: TypeScript Object.prototype.entries 1`] = ` "import "Iterator.prototype.map"; import "Object.prototype.keys";