diff --git a/.componentsignore b/.componentsignore index 094a031f5..7c399b333 100644 --- a/.componentsignore +++ b/.componentsignore @@ -24,5 +24,6 @@ "VariableBindings", "UnionHandler", "WinstonLogger", + "WrappedSetMultiMap", "YargsOptions" ] diff --git a/src/index.ts b/src/index.ts index 47fd5ea13..98550f716 100644 --- a/src/index.ts +++ b/src/index.ts @@ -438,6 +438,8 @@ export * from './util/locking/VoidLocker'; // Util/Map export * from './util/map/HashMap'; +export * from './util/map/SetMultiMap'; +export * from './util/map/WrappedSetMultiMap'; // Util/Templates export * from './util/templates/ChainedTemplateEngine'; diff --git a/src/util/map/SetMultiMap.ts b/src/util/map/SetMultiMap.ts new file mode 100644 index 000000000..a0a15f30e --- /dev/null +++ b/src/util/map/SetMultiMap.ts @@ -0,0 +1,59 @@ +/** + * A SetMultiMap is a Map where a single key can have multiple unique values. + * Deleting a key removes all bindings with this key from the Map. + * Setting a value for a key replaces all previous bindings with this key. + * Using an empty Set when calling the `set` function is the same as deleting that key. + */ +export interface SetMultiMap extends Map> { + /** + * Returns all values stored for the given key. + * Returns `undefined` if there are no values for this key. + */ + get: (key: TKey) => ReadonlySet | undefined; + /** + * Returns true if this key/value binding exists in the Map. + */ + hasEntry: (key: TKey, value: TVal) => boolean; + /** + * Adds the given key/value binding to the Map. + */ + add: (key: TKey, value: TVal | ReadonlySet) => this; + /** + * Deletes the given key/value binding from the Map. + */ + deleteEntry: (key: TKey, value: TVal) => boolean; + + /** + * Returns a Readonly {@link Map} representation of this Map. + */ + asMap: () => ReadonlyMap>; + + /** + * Iterates over all key/value bindings in this Map. + */ + [Symbol.iterator]: () => IterableIterator<[TKey, TVal]>; + /** + * Iterates over all key/value bindings in this Map. + */ + entries: () => IterableIterator<[TKey, TVal]>; + /** + * Iterates over all distinct keys in this Map, together with a {@link Set} of their values. + */ + entrySets: () => IterableIterator<[TKey, ReadonlySet]>; + /** + * Iterates over all distinct keys in this Map. + */ + distinctKeys: () => IterableIterator; + /** + * Iterates over all values in this Map. + */ + values: () => IterableIterator; + /** + * Iterates over all distinct keys and returns their {@link Set} of values. + */ + valueSets: () => IterableIterator>; + /** + * Loops over all key/value bindings. + */ + forEach: (callbackfn: (value: TVal, key: TKey, map: SetMultiMap) => void, thisArg?: any) => void; +} diff --git a/src/util/map/WrappedSetMultiMap.ts b/src/util/map/WrappedSetMultiMap.ts new file mode 100644 index 000000000..7d19687da --- /dev/null +++ b/src/util/map/WrappedSetMultiMap.ts @@ -0,0 +1,149 @@ +import type { SetMultiMap } from './SetMultiMap'; + +/** + * A {@link SetMultiMap} that uses an internal Map based on the provided constructor. + * + * In case no input constructor is provided, the default Map implementation will be used. + * + * It is required that the value type of this map is not Set or any extension of Set, + * otherwise the `set` and `add` functions wil break. + */ +export class WrappedSetMultiMap implements SetMultiMap { + private count: number; + private readonly map: Map>; + + /** + * @param mapConstructor - Will be used to instantiate the internal Map. + * @param iterable - Entries to add to the map. + */ + public constructor(mapConstructor: new() => Map = Map, + iterable?: Iterable]>) { + this.map = new mapConstructor(); + this.count = 0; + + if (iterable) { + for (const [ key, val ] of iterable) { + this.add(key, val); + } + } + } + + public has(key: TKey): boolean { + return this.map.has(key); + } + + public hasEntry(key: TKey, value: TVal): boolean { + return Boolean(this.map.get(key)?.has(value)); + } + + public get(key: TKey): ReadonlySet | undefined { + return this.map.get(key); + } + + public set(key: TKey, value: ReadonlySet | TVal): this { + const setCount = this.get(key)?.size ?? 0; + const set = value instanceof Set ? new Set(value) : new Set([ value ]); + this.count += set.size - setCount; + if (set.size > 0) { + this.map.set(key, set); + } else { + this.map.delete(key); + } + + return this; + } + + public add(key: TKey, value: TVal | ReadonlySet): this { + const it = value instanceof Set ? value : [ value ]; + let set = this.map.get(key); + if (set) { + const originalCount = set.size; + for (const entry of it) { + set.add(entry); + } + this.count += set.size - originalCount; + } else { + set = new Set(it); + this.count += set.size; + this.map.set(key, set); + } + + return this; + } + + public delete(key: TKey): boolean { + const setCount = this.get(key)?.size ?? 0; + const existed = this.map.delete(key); + this.count -= setCount; + return existed; + } + + public deleteEntry(key: TKey, value: TVal): boolean { + const set = this.map.get(key); + if (set?.delete(value)) { + this.count -= 1; + if (set.size === 0) { + this.map.delete(key); + } + return true; + } + return false; + } + + public clear(): void { + this.map.clear(); + this.count = 0; + } + + public asMap(): ReadonlyMap> { + return this.map; + } + + public [Symbol.iterator](): IterableIterator<[TKey, TVal]> { + return this.entries(); + } + + public* entries(): IterableIterator<[TKey, TVal]> { + for (const [ key, set ] of this.map) { + for (const value of set) { + yield [ key, value ]; + } + } + } + + public* entrySets(): IterableIterator<[TKey, ReadonlySet]> { + yield* this.map.entries(); + } + + public* keys(): IterableIterator { + for (const [ key ] of this.entries()) { + yield key; + } + } + + public distinctKeys(): IterableIterator { + return this.map.keys(); + } + + public* values(): IterableIterator { + for (const [ , value ] of this.entries()) { + yield value; + } + } + + public valueSets(): IterableIterator> { + return this.map.values(); + } + + public forEach(callbackfn: (value: TVal, key: TKey, map: SetMultiMap) => void, thisArg?: any): void { + for (const [ key, value ] of this) { + callbackfn.bind(thisArg)(value, key, this); + } + } + + public get size(): number { + return this.count; + } + + public readonly [Symbol.toStringTag] = 'WrappedSetMultiMap'; +} diff --git a/test/unit/util/map/WrappedSetMultiMap.test.ts b/test/unit/util/map/WrappedSetMultiMap.test.ts new file mode 100644 index 000000000..7945ad477 --- /dev/null +++ b/test/unit/util/map/WrappedSetMultiMap.test.ts @@ -0,0 +1,164 @@ +import { WrappedSetMultiMap } from '../../../../src/util/map/WrappedSetMultiMap'; + +describe('A WrappedSetMultiMap', (): void => { + const key = 'key'; + let map: WrappedSetMultiMap; + + beforeEach(async(): Promise => { + map = new WrappedSetMultiMap(); + }); + + it('can set values and check their existence.', async(): Promise => { + expect(map.set(key, 123)).toBe(map); + expect(map.has(key)).toBe(true); + expect(map.hasEntry(key, 123)).toBe(true); + expect(map.get(key)).toEqual(new Set([ 123 ])); + expect(map.size).toBe(1); + }); + + it('can set multiple values simultaneously.', async(): Promise => { + expect(map.set(key, new Set([ 123, 456, 789 ]))).toBe(map); + expect(map.has(key)).toBe(true); + expect(map.hasEntry(key, 123)).toBe(true); + expect(map.hasEntry(key, 456)).toBe(true); + expect(map.hasEntry(key, 789)).toBe(true); + expect(map.get(key)).toEqual(new Set([ 123, 456, 789 ])); + expect(map.size).toBe(3); + }); + + it('overwrites values when setting them.', async(): Promise => { + expect(map.set(key, new Set([ 123, 456 ]))).toBe(map); + expect(map.set(key, new Set([ 456, 789 ]))).toBe(map); + expect(map.has(key)).toBe(true); + expect(map.hasEntry(key, 123)).toBe(false); + expect(map.hasEntry(key, 456)).toBe(true); + expect(map.hasEntry(key, 789)).toBe(true); + expect(map.get(key)).toEqual(new Set([ 456, 789 ])); + expect(map.size).toBe(2); + }); + + it('can set entries in the constructor.', async(): Promise => { + map = new WrappedSetMultiMap(undefined, [[ key, 123 ], [ key, new Set([ 456, 789 ]) ]]); + expect(map.has(key)).toBe(true); + expect(map.hasEntry(key, 123)).toBe(true); + expect(map.hasEntry(key, 456)).toBe(true); + expect(map.hasEntry(key, 789)).toBe(true); + expect(map.get(key)).toEqual(new Set([ 123, 456, 789 ])); + expect(map.size).toBe(3); + }); + + it('can add a single value.', async(): Promise => { + expect(map.set(key, 123)).toBe(map); + expect(map.add(key, 456)).toBe(map); + expect(map.hasEntry(key, 123)).toBe(true); + expect(map.hasEntry(key, 456)).toBe(true); + expect(map.get(key)).toEqual(new Set([ 123, 456 ])); + expect(map.size).toBe(2); + }); + + it('can add multiple values.', async(): Promise => { + expect(map.set(key, new Set([ 123, 456 ]))).toBe(map); + expect(map.add(key, 789)).toBe(map); + expect(map.hasEntry(key, 123)).toBe(true); + expect(map.hasEntry(key, 456)).toBe(true); + expect(map.hasEntry(key, 789)).toBe(true); + expect(map.get(key)).toEqual(new Set([ 123, 456, 789 ])); + expect(map.size).toBe(3); + }); + + it('can add a a value to a non-existent key.', async(): Promise => { + expect(map.add(key, 123)).toBe(map); + expect(map.hasEntry(key, 123)).toBe(true); + expect(map.get(key)).toEqual(new Set([ 123 ])); + expect(map.size).toBe(1); + }); + + it('correctly updates if the new value already exists.', async(): Promise => { + expect(map.set(key, 123)).toBe(map); + expect(map.add(key, 123)).toBe(map); + expect(map.hasEntry(key, 123)).toBe(true); + expect(map.get(key)).toEqual(new Set([ 123 ])); + expect(map.size).toBe(1); + }); + + it('correctly updates if some new values already exist.', async(): Promise => { + expect(map.set(key, new Set([ 123, 456 ]))).toBe(map); + expect(map.add(key, new Set([ 456, 789 ]))).toBe(map); + expect(map.hasEntry(key, 123)).toBe(true); + expect(map.hasEntry(key, 456)).toBe(true); + expect(map.hasEntry(key, 789)).toBe(true); + expect(map.get(key)).toEqual(new Set([ 123, 456, 789 ])); + expect(map.size).toBe(3); + }); + + it('removes the key if it is being set to an empty Set.', async(): Promise => { + expect(map.set(key, 123)).toBe(map); + expect(map.set(key, new Set())).toBe(map); + expect(map.has(key)).toBe(false); + expect(map.size).toBe(0); + }); + + it('can delete a key.', async(): Promise => { + expect(map.set(key, new Set([ 123, 456 ]))).toBe(map); + expect(map.delete(key)).toBe(true); + expect(map.has(key)).toBe(false); + expect(map.hasEntry(key, 123)).toBe(false); + expect(map.hasEntry(key, 456)).toBe(false); + expect(map.get(key)).toBeUndefined(); + expect(map.delete(key)).toBe(false); + expect(map.size).toBe(0); + }); + + it('can delete a single entry.', async(): Promise => { + expect(map.set(key, new Set([ 123, 456 ]))).toBe(map); + expect(map.deleteEntry(key, 123)).toBe(true); + expect(map.has(key)).toBe(true); + expect(map.hasEntry(key, 123)).toBe(false); + expect(map.hasEntry(key, 456)).toBe(true); + expect(map.get(key)).toEqual(new Set([ 456 ])); + expect(map.deleteEntry(key, 123)).toBe(false); + expect(map.size).toBe(1); + }); + + it('removes the key if the last entry is deleted.', async(): Promise => { + expect(map.set(key, 123)).toBe(map); + expect(map.deleteEntry(key, 123)).toBe(true); + expect(map.has(key)).toBe(false); + expect(map.hasEntry(key, 123)).toBe(false); + expect(map.get(key)).toBeUndefined(); + expect(map.delete(key)).toBe(false); + expect(map.size).toBe(0); + }); + + it('can clear the entire Map.', async(): Promise => { + expect(map.set(key, new Set([ 123, 456 ]))).toBe(map); + map.clear(); + expect(map.has(key)).toBe(false); + expect(map.size).toBe(0); + }); + + it('can iterate over the Map.', async(): Promise => { + expect(map.set(key, new Set([ 123, 456 ]))).toBe(map); + expect([ ...map ]).toEqual([[ key, 123 ], [ key, 456 ]]); + expect([ ...map.entries() ]).toEqual([[ key, 123 ], [ key, 456 ]]); + expect([ ...map.entrySets() ]).toEqual([[ key, new Set([ 123, 456 ]) ]]); + expect([ ...map.keys() ]).toEqual([ key, key ]); + expect([ ...map.distinctKeys() ]).toEqual([ key ]); + expect([ ...map.values() ]).toEqual([ 123, 456 ]); + expect([ ...map.valueSets() ]).toEqual([ new Set([ 123, 456 ]) ]); + }); + + it('exposes a readonly view on the internal Map for iteration.', async(): Promise => { + expect(map.set(key, new Set([ 123, 456 ]))).toBe(map); + expect([ ...map.asMap() ]).toEqual([[ key, new Set([ 123, 456 ]) ]]); + }); + + it('supports a forEach call.', async(): Promise => { + expect(map.set(key, new Set([ 123, 456 ]))).toBe(map); + const result: number[] = []; + map.forEach((value): void => { + result.push(value); + }); + expect(result).toEqual([ 123, 456 ]); + }); +});