diff --git a/asynciterator.ts b/asynciterator.ts index cdeec24..bf54129 100644 --- a/asynciterator.ts +++ b/asynciterator.ts @@ -478,6 +478,24 @@ export class AsyncIterator extends EventEmitter { }); } + /** + * Returns a new iterator containing all of the unique items in the original iterator. + * @param by - The derived value by which to determine uniqueness (e.g., stringification). + Defaults to the identity function. + * @returns An iterator with duplicates filtered out. + */ + uniq(by: (item: T) => any = identity): AsyncIterator { + const uniques = new Set(); + return this.filter(function (this: AsyncIterator, item) { + const hashed = by.call(this, item); + if (!uniques.has(hashed)) { + uniques.add(hashed); + return true; + } + return false; + }); + } + /** Prepends the items after those of the current iterator. After this operation, only read the returned iterator instead of the current one. diff --git a/test/MappingIterator-test.js b/test/MappingIterator-test.js index 7d8974f..ef1aab6 100644 --- a/test/MappingIterator-test.js +++ b/test/MappingIterator-test.js @@ -874,6 +874,69 @@ describe('MappingIterator', () => { }); }); + describe('The AsyncIterator#uniq function', () => { + it('should be a function', () => { + expect(AsyncIterator.prototype.uniq).to.be.a('function'); + }); + + describe('when called on an iterator', () => { + let iterator, result; + before(() => { + iterator = new ArrayIterator([1, 1, 2, 1, 1, 2, 2, 3, 3, 3, 3]); + result = iterator.uniq(); + }); + + describe('the return value', () => { + const items = []; + before(done => { + result.on('data', item => { items.push(item); }); + result.on('end', done); + }); + + it('should be a MappingIterator', () => { + result.should.be.an.instanceof(MappingIterator); + }); + + it('only contains unique items', () => { + items.should.deep.equal([1, 2, 3]); + }); + }); + }); + + describe('when called with a hashing function', () => { + let iterator, hash, result; + before(() => { + iterator = new ArrayIterator([{ x: 1 }, { x: 1 }, { x: 1 }]); + hash = sinon.spy(x => JSON.stringify(x)); + result = iterator.uniq(hash); + }); + + describe('the return value', () => { + const items = []; + before(done => { + result.on('data', item => { items.push(item); }); + result.on('end', done); + }); + + it('should be a MappingIterator', () => { + result.should.be.an.instanceof(MappingIterator); + }); + + it('only contains unique items', () => { + items.should.deep.equal([{ x: 1 }]); + }); + + it('should call the hash function once for each item', () => { + hash.should.have.been.calledThrice; + }); + + it('should call the hash function with the returned iterator as `this`', () => { + hash.alwaysCalledOn(result).should.be.true; + }); + }); + }); + }); + describe('The AsyncIterator#skip function', () => { it('should be a function', () => { expect(AsyncIterator.prototype.skip).to.be.a('function');