mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add a SetMultiMap interface and implementation
This commit is contained in:
parent
c35cd599a3
commit
b5d5071403
@ -24,5 +24,6 @@
|
||||
"VariableBindings",
|
||||
"UnionHandler",
|
||||
"WinstonLogger",
|
||||
"WrappedSetMultiMap",
|
||||
"YargsOptions"
|
||||
]
|
||||
|
@ -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';
|
||||
|
59
src/util/map/SetMultiMap.ts
Normal file
59
src/util/map/SetMultiMap.ts
Normal 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;
|
||||
}
|
149
src/util/map/WrappedSetMultiMap.ts
Normal file
149
src/util/map/WrappedSetMultiMap.ts
Normal 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';
|
||||
}
|
164
test/unit/util/map/WrappedSetMultiMap.test.ts
Normal file
164
test/unit/util/map/WrappedSetMultiMap.test.ts
Normal 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 ]);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user