feat: Add a SetMultiMap interface and implementation

This commit is contained in:
Joachim Van Herwegen 2022-07-08 11:38:43 +02:00
parent c35cd599a3
commit b5d5071403
5 changed files with 375 additions and 0 deletions

View File

@ -24,5 +24,6 @@
"VariableBindings",
"UnionHandler",
"WinstonLogger",
"WrappedSetMultiMap",
"YargsOptions"
]

View File

@ -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';

View File

@ -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<TKey, TVal> extends Map<TKey, TVal | ReadonlySet<TVal>> {
/**
* Returns all values stored for the given key.
* Returns `undefined` if there are no values for this key.
*/
get: (key: TKey) => ReadonlySet<TVal> | 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<TVal>) => 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<TKey, ReadonlySet<TVal>>;
/**
* 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<TVal>]>;
/**
* Iterates over all distinct keys in this Map.
*/
distinctKeys: () => IterableIterator<TKey>;
/**
* Iterates over all values in this Map.
*/
values: () => IterableIterator<TVal>;
/**
* Iterates over all distinct keys and returns their {@link Set} of values.
*/
valueSets: () => IterableIterator<ReadonlySet<TVal>>;
/**
* Loops over all key/value bindings.
*/
forEach: (callbackfn: (value: TVal, key: TKey, map: SetMultiMap<TKey, TVal>) => void, thisArg?: any) => void;
}

View File

@ -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<TKey, TVal> implements SetMultiMap<TKey, TVal> {
private count: number;
private readonly map: Map<TKey, Set<TVal>>;
/**
* @param mapConstructor - Will be used to instantiate the internal Map.
* @param iterable - Entries to add to the map.
*/
public constructor(mapConstructor: new() => Map<any, any> = Map,
iterable?: Iterable<readonly [TKey, TVal | ReadonlySet<TVal>]>) {
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<TVal> | undefined {
return this.map.get(key);
}
public set(key: TKey, value: ReadonlySet<TVal> | 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<TVal>): 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<TKey, ReadonlySet<TVal>> {
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<TVal>]> {
yield* this.map.entries();
}
public* keys(): IterableIterator<TKey> {
for (const [ key ] of this.entries()) {
yield key;
}
}
public distinctKeys(): IterableIterator<TKey> {
return this.map.keys();
}
public* values(): IterableIterator<TVal> {
for (const [ , value ] of this.entries()) {
yield value;
}
}
public valueSets(): IterableIterator<ReadonlySet<TVal>> {
return this.map.values();
}
public forEach(callbackfn: (value: TVal, key: TKey, map: SetMultiMap<TKey, TVal>) => 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';
}

View File

@ -0,0 +1,164 @@
import { WrappedSetMultiMap } from '../../../../src/util/map/WrappedSetMultiMap';
describe('A WrappedSetMultiMap', (): void => {
const key = 'key';
let map: WrappedSetMultiMap<string, number>;
beforeEach(async(): Promise<void> => {
map = new WrappedSetMultiMap();
});
it('can set values and check their existence.', async(): Promise<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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 ]);
});
});