feat: Add a map that can check equality between object keys

This commit is contained in:
Joachim Van Herwegen 2022-06-29 10:50:46 +02:00
parent 45f8aa157d
commit c35cd599a3
3 changed files with 171 additions and 0 deletions

View File

@ -436,6 +436,9 @@ export * from './util/locking/ResourceLocker';
export * from './util/locking/WrappedExpiringReadWriteLocker';
export * from './util/locking/VoidLocker';
// Util/Map
export * from './util/map/HashMap';
// Util/Templates
export * from './util/templates/ChainedTemplateEngine';
export * from './util/templates/EjsTemplateEngine';

78
src/util/map/HashMap.ts Normal file
View File

@ -0,0 +1,78 @@
import { map } from '../IterableUtil';
type Entry<TKey, TVal> = { key: TKey; value: TVal };
/**
* A {@link Map} implementation that maps the Key object to a string using the provided hash function.
* This ensures that equal objects that are not the same instance are mapped to the same value.
*/
export class HashMap<TKey = any, TVal = any> implements Map<TKey, TVal> {
private readonly hashMap: Map<string, Entry<TKey, TVal>>;
private readonly hashFn: (key: TKey) => string;
public constructor(hashFn: (key: TKey) => string, iterable?: Iterable<readonly [TKey, TVal]>) {
this.hashFn = hashFn;
if (iterable) {
this.hashMap = new Map(map(iterable, ([ key, value ]): [string, Entry<TKey, TVal>] =>
[ this.hashFn(key), { key, value }]));
} else {
this.hashMap = new Map();
}
}
public has(key: TKey): boolean {
return this.hashMap.has(this.hashFn(key));
}
public get(key: TKey): TVal | undefined {
return this.hashMap.get(this.hashFn(key))?.value;
}
public set(key: TKey, value: TVal): this {
this.hashMap.set(this.hashFn(key), { key, value });
return this;
}
public delete(key: TKey): boolean {
return this.hashMap.delete(this.hashFn(key));
}
public clear(): void {
this.hashMap.clear();
}
public [Symbol.iterator](): IterableIterator<[TKey, TVal]> {
return this.entries();
}
public* entries(): IterableIterator<[TKey, TVal]> {
for (const [ , { key, value }] of this.hashMap) {
yield [ key, value ];
}
}
public* keys(): IterableIterator<TKey> {
for (const [ , { key }] of this.hashMap) {
yield key;
}
}
public* values(): IterableIterator<TVal> {
for (const [ , { value }] of this.hashMap) {
yield value;
}
}
public forEach(callbackfn: (value: TVal, key: TKey, map: Map<TKey, TVal>) => void, thisArg?: any): void {
for (const [ key, value ] of this) {
callbackfn.bind(thisArg)(value, key, this);
}
}
public get size(): number {
return this.hashMap.size;
}
public readonly [Symbol.toStringTag] = 'HashMap';
}

View File

@ -0,0 +1,90 @@
import { HashMap } from '../../../../src/util/map/HashMap';
type KeyType = { field1: string; field2: number };
type ValueType = { field3: string; field4: number };
function hashFn(key: KeyType): string {
return `${key.field1}${key.field2}`;
}
describe('A HashMap', (): void => {
const key1: KeyType = { field1: 'key', field2: 123 };
const key1Eq: KeyType = { field1: 'key', field2: 123 };
const key2: KeyType = { field1: 'key', field2: 321 };
const unknownKey: KeyType = { field1: 'key', field2: 999 };
const value1: ValueType = { field3: 'value', field4: 123 };
const value2: ValueType = { field3: 'value', field4: 321 };
let map: HashMap<KeyType, ValueType>;
beforeEach(async(): Promise<void> => {
map = new HashMap(hashFn);
map.set(key1, value1);
map.set(key2, value2);
});
it('can check if the map has a key.', async(): Promise<void> => {
expect(map.has(key1)).toBe(true);
expect(map.has(key1Eq)).toBe(true);
expect(map.has(key2)).toBe(true);
expect(map.has(unknownKey)).toBe(false);
});
it('can get the values from the map.', async(): Promise<void> => {
expect(map.get(key1)).toBe(value1);
expect(map.get(key1Eq)).toBe(value1);
expect(map.get(key2)).toBe(value2);
expect(map.get(unknownKey)).toBeUndefined();
});
it('can set values.', async(): Promise<void> => {
map.set(key1Eq, value2);
expect(map.get(key1)).toBe(value2);
});
it('can set values in the constructor.', async(): Promise<void> => {
map = new HashMap<KeyType, ValueType>(hashFn, [[ key1, value1 ]]);
expect(map.get(key1)).toBe(value1);
expect(map.has(key2)).toBe(false);
});
it('can remove values.', async(): Promise<void> => {
map.delete(key1Eq);
expect(map.has(key1)).toBe(false);
expect(map.has(key2)).toBe(true);
});
it('can clear the map.', async(): Promise<void> => {
map.clear();
expect(map.has(key1)).toBe(false);
expect(map.has(key2)).toBe(false);
});
it('can iterate over the map.', async(): Promise<void> => {
expect([ ...map ]).toEqual([[ key1, value1 ], [ key2, value2 ]]);
expect([ ...map.entries() ]).toEqual([[ key1, value1 ], [ key2, value2 ]]);
expect([ ...map.keys() ]).toEqual([ key1, key2 ]);
expect([ ...map.values() ]).toEqual([ value1, value2 ]);
});
it('supports a forEach call.', async(): Promise<void> => {
const result: string[] = [];
map.forEach((value): void => {
result.push(value.field3);
});
expect(result).toEqual([ 'value', 'value' ]);
});
it('can return the size.', async(): Promise<void> => {
expect(map.size).toBe(2);
map.delete(key1);
expect(map.size).toBe(1);
map.clear();
expect(map.size).toBe(0);
});
it('returns a string tag.', async(): Promise<void> => {
expect(map[Symbol.toStringTag]).toBe('HashMap');
});
});