Skip to content

Commit

Permalink
Add new itertool: dupes()
Browse files Browse the repository at this point in the history
  • Loading branch information
nvie committed Apr 4, 2024
1 parent cb84d41 commit f1b7c19
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 0 deletions.
3 changes: 3 additions & 0 deletions 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`
Expand Down
10 changes: 10 additions & 0 deletions README.md
Expand Up @@ -432,6 +432,7 @@ Eager version of [izipMany](#izipMany).
- [take](#take)
- [uniqueEverseen](#uniqueEverseen)
- [uniqueJustseen](#uniqueJustseen)
- [dupes](#dupes)

<a name="chunked" href="#chunked">#</a> <b>chunked</b>(iterable: <i>Iterable&lt;T&gt;</i>, size: <i>number</i>): <i>Iterable&lt;T[]&gt;</i> [&lt;&gt;](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js 'Source')

Expand Down Expand Up @@ -525,6 +526,15 @@ Yields elements in order, ignoring serial duplicates.
>>> [...uniqueJustseen('AbBCcAB', s => s.toLowerCase())]
['A', 'b', 'C', 'A', 'B']

<a name="dupes" href="#dupes">#</a> <b>dupes</b>(iterable: <i>Iterable&lt;T&gt;</i>, keyFn?: <i>(item: T) =&gt; Primitive</i></i>): <i>Iterable&lt;T[]&gt;</i> [&lt;&gt;](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)
Expand Down
34 changes: 34 additions & 0 deletions src/more-itertools.ts
Expand Up @@ -239,6 +239,40 @@ export function* uniqueEverseen<T>(
}
}

/**
* 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<T>(
iterable: Iterable<T>,
keyFn: (item: T) => Primitive = primitiveIdentity,
): IterableIterator<T[]> {
const multiples = new Map<Primitive, T[]>();

{
const singles = new Map<Primitive, T>();
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.
*
Expand Down
64 changes: 64 additions & 0 deletions test/more-itertools.test.ts
Expand Up @@ -3,6 +3,7 @@ import { iter, find, range } from "~/builtins";
import { first } from "~/custom";
import {
chunked,
dupes,
flatten,
heads,
intersperse,
Expand Down Expand Up @@ -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" }],
]);
});
});

0 comments on commit f1b7c19

Please sign in to comment.