mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2024-11-22 08:20:58 -05:00
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:
parent
bddf78c669
commit
fc0a818902
2 changed files with 1077 additions and 0 deletions
454
src/utils/collections/map.ts
Normal file
454
src/utils/collections/map.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
623
tests/unit/utils/collections/map.spec.ts
Normal file
623
tests/unit/utils/collections/map.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue