diff --git a/asynciterator.ts b/asynciterator.ts index 3ec2ab6..2c486d1 100644 --- a/asynciterator.ts +++ b/asynciterator.ts @@ -1923,3 +1923,116 @@ type SourceExpression = type InternalSource = AsyncIterator & { _destination: AsyncIterator }; + +interface Transform { filter: boolean, function: Function } + +function build(transforms: Transform[]) { + return transforms.reduceRight((f, transform) => transform.filter ? + (item: any) => transform.function(item) ? f(item) : null : + (item: any) => f(transform.function(item)), (e: any) => e); +} + +/** + An iterator that is used to quickly transform items, it is optimized for performance + and uses *mutations* - this means that it is unsafe to do anything with the original iterator + after map, transform and syncTransform have been applied to it. + @param source The source to transform + @extends module:asynciterator.AsyncIterator +*/ +export class FastTransformIterator extends AsyncIterator { + private transforms: Transform[] = []; + constructor(private source: AsyncIterator) { + super(); + source.on('readable', () => { + this.emit('readable'); + }); + source.on('end', () => { + this.close(); + }); + } + + read(): T | null { + const func = build(this.transforms); + + this.read = () => { + const { source } = this; + let item; + while ((item = source.read()) !== null) { + if ((item = func(item)) !== null) + return item; + } + return null; + }; + + return this.read(); + } + + /** + Filter items according to a given function + @param {Function} filter The function to filter items with + */ + filter(filter: (item: T) => item is K): FastTransformIterator; + filter(filter: (item: T) => boolean): FastTransformIterator; + filter(filter: (item: T) => boolean): FastTransformIterator { + this.transforms.push({ filter: true, function: filter }); + return this as unknown as FastTransformIterator; + } + + /** + Maps items according to a given function + @param {Function} map The function to map items with + */ + map(map: (item: T) => D): FastTransformIterator { + this.transforms.push({ filter: false, function: map }); + return this as unknown as FastTransformIterator; + } + + /** + Transforms items according to a synchronous generator (hence no need for buffering) + @param {Function} transform The function to transform items with + */ + syncTransform(transform: (item: T) => Generator): FastTransformIterator { + const { source } = this; + + // Build the map-filter transformation pipeline between the current source and the use + // of this generator. + const func = build(this.transforms); + this.transforms = []; + + let transformation: Generator | null; + + // Override the current source with a new source that applies the generator mapping + // @ts-ignore + this.source = { + read(): D | null { + let item: any; + + // eslint-disable-next-line no-constant-condition + while (true) { + // If we are not currently using a generator then get one + if (!transformation) { + // Get the first non-null upstream item + while ((item = source.read()) !== null) { + if ((item = func(item)) !== null) + break; + } + + // If we cannot get a non-null item from the + // source then return null + if (item === null) + return item; + + // Otherwise create a new generator + transformation = transform(item); + } + + if (!(item = transformation.next()).done) + return item.value; + transformation = null; + } + }, + } as unknown as AsyncIterator; + + return this as unknown as FastTransformIterator; + } +} diff --git a/test/FastTransformIterator.js b/test/FastTransformIterator.js new file mode 100644 index 0000000..33a9d8d --- /dev/null +++ b/test/FastTransformIterator.js @@ -0,0 +1,76 @@ +import { + FastTransformIterator, + range, +} from '../dist/asynciterator.js'; + +describe('FastTransformIterator', () => { + let iterator; + beforeEach(() => { + iterator = new FastTransformIterator(range(0, 2)); + }); + it('Should handle no transforms', async () => { + iterator.read().should.equal(0); + iterator.read().should.equal(1); + iterator.read().should.equal(2); + }); + it('Should handle no transforms arrayified', async () => { + (await iterator.toArray()).should.deep.equal([0, 1, 2]); + }); + it('Should apply maps that doubles correctly', async () => { + (await iterator.map(x => x * 2).toArray()).should.deep.equal([0, 2, 4]); + }); + it('Should apply maps that doubles correctly', async () => { + (await iterator.map(x => `x${x}`).toArray()).should.deep.equal(['x0', 'x1', 'x2']); + }); + it('Should apply filter correctly', async () => { + (await iterator.filter(x => x % 2 === 0).toArray()).should.deep.equal([0, 2]); + }); + it('Should apply filter then map correctly', async () => { + (await iterator.filter(x => x % 2 === 0).map(x => `x${x}`).toArray()).should.deep.equal(['x0', 'x2']); + }); + it('Should apply map then filter correctly (1)', async () => { + (await iterator.map(x => x).filter(x => x % 2 === 0).toArray()).should.deep.equal([0, 2]); + }); + it('Should apply map then filter to false correctly', async () => { + (await iterator.map(x => `x${x}`).filter(x => true).toArray()).should.deep.equal(['x0', 'x1', 'x2']); + }); + it('Should apply map then filter to true correctly', async () => { + (await iterator.map(x => `x${x}`).filter(x => false).toArray()).should.deep.equal([]); + }); + it('Should apply filter to false then map correctly', async () => { + (await iterator.filter(x => true).map(x => `x${x}`).toArray()).should.deep.equal(['x0', 'x1', 'x2']); + }); + it('Should apply filter to true then map correctly', async () => { + (await iterator.filter(x => false).map(x => `x${x}`).filter(x => false).toArray()).should.deep.equal([]); + }); + it('Should apply filter one then double', async () => { + (await iterator.filter(x => x !== 1).map(x => x * 2).toArray()).should.deep.equal([0, 4]); + }); + it('Should apply double then filter one', async () => { + (await iterator.map(x => x * 2).filter(x => x !== 1).toArray()).should.deep.equal([0, 2, 4]); + }); + it('Should apply map then filter correctly', async () => { + (await iterator.map(x => `x${x}`).filter(x => (x[1] === '0')).toArray()).should.deep.equal(['x0']); + }); + it('Should handle transforms', async () => { + iterator = iterator.syncTransform(function* (data) { + yield `x${data}`; + yield `y${data}`; + }); + (await iterator.toArray()).should.deep.equal(['x0', 'y0', 'x1', 'y1', 'x2', 'y2']); + }); + it('Should handle transforms and maps', async () => { + iterator = iterator.syncTransform(function* (data) { + yield `x${data}`; + yield `y${data}`; + }).map(x => `z${x}`); + (await iterator.toArray()).should.deep.equal(['zx0', 'zy0', 'zx1', 'zy1', 'zx2', 'zy2']); + }); + it('Should handle maps and transforms', async () => { + iterator = iterator.map(x => `z${x}`).syncTransform(function* (data) { + yield `x${data}`; + yield `y${data}`; + }); + (await iterator.toArray()).should.deep.equal(['xz0', 'yz0', 'xz1', 'yz1', 'xz2', 'yz2']); + }); +});