From 8eca2710c7fa8cede7f7e9c96c6c0958d04e3c26 Mon Sep 17 00:00:00 2001 From: adamhamlin Date: Sat, 21 Oct 2023 14:35:51 -0400 Subject: [PATCH] feat: Add static `areEqual` utility method (#15) --- README.md | 5 +++ index.ts | 1 + src/__tests__/areEqual.test.ts | 70 ++++++++++++++++++++++++++++++++++ src/areEqual.ts | 17 +++++++++ src/map.ts | 29 +++++++------- src/set.ts | 29 +++++++------- 6 files changed, 122 insertions(+), 29 deletions(-) create mode 100644 src/__tests__/areEqual.test.ts create mode 100644 src/areEqual.ts diff --git a/README.md b/README.md index d2259ec..a576727 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/index.ts b/index.ts index 65efc3f..71d17a8 100644 --- a/index.ts +++ b/index.ts @@ -1,2 +1,3 @@ export { DeepMap } from './src/map'; export { DeepSet } from './src/set'; +export { areEqual } from './src/areEqual'; diff --git a/src/__tests__/areEqual.test.ts b/src/__tests__/areEqual.test.ts new file mode 100644 index 0000000..5e9bb55 --- /dev/null +++ b/src/__tests__/areEqual.test.ts @@ -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'); + }); + }); +}); diff --git a/src/areEqual.ts b/src/areEqual.ts new file mode 100644 index 0000000..81acbab --- /dev/null +++ b/src/areEqual.ts @@ -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(values: V[], options?: Options): 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; +} diff --git a/src/map.ts b/src/map.ts index 39f7c73..50818bf 100644 --- a/src/map.ts +++ b/src/map.ts @@ -18,6 +18,10 @@ export class DeepMap extends Map implements Compar private readonly map: Map, KeyValuePair>; // 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 = {}) { super(); this.normalizer = new Normalizer(options); @@ -29,6 +33,7 @@ export class DeepMap extends Map implements Compar /** * Getter for number of kev-value pairs in the map. + * @inheritdoc */ override get size(): number { return this.map.size; @@ -36,13 +41,14 @@ export class DeepMap extends Map implements Compar /** * 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 }); @@ -50,14 +56,14 @@ export class DeepMap extends Map implements Compar } /** - * 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)); @@ -65,13 +71,14 @@ export class DeepMap extends Map implements Compar /** * 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) => void): void { this.map.forEach((pair, _key, _internalMap) => { @@ -80,9 +87,8 @@ export class DeepMap extends Map 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]()) { @@ -91,9 +97,8 @@ export class DeepMap extends Map 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]()) { @@ -102,9 +107,7 @@ export class DeepMap extends Map implements Compar } /** - * Map keys iterator - * - * @yields the next key in the map + * @inheritdoc */ override *keys(): IterableIterator { for (const [key, _val] of this[Symbol.iterator]()) { @@ -113,9 +116,7 @@ export class DeepMap extends Map implements Compar } /** - * Map values iterator - * - * @yields the next value in the map + * @inheritdoc */ override *values(): IterableIterator { for (const [_key, val] of this[Symbol.iterator]()) { diff --git a/src/set.ts b/src/set.ts index 36080bf..f4f79b3 100644 --- a/src/set.ts +++ b/src/set.ts @@ -10,6 +10,10 @@ export class DeepSet extends Set implements Comparable; // 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) { super(); const transformedEntries = values ? values.map((el) => [el, null] as const) : null; @@ -18,6 +22,7 @@ export class DeepSet extends Set implements Comparable extends Set implements Comparable extends Set implements Comparable extends Set implements Comparable) => void): void { this.map.forEach((_mapVal, mapKey, _map) => { @@ -62,9 +69,7 @@ export class DeepSet extends Set implements Comparable { for (const [key, _val] of this.map[Symbol.iterator]()) { @@ -73,9 +78,7 @@ export class DeepSet extends Set implements Comparable { for (const val of this[Symbol.iterator]()) { @@ -84,9 +87,7 @@ export class DeepSet extends Set implements Comparable { for (const val of this[Symbol.iterator]()) { @@ -95,9 +96,7 @@ export class DeepSet extends Set implements Comparable { yield* this.keys();