Skip to content
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

feat(collections): add invert() and invertBy() #4710

Merged
merged 4 commits into from
May 12, 2024
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
2 changes: 2 additions & 0 deletions collections/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"./first-not-nullish-of": "./first_not_nullish_of.ts",
"./includes-value": "./includes_value.ts",
"./intersect": "./intersect.ts",
"./invert-by": "./invert_by.ts",
"./invert": "./invert.ts",
"./join-to-string": "./join_to_string.ts",
"./map-entries": "./map_entries.ts",
"./map-keys": "./map_keys.ts",
Expand Down
38 changes: 38 additions & 0 deletions collections/invert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.

/** Return type for {@linkcode invert}. */
export type InvertResult<T extends Record<PropertyKey, PropertyKey>> = {
[P in keyof T as T[P]]: P;
};

/**
* Composes a new record with all keys and values inverted.
*
* If the record contains duplicate values, subsequent values overwrite property
* assignments of previous values. If the record contains values which aren't
* {@linkcode PropertyKey}s their string representation is used as the key.
*
* @template T The type of the input record.
*
* @param record The record to invert.
*
* @returns A new record with all keys and values inverted.
*
* @example Basic usage
* ```ts
* import { invert } from "@std/collections/invert";
* import { assertEquals } from "@std/assert/assert-equals";
*
* const record = { a: "x", b: "y", c: "z" };
*
* assertEquals(invert(record), { x: "a", y: "b", z: "c" });
* ```
*/
export function invert<T extends Record<PropertyKey, PropertyKey>>(
record: Readonly<T>,
): InvertResult<T> {
return Object.fromEntries(
Object.entries(record).map(([key, value]) => [value, key]),
);
}
56 changes: 56 additions & 0 deletions collections/invert_by.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.

/** Return type for {@linkcode invertBy}. */
export type InvertByResult<
T extends Record<PropertyKey, PropertyKey>,
K extends keyof T,
> = Record<PropertyKey, K[]>;

/**
* Composes a new record with all keys and values inverted.
*
* The new record is generated from the result of running each element of the
* input record through the given transformer function.
*
* The corresponding inverted value of each inverted key is an array of keys
* responsible for generating the inverted value.
*
* @template R The type of the input record.
* @template T The type of the iterator function.
*
* @param record The record to invert.
* @param transformer The function to transform keys.
*
* @returns A new record with all keys and values inverted.
*
* @example Basic usage
* ```ts
* import { invertBy } from "@std/collections/invert-by";
* import { assertEquals } from "@std/assert/assert-equals";
*
* const record = { a: "x", b: "y", c: "z" };
*
* assertEquals(
* invertBy(record, (key) => String(key).toUpperCase()),
* { X: ["a"], Y: ["b"], Z: ["c"] }
* );
* ```
*/
export function invertBy<
R extends Record<PropertyKey, PropertyKey>,
T extends (key: PropertyKey) => PropertyKey,
>(record: Readonly<R>, transformer: T): InvertByResult<R, keyof R> {
const result = {} as InvertByResult<R, keyof R>;

for (const [key, value] of Object.entries(record)) {
const mappedKey = transformer(value);
if (!Object.hasOwn(result, mappedKey)) {
result[mappedKey] = [key];
} else {
result[mappedKey]!.push(key);
}
}

return result;
}
33 changes: 33 additions & 0 deletions collections/invert_by_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

import { assertEquals } from "@std/assert/assert-equals";
import { invertBy } from "./invert_by.ts";

function invertByTest<R extends Record<PropertyKey, PropertyKey>>(
input: [Readonly<R>, (key: PropertyKey) => PropertyKey],
expected: Record<PropertyKey, PropertyKey[]>,
) {
const actual = invertBy(...input);
assertEquals(actual, expected);
}

Deno.test("invertBy()", () => {
invertByTest(
[{ a: "x", b: "y", c: "z" }, (key) => String(key).toUpperCase()],
{ X: ["a"], Y: ["b"], Z: ["c"] },
);
});

Deno.test("invertBy() handles empty input", () => {
invertByTest(
[{}, (key) => key],
{},
);
});

Deno.test("invertBy() handles duplicate values", () => {
invertByTest(
[{ a: "x", b: "x", c: "z" }, (key) => key],
{ x: ["a", "b"], z: ["c"] },
);
});
37 changes: 37 additions & 0 deletions collections/invert_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

import { assertEquals } from "@std/assert";
import { invert, type InvertResult } from "./invert.ts";

function invertTest<T extends Record<PropertyKey, PropertyKey>>(
input: Readonly<T>,
expected: InvertResult<T>,
) {
const actual = invert(input);
assertEquals(actual, expected);
}

Deno.test("invert()", () => {
invertTest({ a: "x", b: "y", c: "z" }, { x: "a", y: "b", z: "c" });
});

Deno.test("invert() handles empty input", () => {
invertTest({}, {});
});

Deno.test("invert() handles duplicate values", () => {
invertTest({ a: "x", b: "x", c: "z" }, { x: "b", z: "c" });
});

Deno.test("invert() handles nested input", () => {
// @ts-ignore - testing invalid input
invertTest({ a: { b: "c" } }, { "[object Object]": "a" });
// @ts-ignore - testing invalid input
invertTest({ a: "x", b: () => {} }, { "x": "a", "()=>{}": "b" });
// @ts-ignore - testing invalid input
invertTest({ a: "x", b: ["y", "z"] }, { "x": "a", "y,z": "b" });
// @ts-ignore - testing invalid input
invertTest({ a: "x", b: null }, { "x": "a", "null": "b" });
// @ts-ignore - testing invalid input
invertTest({ a: "x", b: undefined }, { "x": "a", "undefined": "b" });
});
2 changes: 2 additions & 0 deletions collections/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export * from "./find_single.ts";
export * from "./first_not_nullish_of.ts";
export * from "./includes_value.ts";
export * from "./intersect.ts";
export * from "./invert_by.ts";
export * from "./invert.ts";
export * from "./join_to_string.ts";
export * from "./map_entries.ts";
export * from "./map_keys.ts";
Expand Down