Made ArrayMap and MultiMap

`ArrayMap` is a map implementation which can use custom equality comparers

`MultiMap` is mostly an alias for `Map<K, V[]>`, but with some extra methods
This commit is contained in:
Kir_Antipov 2023-01-02 12:41:53 +00:00
parent bddf78c669
commit fc0a818902
2 changed files with 1077 additions and 0 deletions

View file

@ -0,0 +1,454 @@
import { EqualityComparer, createDefaultEqualityComparer } from "@/utils/comparison";
import { $i, asArray, isIterable } from "./iterable";
/**
* Checks if a given value is an instance of a {@link Map}-like object.
*
* @template K - The key type of the `Map`-like object.
* @template V - The value type of the `Map`-like object.
*
* @param value - The value to be checked.
*
* @returns A boolean indicating whether the value is a `Map`-like object or not.
*/
export function isMap<K, V>(value: unknown): value is Map<K, V> {
if (value instanceof Map) {
return true;
}
const map = value as Map<K, V>;
return (
!!map &&
typeof map.keys === "function" &&
typeof map.values === "function" &&
typeof map.entries === "function" &&
typeof map.get === "function" &&
typeof map.set === "function" &&
typeof map.has === "function" &&
typeof map.delete === "function" &&
typeof map[Symbol.iterator] === "function"
);
}
/**
* Checks if a given value is an instance of a {@link ReadOnlyMap}-like object.
*
* @template K - The key type of the `ReadOnlyMap`-like object.
* @template V - The value type of the `ReadOnlyMap`-like object.
*
* @param value - The value to be checked.
*
* @returns A boolean indicating whether the value is a `ReadOnlyMap`-like object or not.
*/
export function isReadOnlyMap<K, V>(value: unknown): value is ReadonlyMap<K, V> {
if (value instanceof Map) {
return true;
}
const map = value as ReadonlyMap<K, V>;
return (
!!map &&
typeof map.keys === "function" &&
typeof map.values === "function" &&
typeof map.entries === "function" &&
typeof map.get === "function" &&
typeof map.has === "function" &&
typeof map[Symbol.iterator] === "function"
);
}
/**
* Checks if a given value is an instance of a {@link MultiMap}-like object.
*
* @template K - The key type of the `MultiMap`-like object.
* @template V - The value type of the `MultiMap`-like object.
*
* @param value - The value to be checked.
*
* @returns A boolean indicating whether the value is a `MultiMap`-like object or not.
*/
export function isMultiMap<K, V>(value: unknown): value is MultiMap<K, V> {
if (value instanceof MultiMap) {
return true;
}
const multiMap = value as MultiMap<K, V>;
return (
isMap(multiMap) &&
typeof multiMap.append === "function"
);
}
/**
* Implements {@link Map} using an array under the hood.
*
* @template K - The type of keys in the Map.
* @template V - The type of values in the Map.
*
* @remarks
*
* Recommended for small collections and/or for occasions when you need to provide a custom equality comparer.
*/
export class ArrayMap<K, V> implements Map<K, V> {
/**
* The array of keys.
*/
private readonly _keys: K[];
/**
* The array of values.
*/
private readonly _values: V[];
/**
* The equality comparer used to compare keys.
*/
private readonly _comparer: EqualityComparer<K>;
/**
* Constructs an empty {@link ArrayMap}.
*
* @param comparer - The equality comparer to use for comparing keys.
*/
constructor(comparer?: EqualityComparer<K>);
/**
* Constructs an {@link ArrayMap} from an iterable of key-value pairs.
*
* @param entries - The iterable of key-value pairs.
* @param comparer - The equality comparer to use for comparing keys.
*/
constructor(entries: Iterable<readonly [K, V]>, comparer?: EqualityComparer<K>);
/**
* Constructs an {@link ArrayMap} from either an iterable of key-value pairs or an equality comparer.
*
* @param entriesOrComparer - The iterable of key-value pairs or the equality comparer to use for comparing keys.
* @param comparer - The equality comparer to use for comparing keys (if `entriesOrComparer` is an iterable).
*/
constructor(entriesOrComparer?: Iterable<readonly [K, V]> | EqualityComparer<K>, comparer?: EqualityComparer<K>) {
// If entriesOrComparer is a function, it must be the comparer, so use it.
// Otherwise, use the default comparer.
comparer ??= typeof entriesOrComparer === "function" ? entriesOrComparer : createDefaultEqualityComparer<K>();
this._keys = [] as K[];
this._values = [] as V[];
this._comparer = comparer;
// If entriesOrComparer is undefined or is in fact a comparer, create an empty array of entries.
const entries = entriesOrComparer && entriesOrComparer !== comparer ? entriesOrComparer as Iterable<readonly [K, V]> : [];
for (const [key, value] of entries) {
this.set(key, value);
}
}
/**
* The number of key-value pairs in the map.
*/
get size(): number {
return this._keys.length;
}
/**
* Gets the value associated with the specified key.
*
* @param key - The key of the value to get.
*
* @returns The value associated with the specified key, or `undefined` if the key is not found.
*/
get(key: K): V | undefined {
const i = $i(this._keys).indexOf(key, this._comparer);
// Will return `undefined` if i === -1, which is exactly what we are looking for.
return this._values[i];
}
/**
* Sets the value associated with the specified key.
*
* @param key - The key of the value to set.
* @param value - The value to set.
*
* @returns This {@link ArrayMap} instance for chaining purposes.
*/
set(key: K, value: V): this {
const i = $i(this._keys).indexOf(key, this._comparer);
if (i === -1) {
this._keys.push(key);
this._values.push(value);
} else {
// Since we use a custom comparer, we need to update the key too.
this._keys[i] = key;
this._values[i] = value;
}
return this;
}
/**
* Determines whether the map contains the specified key.
*
* @param key - The key to check for.
*
* @returns `true` if the map contains the key; otherwise, `false`.
*/
has(key: K): boolean {
return $i(this._keys).includes(key, this._comparer);
}
/**
* Removes the entry with the specified key from the map.
*
* @param key - The key of the entry to remove.
*
* @returns `true` if an entry with the specified key was found and removed; otherwise, `false`.
*/
delete(key: K): boolean {
const i = $i(this._keys).indexOf(key, this._comparer);
if (i === -1) {
return false;
}
this._keys.splice(i, 1);
this._values.splice(i, 1);
return true;
}
/**
* Removes all key-value pairs from the map.
*/
clear(): void {
this._keys.splice(0);
this._values.splice(0);
}
/**
* Returns an iterator over the keys in the map.
*/
keys(): IterableIterator<K> {
return this._keys[Symbol.iterator]();
}
/**
* Returns an iterator over the values in the map.
*/
values(): IterableIterator<V> {
return this._values[Symbol.iterator]();
}
/**
* Returns an iterator over the entries in the map.
*/
*entries(): IterableIterator<[K, V]> {
const keys = this._keys;
const values = this._values;
for (let i = 0; i < keys.length; ++i) {
yield [keys[i], values[i]];
}
}
/**
* Calls the specified callback function for each key-value pair in the map.
*
* @param callbackFn - This function is called one time for each element in the map. It takes the value, key, and the map itself as arguments.
* @param thisArg - An optional object to which `this` keyword can refer in the `callbackFn` function.
*/
forEach(callbackFn: (value: V, key: K, map: ArrayMap<K, V>) => void, thisArg?: unknown): void {
callbackFn = thisArg === undefined ? callbackFn : callbackFn.bind(thisArg);
const keys = this._keys;
const values = this._values;
for (let i = 0; i < keys.length; ++i) {
callbackFn(values[i], keys[i], this);
}
}
/**
* Returns an iterator over the entries in the map.
*/
[Symbol.iterator](): IterableIterator<[K, V]> {
return this.entries();
}
/**
* Returns a string representation of this object.
*/
get [Symbol.toStringTag](): string {
return "Map";
}
}
/**
* A multi-map class that allows multiple values per key.
*
* @template K - The type of keys in the MultiMap.
* @template V - The type of values in the MultiMap.
*
* @remarks
*
* This class extends {@link ArrayMap} and stores values in arrays.
*/
export class MultiMap<K, V> extends ArrayMap<K, V[]> {
/**
* Gets the first value associated with the specified key.
*
* @param key - The key of the value to get.
*
* @returns The first value associated with the specified key, or `undefined` if the key is not found.
*/
getFirst(key: K): V | undefined {
return this.get(key)?.[0];
}
/**
* Sets a single value associated with the specified key, replacing any existing values.
*
* @param key - The key of the value to set.
* @param value - The value to set.
*
* @returns This {@link MultiMap} instance for chaining purposes.
*/
set(key: K, value: V): this;
/**
* Sets multiple values associated with the specified key, replacing any existing values.
*
* @param key - The key of the values to set.
* @param value - The iterable of values to set.
*
* @returns This {@link MultiMap} instance for chaining purposes.
*/
set(key: K, value: Iterable<V>): this;
/**
* Sets a single value or multiple values associated with the specified key, replacing any existing values.
*
* @param key - The key of the value to set.
* @param value - The value or values to set.
*
* @returns This {@link MultiMap} instance for chaining purposes.
*/
set(key: K, value: V | Iterable<V>): this {
const values = isIterable(value) ? asArray(value) : [value];
return super.set(key, values);
}
/**
* Appends a single value to the values associated with the specified key.
*
* @param key - The key of the value to append.
* @param value - The value to append.
*
* @returns This {@link MultiMap} instance for chaining purposes.
*/
append(key: K, value: V): this;
/**
* Appends multiple values to the values associated with the specified key.
*
* @param key - The key of the values to append.
* @param value - The iterable of values to append.
*
* @returns This {@link MultiMap} instance for chaining purposes.
*/
append(key: K, value: Iterable<V>): this;
/**
* Appends a single value or multiple values to the values associated with the specified key.
*
* @param key - The key of the values to append.
* @param value - The iterable of values to append.
*
* @returns This {@link MultiMap} instance for chaining purposes.
*/
append(key: K, value: V | Iterable<V>): this {
const existingValues = this.get(key);
if (!existingValues) {
return this.set(key, value as V);
}
if (isIterable(value)) {
existingValues.push(...value);
} else {
existingValues.push(value);
}
return this;
}
/**
* Removes all values associated with the specified key.
*
* @param key - The key of the values to remove.
*
* @returns `true` if values were found and removed; otherwise, `false`.
*/
delete(key: K): boolean;
/**
* Removes a specific value associated with the specified key using the optional comparer.
*
* @param key - The key of the value to remove.
* @param value - The value to remove.
* @param comparer - The optional equality comparer to use for comparing values.
*
* @returns `true` if the value was found and removed; otherwise, `false`.
*/
delete(key: K, value: V, comparer?: EqualityComparer<V>): boolean;
/**
* Removes value/values associated with the specified key.
*
* @param key - The key of the values to remove.
* @param value - The value to remove.
* @param comparer - The optional equality comparer to use for comparing values.
*
* @returns `true` if value/values were found and removed; otherwise, `false`.
*/
delete(key: K, value?: V, comparer?: EqualityComparer<V>): boolean {
if (value === undefined) {
return super.delete(key);
}
const values = this.get(key);
if (!values) {
return false;
}
const i = $i(values).indexOf(value, comparer);
if (i === -1) {
return false;
}
values.splice(i, 1);
return true;
}
/**
* Returns an iterable of all values in the MultiMap.
*/
flatValues(): Iterable<V> {
return $i(this.values()).flatMap(x => x);
}
/**
* Returns an iterable of key-value pairs in the MultiMap, where each key is associated with a single value.
*/
flatEntries(): Iterable<[K, V]> {
return $i(this.entries()).flatMap(([key, values]) => $i(values).map(value => [key, value]));
}
/**
* Calls the specified callback function for each key-value pair in the MultiMap, with each key associated with a single value.
*
* @param callbackFn - This function is called one time for each key-value pair in the MultiMap. It takes the value, key, and the MultiMap itself as arguments.
* @param thisArg - An optional object to which `this` keyword can refer in the `callbackFn` function.
*/
forEachFlat(callbackFn: (value: V, key: K, map: MultiMap<K, V>) => void, thisArg?: unknown): void {
callbackFn = thisArg === undefined ? callbackFn : callbackFn.bind(thisArg);
for (const [key, value] of this.flatEntries()) {
callbackFn(value, key, this);
}
}
}

View file

@ -0,0 +1,623 @@
import { IGNORE_CASE_EQUALITY_COMPARER } from "@/utils/comparison/string-equality-comparer";
import { isMap, isReadOnlyMap, isMultiMap, ArrayMap, MultiMap } from "@/utils/collections/map";
const readOnlyMapLike = {
keys: () => {},
values: () => {},
entries: () => {},
get: () => {},
has: () => {},
[Symbol.iterator]: () => {}
};
const mapLike = {
...readOnlyMapLike,
set: () => {},
delete: () => {},
};
const multiMapLike = {
...mapLike,
append: () => {},
};
describe("isMap", () => {
test("returns true for Map instances", () => {
expect(isMap(new Map())).toBe(true);
});
test("returns true for Map-like objects", () => {
expect(isMap(mapLike)).toBe(true);
});
test("returns false for non-Map-like objects", () => {
expect(isMap({})).toBe(false);
});
test("returns false for null and undefined", () => {
expect(isMap(null)).toBe(false);
expect(isMap(undefined)).toBe(false);
});
});
describe("isReadOnlyMap", () => {
test("returns true for Map instances", () => {
expect(isReadOnlyMap(new Map())).toBe(true);
});
test("returns true for ReadOnlyMap-like objects", () => {
expect(isReadOnlyMap(readOnlyMapLike)).toBe(true);
});
test("returns false for non-ReadOnlyMap-like objects", () => {
expect(isReadOnlyMap({})).toBe(false);
});
test("returns false for null and undefined", () => {
expect(isReadOnlyMap(null)).toBe(false);
expect(isReadOnlyMap(undefined)).toBe(false);
});
});
describe("isMultiMap", () => {
test("returns true for MultiMap instances", () => {
expect(isMultiMap(new MultiMap())).toBe(true);
});
test("returns true for MultiMap-like objects", () => {
expect(isMultiMap(multiMapLike)).toBe(true);
});
test("returns false for non-MultiMap-like objects", () => {
expect(isMultiMap({})).toBe(false);
});
test("returns false for null and undefined", () => {
expect(isMultiMap(null)).toBe(false);
expect(isMultiMap(undefined)).toBe(false);
});
});
describe("ArrayMap", () => {
describe("constructor", () => {
test("creates an empty map when no parameters are provided", () => {
const map = new ArrayMap();
expect(map.size).toBe(0);
});
test("creates a map from an iterable of entries", () => {
const map = new ArrayMap([[1, "one"], [2, "two"]]);
expect(map.size).toBe(2);
expect(map.get(1)).toBe("one");
expect(map.get(2)).toBe("two");
});
test("creates a map from an iterable of entries and a custom comparer", () => {
const map = new ArrayMap([["one", 1], ["two", 2]], IGNORE_CASE_EQUALITY_COMPARER);
expect(map.size).toBe(2);
expect(map.get("ONE")).toBe(1);
expect(map.get("TWO")).toBe(2);
});
test("creates a map from an iterable of entries, and eliminates duplicates", () => {
const map = new ArrayMap([[1, "zero"], [2, "two"], [1, "one"]]);
expect(map.size).toBe(2);
expect(map.get(1)).toBe("one");
expect(map.get(2)).toBe("two");
});
test("creates a map from an iterable of entries and a custom comparer, and eliminates duplicates", () => {
const map = new ArrayMap([["ONE", -1], ["two", 2], ["one", 1]], IGNORE_CASE_EQUALITY_COMPARER);
expect(map.size).toBe(2);
expect(map.get("ONE")).toBe(1);
expect(map.get("TWO")).toBe(2);
});
});
describe("get", () => {
test("returns value associated with the specified key", () => {
const map = new ArrayMap([[1, "one"], [2, "two"]]);
expect(map.get(1)).toBe("one");
});
test("respects custom comparer when retrieving value by key", () => {
const map = new ArrayMap([["one", 1]], IGNORE_CASE_EQUALITY_COMPARER);
expect(map.get("one")).toBe(1);
expect(map.get("One")).toBe(1);
expect(map.get("ONE")).toBe(1);
});
test("returns undefined if the key is not found", () => {
const map = new ArrayMap();
expect(map.get(1)).toBeUndefined();
});
});
describe("set", () => {
test("adds a new key-value pair if the key is not present", () => {
const map = new ArrayMap();
map.set(1, "one");
expect(map.get(1)).toBe("one");
expect(map.size).toBe(1);
});
test("respects custom comparer when setting value by key", () => {
const map = new ArrayMap([["one", 1]], IGNORE_CASE_EQUALITY_COMPARER);
map.set("Two", 2);
expect(map.get("two")).toBe(2);
map.set("TWO", 3);
expect(map.get("two")).toBe(3);
});
test("updates the value if the key is already present", () => {
const map = new ArrayMap([[1, "one"]]);
map.set(1, "updated");
expect(map.get(1)).toBe("updated");
expect(map.size).toBe(1);
});
});
describe("has", () => {
test("returns true if the key is present", () => {
const map = new ArrayMap([[1, "one"]]);
expect(map.has(1)).toBe(true);
});
test("respects custom comparer when checking for key presence", () => {
const map = new ArrayMap([["one", 1]], IGNORE_CASE_EQUALITY_COMPARER);
expect(map.has("one")).toBe(true);
expect(map.has("One")).toBe(true);
expect(map.has("ONE")).toBe(true);
});
test("returns false if the key is not present", () => {
const map = new ArrayMap();
expect(map.has(1)).toBe(false);
});
});
describe("delete", () => {
test("removes the entry with the specified key", () => {
const map = new ArrayMap([[1, "one"], [2, "two"]]);
expect(map.delete(1)).toBe(true);
expect(map.has(1)).toBe(false);
expect(map.size).toBe(1);
});
test("respects custom comparer when deleting by key", () => {
const map = new ArrayMap([["one", 1]], IGNORE_CASE_EQUALITY_COMPARER);
expect(map.delete("One")).toBe(true);
expect(map.has("one")).toBe(false);
expect(map.delete("ONE")).toBe(false);
});
test("returns false if the key is not present", () => {
const map = new ArrayMap();
expect(map.delete(1)).toBe(false);
});
});
describe("clear", () => {
test("removes all entries", () => {
const map = new ArrayMap([[1, "one"], [2, "two"]]);
map.clear();
expect(map.size).toBe(0);
expect(map.get(1)).toBeUndefined();
expect(map.has(1)).toBe(false);
});
});
describe("keys", () => {
test("returns an iterator over the keys", () => {
const map = new ArrayMap([[1, "one"], [2, "two"]]);
const keys = Array.from(map.keys());
expect(keys).toEqual([1, 2]);
});
});
describe("values", () => {
test("returns an iterator over the values", () => {
const map = new ArrayMap([[1, "one"], [2, "two"]]);
const values = Array.from(map.values());
expect(values).toEqual(["one", "two"]);
});
});
describe("entries", () => {
test("returns an iterator over the entries", () => {
const map = new ArrayMap([[1, "one"], [2, "two"]]);
const entries = Array.from(map.entries());
expect(entries).toEqual([[1, "one"], [2, "two"]]);
});
});
describe("forEach", () => {
test("calls the specified callback function for each entry", () => {
const map = new ArrayMap([[1, "one"], [2, "two"]]);
const callback = jest.fn();
map.forEach(callback);
expect(callback).toHaveBeenCalledTimes(2);
expect(callback).toHaveBeenCalledWith("one", 1, map);
expect(callback).toHaveBeenCalledWith("two", 2, map);
});
test("binds the callback function to the provided thisArg", () => {
const map = new ArrayMap([[1, "one"]]);
const thisArg = {};
map.forEach(function (this: typeof thisArg) {
expect(this).toBe(thisArg);
}, thisArg);
});
});
describe("[Symbol.iterator]", () => {
test("returns an iterator over the entries", () => {
const map = new ArrayMap([[1, "one"], [2, "two"]]);
const entries = Array.from(map[Symbol.iterator]());
expect(entries).toEqual([[1, "one"], [2, "two"]]);
});
});
describe("[Symbol.toStringTag]", () => {
test("returns 'Map'", () => {
const map = new ArrayMap();
expect(map[Symbol.toStringTag]).toBe("Map");
});
});
});
describe("MultiMap", () => {
describe("constructor", () => {
test("creates an empty map when no parameters are provided", () => {
const map = new MultiMap();
expect(map.size).toBe(0);
});
test("creates a map from an iterable of entries", () => {
const map = new MultiMap([[1, ["one"]], [2, ["two"]]]);
expect(map.size).toBe(2);
expect(map.getFirst(1)).toBe("one");
expect(map.getFirst(2)).toBe("two");
});
test("creates a map from an iterable of entries and a custom comparer", () => {
const map = new MultiMap([["one", [1]], ["two", [2]]], IGNORE_CASE_EQUALITY_COMPARER);
expect(map.size).toBe(2);
expect(map.getFirst("ONE")).toBe(1);
expect(map.getFirst("TWO")).toBe(2);
});
test("creates a map from an iterable of entries, and eliminates duplicates", () => {
const map = new MultiMap([[1, ["zero"]], [2, ["two"]], [1, ["one"]]]);
expect(map.size).toBe(2);
expect(map.getFirst(1)).toBe("one");
expect(map.getFirst(2)).toBe("two");
});
test("creates a map from an iterable of entries and a custom comparer, and eliminates duplicates", () => {
const map = new MultiMap([["ONE", [-1]], ["two", [2]], ["one", [1]]], IGNORE_CASE_EQUALITY_COMPARER);
expect(map.size).toBe(2);
expect(map.getFirst("ONE")).toBe(1);
expect(map.getFirst("TWO")).toBe(2);
});
});
describe("get", () => {
test("returns value associated with the specified key", () => {
const map = new MultiMap([[1, ["one"]], [2, ["two"]]]);
expect(map.get(1)).toEqual(["one"]);
});
test("respects custom comparer when retrieving value by key", () => {
const map = new MultiMap([["one", [1]]], IGNORE_CASE_EQUALITY_COMPARER);
expect(map.get("one")).toEqual([1]);
expect(map.get("One")).toEqual([1]);
expect(map.get("ONE")).toEqual([1]);
});
test("returns undefined if the key is not found", () => {
const map = new MultiMap();
expect(map.get(1)).toBeUndefined();
});
});
describe("getFirst", () => {
test("returns the first value for a given key", () => {
const map = new MultiMap([[1, ["one", "One", "ONE"]]]);
expect(map.getFirst(1)).toBe("one");
});
test("respects custom comparer when retrieving the first value by key", () => {
const map = new MultiMap([["one", [1, -1]]], IGNORE_CASE_EQUALITY_COMPARER);
expect(map.getFirst("one")).toEqual(1);
expect(map.getFirst("One")).toEqual(1);
expect(map.getFirst("ONE")).toEqual(1);
});
test("returns undefined if the key is not found", () => {
const map = new MultiMap();
expect(map.getFirst(1)).toBeUndefined();
});
});
describe("set", () => {
test("adds a new key-value pair if the key is not present", () => {
const map = new MultiMap();
map.set(1, ["one"]);
expect(map.getFirst(1)).toBe("one");
expect(map.size).toBe(1);
});
test("updates the value if the key is already present", () => {
const map = new MultiMap([[1, ["one"]]]);
map.set(1, ["updated"]);
expect(map.getFirst(1)).toBe("updated");
expect(map.size).toBe(1);
});
test("sets a single value for a given key", () => {
const map = new MultiMap();
map.set("one", 1);
expect(map.get("one")).toEqual([1]);
});
test("respects custom comparer when setting value by key", () => {
const map = new MultiMap([["one", [1]]], IGNORE_CASE_EQUALITY_COMPARER);
map.set("Two", [2]);
expect(map.getFirst("two")).toBe(2);
map.set("TWO", 3);
expect(map.getFirst("two")).toBe(3);
});
});
describe("append", () => {
test("appends a single value to existing values for a given key", () => {
const map = new MultiMap([["one", [1]]]);
map.append("one", -1);
expect(map.get("one")).toEqual([1, -1]);
});
test("appends multiple values to existing values for a given key", () => {
const map = new MultiMap([[1, ["one"]]]);
map.append(1, ["One", "ONE"]);
expect(map.get(1)).toEqual(["one", "One", "ONE"]);
});
test("respects custom comparer when appending values by key", () => {
const map = new MultiMap([["one", [1]]], IGNORE_CASE_EQUALITY_COMPARER);
map.append("One", -1);
map.append("ONE", [1, -1]);
expect(map.get("one")).toEqual([1, -1, 1, -1]);
});
});
describe("has", () => {
test("returns true if the key is present", () => {
const map = new MultiMap([[1, ["one"]]]);
expect(map.has(1)).toBe(true);
});
test("respects custom comparer when checking for key presence", () => {
const map = new MultiMap([["one", [1]]], IGNORE_CASE_EQUALITY_COMPARER);
expect(map.has("one")).toBe(true);
expect(map.has("One")).toBe(true);
expect(map.has("ONE")).toBe(true);
});
test("returns false if the key is not present", () => {
const map = new MultiMap();
expect(map.has(1)).toBe(false);
});
});
describe("delete", () => {
test("removes the entry with the specified key", () => {
const map = new MultiMap([[1, ["one"]], [2, ["two"]]]);
expect(map.delete(1)).toBe(true);
expect(map.has(1)).toBe(false);
expect(map.size).toBe(1);
});
test("deletes a specific value for a given key", () => {
const map = new MultiMap([[1, ["one", "One", "ONE"]]]);
expect(map.delete(1, "One")).toBe(true);
expect(map.get(1)).toEqual(["one", "ONE"]);
});
test("deletes a specific value for a given key using a custom comparer", () => {
const map = new MultiMap([[1, ["one", "not one"]]]);
expect(map.delete(1, "ONE", IGNORE_CASE_EQUALITY_COMPARER)).toBe(true);
expect(map.get(1)).toEqual(["not one"]);
});
test("respects custom comparer when deleting by key", () => {
const map = new MultiMap([["one", [1]], ["two", [2, -2]]], IGNORE_CASE_EQUALITY_COMPARER);
expect(map.delete("One")).toBe(true);
expect(map.has("one")).toBe(false);
expect(map.delete("ONE")).toBe(false);
expect(map.delete("TWO", -2)).toBe(true);
expect(map.has("Two")).toBe(true);
expect(map.get("two")).toEqual([2]);
});
test("returns false if the key is not present", () => {
const map = new MultiMap();
expect(map.delete(1)).toBe(false);
});
});
describe("clear", () => {
test("removes all entries", () => {
const map = new MultiMap([[1, ["one"]], [2, ["two"]]]);
map.clear();
expect(map.size).toBe(0);
expect(map.get(1)).toBeUndefined();
expect(map.has(1)).toBe(false);
});
});
describe("keys", () => {
test("returns an iterator over the keys", () => {
const map = new MultiMap([[1, ["one"]], [2, ["two"]]]);
const keys = Array.from(map.keys());
expect(keys).toEqual([1, 2]);
});
});
describe("values", () => {
test("returns an iterator over the values", () => {
const map = new MultiMap([[1, ["one"]], [2, ["two"]]]);
const values = Array.from(map.values());
expect(values).toEqual([["one"], ["two"]]);
});
});
describe("flatValues", () => {
test("returns an iterator over all values", () => {
const map = new MultiMap([[1, ["one", "One"]], [2, ["two", "Two"]]]);
const values = Array.from(map.flatValues());
expect(values).toEqual(["one", "One", "two", "Two"]);
});
});
describe("entries", () => {
test("returns an iterator over the entries", () => {
const map = new MultiMap([[1, ["one"]], [2, ["two"]]]);
const entries = Array.from(map.entries());
expect(entries).toEqual([[1, ["one"]], [2, ["two"]]]);
});
});
describe("flatEntries", () => {
test("returns an iterable of key-value pairs, with each key associated with a single value", () => {
const map = new MultiMap([[1, ["one", "One"]], [2, ["two", "Two"]]]);
const entries = Array.from(map.flatEntries());
expect(entries).toEqual([[1, "one"], [1, "One"], [2, "two"], [2, "Two"]]);
});
});
describe("forEach", () => {
test("calls the specified callback function for each entry", () => {
const map = new MultiMap([[1, ["one"]], [2, ["two"]]]);
const callback = jest.fn();
map.forEach(callback);
expect(callback).toHaveBeenCalledTimes(2);
expect(callback).toHaveBeenCalledWith(["one"], 1, map);
expect(callback).toHaveBeenCalledWith(["two"], 2, map);
});
test("binds the callback function to the provided thisArg", () => {
const map = new MultiMap([[1, ["one"]]]);
const thisArg = {};
map.forEach(function (this: typeof thisArg) {
expect(this).toBe(thisArg);
}, thisArg);
});
});
describe("forEachFlat", () => {
test("calls the callback function for each standalone value coupled with its key", () => {
const map = new MultiMap([[1, ["one", "One"]], [2, ["two", "Two"]]]);
const callbackFn = jest.fn();
map.forEachFlat(callbackFn);
expect(callbackFn).toHaveBeenCalledTimes(4);
expect(callbackFn).toHaveBeenNthCalledWith(1, "one", 1, map);
expect(callbackFn).toHaveBeenNthCalledWith(2, "One", 1, map);
expect(callbackFn).toHaveBeenNthCalledWith(3, "two", 2, map);
expect(callbackFn).toHaveBeenNthCalledWith(4, "Two", 2, map);
});
test("binds the callback function to the provided thisArg", () => {
const map = new MultiMap([[1, ["one"]]]);
const thisArg = {};
map.forEachFlat(function (this: typeof thisArg) {
expect(this).toBe(thisArg);
}, thisArg);
});
});
describe("[Symbol.iterator]", () => {
test("returns an iterator over the entries", () => {
const map = new MultiMap([[1, ["one"]], [2, ["two"]]]);
const entries = Array.from(map[Symbol.iterator]());
expect(entries).toEqual([[1, ["one"]], [2, ["two"]]]);
});
});
describe("[Symbol.toStringTag]", () => {
test("returns 'Map'", () => {
const map = new MultiMap();
expect(map[Symbol.toStringTag]).toBe("Map");
});
});
});