From e4bd07b2394f227ea06b72eb6999de9ab3127b25 Mon Sep 17 00:00:00 2001 From: 1Computer1 <22125769+1Computer1@users.noreply.github.com> Date: Wed, 26 Jan 2022 15:46:31 -0500 Subject: [PATCH] feat(Collection): add merging functions (#7299) Co-authored-by: Vlad Frangu --- .../collection/__tests__/collection.test.ts | 92 +++++++++++++++++++ packages/collection/src/index.ts | 80 ++++++++++++++++ 2 files changed, 172 insertions(+) diff --git a/packages/collection/__tests__/collection.test.ts b/packages/collection/__tests__/collection.test.ts index 0de9655a0..edd9ddb76 100644 --- a/packages/collection/__tests__/collection.test.ts +++ b/packages/collection/__tests__/collection.test.ts @@ -460,3 +460,95 @@ describe('ensure() tests', () => { expect(coll.size).toStrictEqual(2); }); }); + +describe('merge() tests', () => { + const cL = new Collection([ + ['L', 1], + ['LR', 2], + ]); + const cR = new Collection([ + ['R', 3], + ['LR', 4], + ]); + + test('merges two collection, with all keys together', () => { + const c = cL.merge( + cR, + (x) => ({ keep: true, value: `L${x}` }), + (y) => ({ keep: true, value: `R${y}` }), + (x, y) => ({ keep: true, value: `LR${x},${y}` }), + ); + expect(c.get('L')).toStrictEqual('L1'); + expect(c.get('R')).toStrictEqual('R3'); + expect(c.get('LR')).toStrictEqual('LR2,4'); + expect(c.size).toStrictEqual(3); + }); + + test('merges two collection, removing left entries', () => { + const c = cL.merge( + cR, + () => ({ keep: false }), + (y) => ({ keep: true, value: `R${y}` }), + (x, y) => ({ keep: true, value: `LR${x},${y}` }), + ); + expect(c.get('R')).toStrictEqual('R3'); + expect(c.get('LR')).toStrictEqual('LR2,4'); + expect(c.size).toStrictEqual(2); + }); + + test('merges two collection, removing right entries', () => { + const c = cL.merge( + cR, + (x) => ({ keep: true, value: `L${x}` }), + () => ({ keep: false }), + (x, y) => ({ keep: true, value: `LR${x},${y}` }), + ); + expect(c.get('L')).toStrictEqual('L1'); + expect(c.get('LR')).toStrictEqual('LR2,4'); + expect(c.size).toStrictEqual(2); + }); + + test('merges two collection, removing in-both entries', () => { + const c = cL.merge( + cR, + (x) => ({ keep: true, value: `L${x}` }), + (y) => ({ keep: true, value: `R${y}` }), + () => ({ keep: false }), + ); + expect(c.get('L')).toStrictEqual('L1'); + expect(c.get('R')).toStrictEqual('R3'); + expect(c.size).toStrictEqual(2); + }); +}); + +describe('combineEntries() tests', () => { + test('it adds entries together', () => { + const c = Collection.combineEntries( + [ + ['a', 1], + ['b', 2], + ['a', 2], + ], + (x, y) => x + y, + ); + expect([...c]).toStrictEqual([ + ['a', 3], + ['b', 2], + ]); + }); + + test('it really goes through all the entries', () => { + const c = Collection.combineEntries( + [ + ['a', [1]], + ['b', [2]], + ['a', [2]], + ], + (x, y) => x.concat(y), + ); + expect([...c]).toStrictEqual([ + ['a', [1, 2]], + ['b', [2]], + ]); + }); +}); diff --git a/packages/collection/src/index.ts b/packages/collection/src/index.ts index c0180a20f..a2bb42852 100644 --- a/packages/collection/src/index.ts +++ b/packages/collection/src/index.ts @@ -666,6 +666,57 @@ export class Collection extends Map { return coll; } + /** + * Merges two Collections together into a new Collection. + * @param other The other Collection to merge with + * @param whenInSelf Function getting the result if the entry only exists in this Collection + * @param whenInOther Function getting the result if the entry only exists in the other Collection + * @param whenInBoth Function getting the result if the entry exists in both Collections + * + * @example + * // Sums up the entries in two collections. + * coll.merge( + * other, + * x => ({ keep: true, value: x }), + * y => ({ keep: true, value: y }), + * (x, y) => ({ keep: true, value: x + y }), + * ); + * + * @example + * // Intersects two collections in a left-biased manner. + * coll.merge( + * other, + * x => ({ keep: false }), + * y => ({ keep: false }), + * (x, _) => ({ keep: true, value: x }), + * ); + */ + public merge( + other: ReadonlyCollection, + whenInSelf: (value: V, key: K) => Keep, + whenInOther: (valueOther: T, key: K) => Keep, + whenInBoth: (value: V, valueOther: T, key: K) => Keep, + ): Collection { + const coll = new this.constructor[Symbol.species](); + const keys = new Set([...this.keys(), ...other.keys()]); + for (const k of keys) { + const hasInSelf = this.has(k); + const hasInOther = other.has(k); + + if (hasInSelf && hasInOther) { + const r = whenInBoth(this.get(k)!, other.get(k)!, k); + if (r.keep) coll.set(k, r.value); + } else if (hasInSelf) { + const r = whenInSelf(this.get(k)!, k); + if (r.keep) coll.set(k, r.value); + } else if (hasInOther) { + const r = whenInOther(other.get(k)!, k); + if (r.keep) coll.set(k, r.value); + } + } + return coll; + } + /** * The sorted method sorts the items of a collection and returns it. * The sort is not necessarily stable in Node 10 or older. @@ -690,8 +741,37 @@ export class Collection extends Map { private static defaultSort(firstValue: V, secondValue: V): number { return Number(firstValue > secondValue) || Number(firstValue === secondValue) - 1; } + + /** + * Creates a Collection from a list of entries. + * @param entries The list of entries + * @param combine Function to combine an existing entry with a new one + * + * @example + * Collection.combineEntries([["a", 1], ["b", 2], ["a", 2]], (x, y) => x + y); + * // returns Collection { "a" => 3, "b" => 2 } + */ + public static combineEntries( + entries: Iterable<[K, V]>, + combine: (firstValue: V, secondValue: V, key: K) => V, + ): Collection { + const coll = new Collection(); + for (const [k, v] of entries) { + if (coll.has(k)) { + coll.set(k, combine(coll.get(k)!, v, k)); + } else { + coll.set(k, v); + } + } + return coll; + } } +/** + * @internal + */ +export type Keep = { keep: true; value: V } | { keep: false }; + /** * @internal */