feat(data-structures/unstable): @std/data-structures/bidirectional-map (#5910)

* feat(data-structures/unstable): `@std/data-structures/BidirectionalMap`

* add type params

* fmt

* sort mod exports

* add header

* add missing comments

* fmt

* close example code block

* add missing comments

* fmt

* close code block

* tweaks

* tweak

---------

Co-authored-by: Asher Gomez <ashersaupingomez@gmail.com>
This commit is contained in:
Liam Tait 2024-09-05 12:56:40 +12:00 committed by GitHub
parent 67e9cfa0ff
commit 7c0e917b93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 539 additions and 0 deletions

View File

@ -0,0 +1,223 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.
/**
* An extension of {@linkcode Map} that allows lookup by both key and value.
*
* Keys and values must be unique. Setting an existing key updates its value.
* Setting an existing value updates its key.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @typeParam K The type of the keys in the map.
* @typeParam V The type of the values in the map.
*
* @example Usage
* ```ts
* import { BidirectionalMap } from "@std/data-structures/bidirectional-map";
* import { assertEquals } from "@std/assert";
*
* const map = new BidirectionalMap([["one", 1]]);
*
* assertEquals(map.get("one"), 1);
* assertEquals(map.getReverse(1), "one");
* ```
*
* @example Inserting a value that already exists
* ```ts
* import { BidirectionalMap } from "@std/data-structures/bidirectional-map";
* import { assertEquals } from "@std/assert";
*
* const map = new BidirectionalMap();
* map.set(1, "one");
* map.set(2, "one");
*
* assertEquals(map.size, 1);
* assertEquals(map.get(1), undefined);
* assertEquals(map.getReverse("one"), 2);
* ```
*/
export class BidirectionalMap<K, V> extends Map<K, V> {
#reverseMap: Map<V, K>;
/**
* Creates a new instance.
*
* @param entries An iterable of key-value pairs for the initial entries.
*/
constructor(entries?: readonly (readonly [K, V])[] | null) {
super();
this.#reverseMap = new Map<V, K>();
if (entries) {
for (const [key, value] of entries) {
this.set(key, value);
}
}
}
/**
* Clears all entries.
*
* @example Usage
* ```ts
* import { BidirectionalMap } from "@std/data-structures/bidirectional-map";
* import { assertEquals } from "@std/assert";
*
* const map = new BidirectionalMap([["one", 1]]);
* map.clear();
* assertEquals(map.size, 0);
* ```
*/
override clear() {
super.clear();
this.#reverseMap.clear();
}
/**
* Adds a new element with a specified key and value. If an entry with the
* same key or value already exists, the entry will be updated.
*
* @param key The key to set.
* @param value The value to associate with the key.
*
* @returns The instance.
*
* @example Usage
* ```ts
* import { BidirectionalMap } from "@std/data-structures/bidirectional-map";
* import { assertEquals } from "@std/assert";
*
* const map = new BidirectionalMap();
* map.set("one", 1);
*
* assertEquals(map.get("one"), 1);
* assertEquals(map.getReverse(1), "one");
* ```
*/
override set(key: K, value: V): this {
const oldValue = super.get(key);
if (oldValue !== undefined) {
this.#reverseMap.delete(oldValue);
}
const oldKey = this.#reverseMap.get(value);
if (oldKey !== undefined) {
super.delete(oldKey);
}
super.set(key, value);
this.#reverseMap.set(value, key);
return this;
}
/**
* Returns the key associated with the specified value. If no key is
* associated with the specified value, `undefined` is returned.
*
* @param value The value to search for.
* @returns The key associated with the specified value, or `undefined` if no
* key is found.
*
* @example Usage
* ```ts
* import { BidirectionalMap } from "@std/data-structures/bidirectional-map";
* import { assertEquals } from "@std/assert";
*
* const map = new BidirectionalMap([["one", 1]]);
*
* assertEquals(map.getReverse(1), "one");
* ```
*/
getReverse(value: V): K | undefined {
return this.#reverseMap.get(value);
}
/**
* Removes the element with the specified key. If the element does not exist,
* the instance remains unchanged.
*
* @param key The key of the element to remove.
*
* @returns `true` if an element in the instance existed and has been removed,
* or `false` if the element does not exist.
*
* @example Usage
* ```ts
* import { BidirectionalMap } from "@std/data-structures/bidirectional-map";
* import { assertEquals } from "@std/assert";
*
* const map = new BidirectionalMap([["one", 1]]);
* map.delete("one");
*
* assertEquals(map.size, 0);
* ```
*/
override delete(key: K): boolean {
const value = super.get(key);
if (value === undefined) return false;
return super.delete(key) && this.#reverseMap.delete(value);
}
/**
* Removes the element with the specified value. If the element does not
* exist, the instance remains unchanged.
*
* @param value The value of the element to remove.
* @returns `true` if an element in the instance existed and has been removed,
* or `false` if the element does not exist.
*
* @example Usage
* ```ts
* import { BidirectionalMap } from "@std/data-structures/bidirectional-map";
* import { assertEquals } from "@std/assert";
*
* const map = new BidirectionalMap([["one", 1]]);
*
* map.deleteReverse(1);
*
* assertEquals(map.get("one"), undefined);
* assertEquals(map.getReverse(1), undefined);
* assertEquals(map.size, 0);
* ```
*/
deleteReverse(value: V): boolean {
const key = this.#reverseMap.get(value);
if (key === undefined) return false;
return super.delete(key) && this.#reverseMap.delete(value);
}
/**
* Checks if an element with the specified value exists.
*
* @param value The value to search for.
*
* @returns `true` if an element with the specified value exists, otherwise
* `false`.
*
* @example Usage
* ```ts
* import { BidirectionalMap } from "@std/data-structures/bidirectional-map";
* import { assertEquals } from "@std/assert";
*
* const map = new BidirectionalMap([["one", 1]]);
*
* assertEquals(map.hasReverse(1), true);
* ```
*/
hasReverse(value: V): boolean {
return this.#reverseMap.has(value);
}
/**
* A String value that is used in the creation of the default string description of an object.
* Called by the built-in method `Object.prototype.toString`.
*
* @example Usage
* ```ts
* import { BidirectionalMap } from "@std/data-structures/bidirectional-map";
* import { assertEquals } from "@std/assert";
*
* const map = new BidirectionalMap();
* assertEquals(map.toString(), "[object BidirectionalMap]");
* ```
*/
readonly [Symbol.toStringTag] = "BidirectionalMap";
}

View File

@ -0,0 +1,314 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assert, assertEquals } from "@std/assert";
import { BidirectionalMap } from "./bidirectional_map.ts";
Deno.test("BidirectionalMap is an instance of Map", () => {
const biMap = new BidirectionalMap();
assert(biMap instanceof Map);
});
Deno.test("BidirectionalMap.set() removes values", () => {
const map = new BidirectionalMap<number, string>();
map.set(1, "one");
map.set(2, "one");
assertEquals(map.size, 1);
assertEquals(map.get(1), undefined);
assertEquals(map.get(2), "one");
assertEquals(map.getReverse("one"), 2);
});
Deno.test("BidirectionalMap.clear()", () => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
]);
map.clear();
assertEquals(map.size, 0);
assertEquals(map.has("one"), false);
assertEquals(map.hasReverse(1), false);
});
Deno.test("BidirectionalMap.delete() removes the key-value pair", () => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
]);
assertEquals(map.delete("two"), true);
assertEquals(map.size, 2);
assertEquals(map.has("two"), false);
assertEquals(map.hasReverse(2), false);
});
Deno.test(
"BidirectionalMap.delete() returns false if the key does not exist",
() => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
]);
assertEquals(map.delete("four"), false);
assertEquals(map.size, 3);
},
);
Deno.test("BidirectionalMap.deleteReverse() removes the value-key pair", () => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
]);
assertEquals(map.deleteReverse(2), true);
assertEquals(map.size, 2);
assertEquals(map.has("two"), false);
assertEquals(map.hasReverse(2), false);
});
Deno.test(
"BidirectionalMap.deleteReverse() returns false if the value does not exist",
() => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
]);
assertEquals(map.deleteReverse(4), false);
assertEquals(map.size, 3);
},
);
Deno.test(
"BidirectionalMap.forEach() iterates over the key-value pairs",
() => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
]);
const result: [string, number][] = [];
map.forEach((value, key) => {
result.push([key, value]);
});
assertEquals(result, [
["one", 1],
["two", 2],
["three", 3],
]);
},
);
Deno.test(
"BidirectionalMap.get() returns the value associated with the key",
() => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
]);
assertEquals(map.get("one"), 1);
assertEquals(map.get("two"), 2);
assertEquals(map.get("three"), 3);
},
);
Deno.test(
"BidirectionalMap.getReverse() returns the key associated with the value",
() => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
]);
assertEquals(map.getReverse(1), "one");
assertEquals(map.getReverse(2), "two");
assertEquals(map.getReverse(3), "three");
},
);
Deno.test("BidirectionalMap.has() returns true if the key exists", () => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
]);
assertEquals(map.has("one"), true);
assertEquals(map.has("two"), true);
assertEquals(map.has("three"), true);
assertEquals(map.has("four"), false);
});
Deno.test(
"BidirectionalMap.hasReverse() returns true if the value exists",
() => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
]);
assertEquals(map.hasReverse(1), true);
assertEquals(map.hasReverse(2), true);
assertEquals(map.hasReverse(3), true);
assertEquals(map.hasReverse(4), false);
},
);
Deno.test("BidirectionalMap.set() adds a new key-value pair", () => {
const map = new BidirectionalMap();
map.set("one", 1);
map.set("two", 2);
map.set("three", 3);
assertEquals(map.size, 3);
assertEquals(map.get("one"), 1);
assertEquals(map.get("two"), 2);
assertEquals(map.get("three"), 3);
assertEquals(map.getReverse(1), "one");
assertEquals(map.getReverse(2), "two");
assertEquals(map.getReverse(3), "three");
});
Deno.test(
"BidirectionalMap.set() updates the value if the key already exists",
() => {
const map = new BidirectionalMap();
map.set("one", 1);
map.set("one", 2);
assertEquals(map.size, 1);
assertEquals(map.get("one"), 2);
assertEquals(map.getReverse(2), "one");
},
);
Deno.test(
"BidirectionalMap.set() twice with the same key updates the value",
() => {
const map = new BidirectionalMap();
map.set("key", "value");
map.set("key", "secondValue");
assertEquals(map.get("key"), "secondValue");
assertEquals(map.getReverse("value"), undefined);
assertEquals(map.getReverse("secondValue"), "key");
},
);
Deno.test(
"BidirectionalMap.set() returns the BidirectionalMap instance",
() => {
const map = new BidirectionalMap();
const result = map.set(1, "one");
assertEquals(result, map);
},
);
Deno.test("BidirectionalMap.size returns the number of key-value pairs", () => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
["four", 4],
]);
assertEquals(map.size, 4);
});
Deno.test(
"BidirectionalMap.entries() returns an iterator of key-value pairs",
() => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
]);
const result = Array.from(map.entries());
assertEquals(result, [
["one", 1],
["two", 2],
["three", 3],
]);
},
);
Deno.test("BidirectionalMap.keys() returns an iterator of keys", () => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
]);
const result = Array.from(map.keys());
assertEquals(result, ["one", "two", "three"]);
});
Deno.test("BidirectionalMap.values() returns an iterator of values", () => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
]);
const result = Array.from(map.values());
assertEquals(result, [1, 2, 3]);
});
Deno.test(
"BidirectionalMap[Symbol.iterator]() returns an iterator of key-value pairs",
() => {
const map = new BidirectionalMap([
["one", 1],
["two", 2],
["three", 3],
]);
const result = Array.from(map[Symbol.iterator]());
assertEquals(result, [
["one", 1],
["two", 2],
["three", 3],
]);
},
);
Deno.test("BidirectionalMap[Symbol.toStringTag] is 'BidirectionalMap'", () => {
const map = new BidirectionalMap();
assertEquals(map.toString(), "[object BidirectionalMap]");
assertEquals(
Object.prototype.toString.call(map),
"[object BidirectionalMap]",
);
});
Deno.test(
"BidirectionalMap.get() have the same references as BidirectionalMap.getReverse()",
() => {
const key = { name: "Pīwakawaka" };
const value = { favourite: false };
const map = new BidirectionalMap([[key, value]]);
map.get(key)!.favourite = true;
map.getReverse(value)!.name = "Tūī";
assertEquals(key.name, "Tūī");
assertEquals(value.favourite, true);
assertEquals(map.get(key)!.favourite, true);
assertEquals(map.getReverse(value)!.name, "Tūī");
const entries = Array.from(map.entries());
assertEquals(entries, [[{ name: "Tūī" }, { favourite: true }]]);
},
);

View File

@ -3,6 +3,7 @@
"version": "1.0.2",
"exports": {
".": "./mod.ts",
"./bidirectional-map": "./bidirectional_map.ts",
"./binary-heap": "./binary_heap.ts",
"./binary-search-tree": "./binary_search_tree.ts",
"./comparators": "./comparators.ts",

View File

@ -25,6 +25,7 @@
* @module
*/
export * from "./bidirectional_map.ts";
export * from "./binary_heap.ts";
export * from "./binary_search_tree.ts";
export * from "./comparators.ts";