From f1b7c1966aa203eacf4d54a58fe4d38841420969 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Fri, 5 Apr 2024 01:39:03 +0200 Subject: [PATCH] Add new itertool: dupes() --- CHANGELOG.md | 3 ++ README.md | 10 ++++++ src/more-itertools.ts | 34 ++++++++++++++++++++ test/more-itertools.test.ts | 64 +++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed5d60a9..b410ddb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +- Add new `dupes(iterable, keyFn?)` function, which returns groups of all + duplicate items in iterable. + ## [2.2.5] - 2024-03-07 - Add missing export for `repeat` diff --git a/README.md b/README.md index e4309c20..fd43c43b 100644 --- a/README.md +++ b/README.md @@ -432,6 +432,7 @@ Eager version of [izipMany](#izipMany). - [take](#take) - [uniqueEverseen](#uniqueEverseen) - [uniqueJustseen](#uniqueJustseen) +- [dupes](#dupes) # chunked(iterable: Iterable<T>, size: number): Iterable<T[]> [<>](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js 'Source') @@ -525,6 +526,15 @@ Yields elements in order, ignoring serial duplicates. >>> [...uniqueJustseen('AbBCcAB', s => s.toLowerCase())] ['A', 'b', 'C', 'A', 'B'] +# dupes(iterable: Iterable<T>, keyFn?: (item: T) => Primitive): Iterable<T[]> [<>](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js 'Source') + +Yield only elements from the input that occur more than once. Needs to consume the entire input before being able to produce the first result. + + >>> [...dupes('AAAABCDEEEFABG')] + [['A', 'A', 'A', 'A', 'A'], ['E', 'E', 'E'], ['B', 'B']] + >>> [...dupes('AbBCcAB', s => s.toLowerCase())] + [['b', 'B', 'B'], ['C', 'c'], ['A', 'A']] + ### Additions - [compact](#compact) diff --git a/src/more-itertools.ts b/src/more-itertools.ts index 91b2da60..1c4cc143 100644 --- a/src/more-itertools.ts +++ b/src/more-itertools.ts @@ -239,6 +239,40 @@ export function* uniqueEverseen( } } +/** + * Yield only elements from the input that occur more than once. Needs to + * consume the entire input before being able to produce the first result. + * + * >>> [...dupes('AAAABCDEEEFABG')] + * [['A', 'A', 'A', 'A', 'A'], ['E', 'E', 'E'], ['B', 'B']] + * >>> [...dupes('AbBCcAB', s => s.toLowerCase())] + * [['b', 'B', 'B'], ['C', 'c'], ['A', 'A']] + * + */ +export function dupes( + iterable: Iterable, + keyFn: (item: T) => Primitive = primitiveIdentity, +): IterableIterator { + const multiples = new Map(); + + { + const singles = new Map(); + for (const item of iterable) { + const key = keyFn(item); + if (multiples.has(key)) { + multiples.get(key)!.push(item); + } else if (singles.has(key)) { + multiples.set(key, [singles.get(key)!, item]); + singles.delete(key); + } else { + singles.set(key, item); + } + } + } + + return multiples.values(); +} + /** * Yields elements in order, ignoring serial duplicates. * diff --git a/test/more-itertools.test.ts b/test/more-itertools.test.ts index 3e83a943..d4438d9d 100644 --- a/test/more-itertools.test.ts +++ b/test/more-itertools.test.ts @@ -3,6 +3,7 @@ import { iter, find, range } from "~/builtins"; import { first } from "~/custom"; import { chunked, + dupes, flatten, heads, intersperse, @@ -392,3 +393,66 @@ describe("uniqueEverseen", () => { expect(Array.from(uniqueEverseen("AbCBBcAb", (s) => s.toLowerCase()))).toEqual(["A", "b", "C"]); }); }); + +describe("dupes", () => { + it("dupes w/ empty list", () => { + expect(Array.from(dupes([]))).toEqual([]); + }); + + it("dupes on a list without dupes", () => { + expect(Array.from(dupes([1, 2, 3, 4, 5]))).toEqual([]); + }); + + it("dupes on a list with dupes", () => { + expect(Array.from(dupes(Array.from("Hello")))).toEqual([["l", "l"]]); + expect(Array.from(dupes(Array.from("AAAABCDEEEFABG")))).toEqual([ + ["A", "A", "A", "A", "A"], + ["E", "E", "E"], + ["B", "B"], + ]); + }); + + it("dupes with a key function", () => { + expect(Array.from(dupes(Array.from("AbBCcABdE"), (s) => s.toLowerCase()))).toEqual([ + ["b", "B", "B"], + ["C", "c"], + ["A", "A"], + ]); + }); + + it("dupes with complex objects and a key function", () => { + expect( + Array.from( + dupes( + [ + { name: "Charlie", surname: "X" }, + { name: "Alice", surname: "Rubrik" }, + { name: "Alice", surname: "Doe" }, + { name: "Bob" }, + ], + (p) => p.name, + ), + ), + ).toEqual([ + [ + { name: "Alice", surname: "Rubrik" }, + { name: "Alice", surname: "Doe" }, + ], + ]); + + expect( + Array.from( + dupes( + [{ name: "Bob" }, { name: "Alice", surname: "Rubrik" }, { name: "Alice", surname: "Doe" }, { name: "Bob" }], + (p) => p.name, + ), + ), + ).toEqual([ + [ + { name: "Alice", surname: "Rubrik" }, + { name: "Alice", surname: "Doe" }, + ], + [{ name: "Bob" }, { name: "Bob" }], + ]); + }); +});