Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions asynciterator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1923,3 +1923,116 @@ type SourceExpression<T> =

type InternalSource<T> =
AsyncIterator<T> & { _destination: AsyncIterator<any> };

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<T> extends AsyncIterator<T> {
private transforms: Transform[] = [];
constructor(private source: AsyncIterator<T>) {
super();
source.on('readable', () => {
this.emit('readable');
});
source.on('end', () => {
this.close();
});
}

read(): T | null {
const func = build(this.transforms);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can cache this instead.


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<K extends T>(filter: (item: T) => item is K): FastTransformIterator<K>;
filter(filter: (item: T) => boolean): FastTransformIterator<T>;
filter(filter: (item: T) => boolean): FastTransformIterator<T> {
this.transforms.push({ filter: true, function: filter });
return this as unknown as FastTransformIterator<T>;
}

/**
Maps items according to a given function
@param {Function} map The function to map items with
*/
map<D>(map: (item: T) => D): FastTransformIterator<D> {
this.transforms.push({ filter: false, function: map });
return this as unknown as FastTransformIterator<D>;
}

/**
Transforms items according to a synchronous generator (hence no need for buffering)
@param {Function} transform The function to transform items with
*/
syncTransform<D>(transform: (item: T) => Generator<D>): FastTransformIterator<D> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the use cases for this transformation? Is this just a multimap?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep!

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<D> | 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<D>;

return this as unknown as FastTransformIterator<D>;
}
}
76 changes: 76 additions & 0 deletions test/FastTransformIterator.js
Original file line number Diff line number Diff line change
@@ -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']);
});
});