Skip to content

Commit

Permalink
feat: Add static areEqual utility method (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamhamlin authored Oct 21, 2023
1 parent aa2c8a8 commit 8eca271
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 29 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ The `options` argument is a superset of the options defined for [object-hash](ht
set.size; // 1
```
## Static Utility Methods
- _`areEqual(values, options?)`_: Returns `true` if all elements in `values` are equal. This can be useful when you need to quickly
test equality of more than 2 values, or when you want to specify an equality transform (via `options.transformer`).
## Notes/Caveats
- This still supports primitive keys/values like traditional `Map`/`Set`.
Expand Down
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { DeepMap } from './src/map';
export { DeepSet } from './src/set';
export { areEqual } from './src/areEqual';
70 changes: 70 additions & 0 deletions src/__tests__/areEqual.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { areEqual } from '../areEqual';

/**
* NOTE: areEqual relies on the use of a DeepSet, so we won't exhaustively cover the different equality scenarios.
*/
describe('areEqual', () => {
describe('Using object values', () => {
const a = { key: 'value' };
const b = { key: 'value' };
const c = { key: 'value' };
const d = { key: 'otherValue' };

it('Returns true when all values are equal', async () => {
expect(areEqual([a, b])).toBe(true);
expect(areEqual([b, c])).toBe(true);
expect(areEqual([a, b, c])).toBe(true);
expect(areEqual([a])).toBe(true); // we'll define this as vacuously true
});

it('Returns false when all values are not equal', async () => {
expect(areEqual([a, d])).toBe(false);
expect(areEqual([b, d])).toBe(false);
expect(areEqual([a, b, c, d])).toBe(false);
});
});

describe('Using primitive values', () => {
const a = 'value';
const b = 'value';
const c = 'value';
const d = 'otherValue';

it('Returns true when all values are equal', async () => {
expect(areEqual([a, b])).toBe(true);
expect(areEqual([b, c])).toBe(true);
expect(areEqual([a, b, c])).toBe(true);
expect(areEqual([a])).toBe(true); // we'll define this as vacuously true
});

it('Returns false when all values are not equal', async () => {
expect(areEqual([a, d])).toBe(false);
expect(areEqual([b, d])).toBe(false);
expect(areEqual([a, b, c, d])).toBe(false);
});
});

describe('Using options', () => {
type MyObject = { key: string; other: string };
const a = { key: 'value', other: 'a' };
const b = { key: 'value', other: 'b' };
const c = { key: 'value', other: 'c' };
const opts = {
transformer: (obj: MyObject): string => obj.key,
};

it('Returns true when all values are equal according to transformer', async () => {
expect(areEqual([a, b])).toBe(false);
expect(areEqual([a, b, c])).toBe(false);
// Now, with the transformer option
expect(areEqual([a, b], opts)).toBe(true);
expect(areEqual([a, b, c], opts)).toBe(true);
});
});

describe('Misc', () => {
it('Throws error when passing empty list of values', async () => {
expect(() => areEqual([])).toThrow('Empty values list passed to areEqual function');
});
});
});
17 changes: 17 additions & 0 deletions src/areEqual.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Options } from './options';
import { DeepSet } from './set';

/**
* Static utility for doing a one-time equality check across the provided values.
* @param values list whose elements will be compared to each other
* @param options configuration options
* @returns true if every element in `values` is equal to every other element
* @throws {Error} if `values` list is empty
*/
export function areEqual<V, TxV = V>(values: V[], options?: Options<V, null, TxV, null>): boolean {
if (values.length === 0) {
throw new Error('Empty values list passed to areEqual function');
}
const set = new DeepSet(values, options);
return set.size === 1;
}
29 changes: 15 additions & 14 deletions src/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export class DeepMap<K, V, TxK = K, TxV = V> extends Map<K, V> implements Compar
private readonly map: Map<Normalized<TxK>, KeyValuePair<K, V>>;

// NOTE: This is actually a thin wrapper. We're not using super other than to drive the (typed) API contract.
/**
* @param entries optional list of key-value pairs to initialize the map
* @param options configuration options
*/
constructor(entries?: readonly (readonly [K, V])[] | null, private options: Options<K, V, TxK, TxV> = {}) {
super();
this.normalizer = new Normalizer(options);
Expand All @@ -29,49 +33,52 @@ export class DeepMap<K, V, TxK = K, TxV = V> extends Map<K, V> implements Compar

/**
* Getter for number of kev-value pairs in the map.
* @inheritdoc
*/
override get size(): number {
return this.map.size;
}

/**
* Returns true if the given key is present in the map.
* @inheritdoc
*/
override has(key: K): boolean {
return this.map.has(this.normalizeKey(key));
}

/**
* Store the given key-value pair.
* @inheritdoc
*/
override set(key: K, val: V): this {
this.map.set(this.normalizeKey(key), { key, val });
return this;
}

/**
* Get the value associated with key. Otherwise, undefined.
* @inheritdoc
*/
override get(key: K): V | undefined {
return this.map.get(this.normalizeKey(key))?.val;
}

/**
* Delete the value associated with key.
* @inheritdoc
*/
override delete(key: K): boolean {
return this.map.delete(this.normalizeKey(key));
}

/**
* Clear all key-value pairs from the map.
* @inheritdoc
*/
override clear(): void {
this.map.clear();
}

/**
* Standard forEach function.
* @inheritdoc
*/
override forEach(callbackfn: (val: V, key: K, map: Map<K, V>) => void): void {
this.map.forEach((pair, _key, _internalMap) => {
Expand All @@ -80,9 +87,8 @@ export class DeepMap<K, V, TxK = K, TxV = V> extends Map<K, V> implements Compar
}

/**
* Map iterator
*
* @yields the next key-value pair in the map
* @inheritdoc
*/
override *[Symbol.iterator](): IterableIterator<[K, V]> {
for (const [_hashStr, pair] of this.map[Symbol.iterator]()) {
Expand All @@ -91,9 +97,8 @@ export class DeepMap<K, V, TxK = K, TxV = V> extends Map<K, V> implements Compar
}

/**
* Map iterator. Equivalent to Symbol.iterator.
*
* @yields the next key-value pair in the map
* @inheritdoc
*/
override *entries(): IterableIterator<[K, V]> {
for (const entry of this[Symbol.iterator]()) {
Expand All @@ -102,9 +107,7 @@ export class DeepMap<K, V, TxK = K, TxV = V> extends Map<K, V> implements Compar
}

/**
* Map keys iterator
*
* @yields the next key in the map
* @inheritdoc
*/
override *keys(): IterableIterator<K> {
for (const [key, _val] of this[Symbol.iterator]()) {
Expand All @@ -113,9 +116,7 @@ export class DeepMap<K, V, TxK = K, TxV = V> extends Map<K, V> implements Compar
}

/**
* Map values iterator
*
* @yields the next value in the map
* @inheritdoc
*/
override *values(): IterableIterator<V> {
for (const [_key, val] of this[Symbol.iterator]()) {
Expand Down
29 changes: 14 additions & 15 deletions src/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export class DeepSet<V, TxV = V> extends Set<V> implements Comparable<DeepSet<V,
private readonly map: DeepMap<V, null, TxV, null>;

// NOTE: This is actually a thin wrapper. We're not using super other than to drive the (typed) API contract.
/**
* @param values optional list of values to initialize the set
* @param options configuration options
*/
constructor(values?: readonly V[] | null, private options?: Options<V, null, TxV, null>) {
super();
const transformedEntries = values ? values.map((el) => [el, null] as const) : null;
Expand All @@ -18,42 +22,45 @@ export class DeepSet<V, TxV = V> extends Set<V> implements Comparable<DeepSet<V,

/**
* Getter for number of elements in the set.
* @inheritdoc
*/
override get size(): number {
return this.map.size;
}

/**
* Returns true if the given value is present in the set.
* @inheritdoc
*/
override has(val: V): boolean {
return this.map.has(val);
}

/**
* Store the given value.
* @inheritdoc
*/
override add(val: V): this {
this.map.set(val, null);
return this;
}

/**
* Deletes the specified value.
* @inheritdoc
*/
override delete(val: V): boolean {
return this.map.delete(val);
}

/**
* Clear all values from the map.
* @inheritdoc
*/
override clear(): void {
this.map.clear();
}

/**
* Standard forEach function.
* @inheritdoc
*/
override forEach(callbackfn: (val: V, val2: V, set: Set<V>) => void): void {
this.map.forEach((_mapVal, mapKey, _map) => {
Expand All @@ -62,9 +69,7 @@ export class DeepSet<V, TxV = V> extends Set<V> implements Comparable<DeepSet<V,
}

/**
* Set iterator
*
* @yields the next value in the set
* @inheritdoc
*/
override *[Symbol.iterator](): IterableIterator<V> {
for (const [key, _val] of this.map[Symbol.iterator]()) {
Expand All @@ -73,9 +78,7 @@ export class DeepSet<V, TxV = V> extends Set<V> implements Comparable<DeepSet<V,
}

/**
* Set iterator. Equivalent to Symbol.iterator.
*
* @yields the next value-value pair in the set
* @inheritdoc
*/
override *entries(): IterableIterator<[V, V]> {
for (const val of this[Symbol.iterator]()) {
Expand All @@ -84,9 +87,7 @@ export class DeepSet<V, TxV = V> extends Set<V> implements Comparable<DeepSet<V,
}

/**
* Set keys iterator. Equivalent to this.values().
*
* @yields the next key in the map
* @inheritdoc
*/
override *keys(): IterableIterator<V> {
for (const val of this[Symbol.iterator]()) {
Expand All @@ -95,9 +96,7 @@ export class DeepSet<V, TxV = V> extends Set<V> implements Comparable<DeepSet<V,
}

/**
* Set values iterator. Equivalent to this.keys().
*
* @yields the next value in the map
* @inheritdoc
*/
override *values(): IterableIterator<V> {
yield* this.keys();
Expand Down

0 comments on commit 8eca271

Please sign in to comment.