Skip to content
This repository has been archived by the owner on Oct 1, 2020. It is now read-only.

Commit

Permalink
Merge pull request #4 from surol/revertible-iterable
Browse files Browse the repository at this point in the history
Revertible iterables support
  • Loading branch information
surol authored Nov 2, 2018
2 parents a90ffe8 + b1fc6d2 commit c3d08ff
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 16 deletions.
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ expect([...it.map(item => item * item)]).toBe([1, 4, 9]);
```


`RevertibleIterable`
--------------------

The library contains a `RevertibleIterable` implemented by `AIterable`. It extends the standard `Iterable` interface
with `reverse()` method, that constructs an iterable containing original iterable's elements in reverse order.

Arrays implement this interface. Note however, that the array counterpart reverses elements _in place_ rather than
creating a new array.


API
___

Expand Down Expand Up @@ -96,6 +106,11 @@ Converts the source `Iterable` to `AIterable`.
Unlike [AIterable.of()] this function always creates new iterable instance. This may be useful when converting array
and iterating over its elements. This way new array instances won't be created.

If the `source` iterable is an array, then uses `reverseArray()` function to revert the constructed iterable.
If the `source` iterable is revertible, then uses its `reverse()` method to revert the constructed one.
Otherwise implements reversion with default technique. I.e. by storing elements to array and reverting them
with `reverseArray()` function.

```TypeScript
import { AIterable, itsFirst } from 'a-iterable';

Expand Down Expand Up @@ -195,6 +210,25 @@ numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0); //

[Array.prototype.reduce()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce

### `reverse()`

Constructs an iterable containing this iterable's elements in reverse order.

Corresponds to [Array.prototype.reverse()]. Note however, that the array counterpart reverses elements _in place_
rather than creating a new array.

```TypeScript
import { AIterable } from 'a-iterable';

const numbers = [1, 2, 3, 4];
const iter1 = AIterable.from(numbers);

iter1.reverse(); // [4, 3, 2, 1], `numbers` are also reverted.
```


[Array.prototype.reverse()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse


Utilities
---------
Expand All @@ -219,3 +253,28 @@ import { AIterable, itsFirst } from 'a-iterable';

itsFirst(AIterable.from([1, 2, 3]).filter(x => x === 2)); // `Array.find()` analog
```


### `itsLast()`

Returns the last element of the given iterable.

If the given iterable is an array, then just returns its last element. If it is revertible, then extracts the first
element of reverted iterable. Otherwise iterates over elements to find the last one.

```TypeScript
import { itsLast } from 'a-iterable';

itsLast([1, 2, 3]); // 3
```


### `reverseArray()`

Constructs an iterable of array elements in reverse order.

```TypeScript
import { reverseArray } from 'a-iterable';

reverseArray([1, 2 ,3]); // [3, 2, 1]
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "a-iterable",
"version": "1.0.2",
"version": "1.1.0",
"description": "An Iterable implementation with ES6 Array-like API",
"keywords": [
"iterable",
Expand Down
63 changes: 56 additions & 7 deletions src/a-iterable.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
import { AIterable } from './a-iterable';
import { RevertibleIterable } from './revertible-iterable';
import SpyObj = jasmine.SpyObj;

describe('AIterable', () => {

let elements: number[];
let iter: AIterable<number>;
let empty: AIterable<number>;
let none: AIterable<number>;

beforeEach(() => {
elements = [11, 22, 33];
iter = AIterable.of({
[Symbol.iterator]() {
return elements[Symbol.iterator]();
return elements.values();
},
});
empty = AIterable.none();
none = AIterable.none();
});

describe('none', () => {
it('has no elements', () => {
expect([...none]).toEqual([]);
});
describe('reverse', () => {
it('returns the same instance', () => {
expect(none.reverse()).toBe(none);
});
});
});

describe('is', () => {
Expand Down Expand Up @@ -49,11 +62,24 @@ describe('AIterable', () => {
expect<Iterable<number>>(AIterable.from(elements)).not.toBe(elements);
expect([...AIterable.from(elements)]).toEqual(elements);
});
it('converts revertible iterable to `AIterable`', () => {

const it: RevertibleIterable<number> = {
[Symbol.iterator]() {
return elements.values();
},
reverse() {
throw new Error('Unsupported');
},
};

expect([...AIterable.from(it)]).toEqual(elements);
});
});

describe('every', () => {
it('returns `true` for empty iterable', () => {
expect(empty.every(() => false)).toBe(true);
expect(none.every(() => false)).toBe(true);
});
it('returns `true` when all elements pass the test', () => {
expect(iter.every(element => element > 0)).toBe(true);
Expand All @@ -68,7 +94,7 @@ describe('AIterable', () => {
expect([...iter.filter(element => element > 11)]).toEqual([22, 33]);
});
it('does not filter empty iterable', () => {
expect([...empty.filter(() => true)]).toEqual([]);
expect([...none.filter(() => true)]).toEqual([]);
});
});

Expand All @@ -91,7 +117,7 @@ describe('AIterable', () => {

const spy = jasmine.createSpy('action');

empty.forEach(spy);
none.forEach(spy);

expect(spy).not.toHaveBeenCalled();
});
Expand All @@ -108,7 +134,30 @@ describe('AIterable', () => {
expect(iter.reduce((prev, element) => prev + element, 1)).toBe(67);
});
it('returns initial value on empty iterable', () => {
expect(empty.reduce((prev, element) => prev + element, 1)).toBe(1);
expect(none.reduce((prev, element) => prev + element, 1)).toBe(1);
});
});

describe('reverse', () => {
it('reverts elements', () => {
expect([...iter.reverse()]).toEqual(elements.reverse());
});
it('reverts array elements', () => {
expect([...AIterable.from(elements).reverse()]).toEqual([33, 22, 11]);
});
it('does not revert array elements in-place', () => {
AIterable.from(elements).reverse();
expect(elements).toEqual([11, 22, 33]);
});
it('reverts elements using source `reverse()` method', () => {

const reverted = [...elements].reverse();
const it: SpyObj<RevertibleIterable<number>> = jasmine.createSpyObj('it', ['reverse']);

it.reverse.and.returnValue(reverted);

expect([...AIterable.from(it).reverse()]).toEqual(reverted);
expect(it.reverse).toHaveBeenCalled();
});
});

Expand Down
82 changes: 75 additions & 7 deletions src/a-iterable.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { reverseArray } from './iteration';
import { itsRevertible, RevertibleIterable } from './revertible-iterable';

/**
* Abstract `Iterable` implementation with Array-like iteration operations.
*
* @param <T> E type of elements.
*/
export abstract class AIterable<T> implements Iterable<T> {
export abstract class AIterable<T> implements RevertibleIterable<T> {

/**
* Returns an iterable without elements.
Expand All @@ -24,13 +27,14 @@ export abstract class AIterable<T> implements Iterable<T> {
* @returns `true` is the `source` has all `AIterable` methods (like `Array` or `AIterable` instance),
* or `false` otherwise.
*/
static is<T>(source: Iterable<T>): source is AIterable<T> {
static is<T>(source: Iterable<T> | RevertibleIterable<T>): source is AIterable<T> {
return 'every' in source
&& 'filter' in source
&& 'flatMap' in source
&& 'forEach' in source
&& 'map' in source
&& 'reduce' in source;
&& 'reduce' in source
&& itsRevertible(source);
}

static of<T>(source: T[]): T[];
Expand All @@ -44,7 +48,7 @@ export abstract class AIterable<T> implements Iterable<T> {
* @return Either `source` itself if it implements `AIterable` already (see `is()` method),
* or new `AIterable` instance.
*/
static of<T>(source: Iterable<T>): AIterable<T> | T[] {
static of<T>(source: Iterable<T> | RevertibleIterable<T> | T[]): AIterable<T> | T[] {
if (AIterable.is(source)) {
return source;
}
Expand All @@ -54,21 +58,63 @@ export abstract class AIterable<T> implements Iterable<T> {
/**
* Creates an `AIterable` instance that iterates over the same elements as the given one.
*
* If the `source` iterable is an array, then uses `reverseArray()` function to revert the constructed iterable.
* If the `source` iterable is revertible, then uses its `reverse()` method to revert the constructed one.
* Otherwise implements reversion with default technique. I.e. by storing elements to array and reverting them
* with `reverseArray()` function.
*
* @param source A source iterable.
*
* @return Always new `AIterable` instance.
*/
static from<T>(source: Iterable<T>): AIterable<T> {
static from<T>(source: Iterable<T> | RevertibleIterable<T> | T[]): AIterable<T> {
if (Array.isArray(source)) {

const array: T[] = source;

class ArrayWrapper extends AIterable<T> {

[Symbol.iterator](): Iterator<T> {
return source[Symbol.iterator]();
}

reverse() {
return AIterable.from(reverseArray(array));
}

}

return new ArrayWrapper();
}

if (!itsRevertible(source)) {

class IterableWrapper extends AIterable<T> {

[Symbol.iterator](): Iterator<T> {
return source[Symbol.iterator]();
}

}

class IterableWrapper extends AIterable<T> {
return new IterableWrapper();
}

const revertible: RevertibleIterable<T> = source;

class RevertibleIterableWrapper extends AIterable<T> {

[Symbol.iterator](): Iterator<T> {
return source[Symbol.iterator]();
}

reverse(): AIterable<T> {
return AIterable.from(revertible.reverse());
}

}

return new IterableWrapper();
return new RevertibleIterableWrapper();
}

abstract [Symbol.iterator](): Iterator<T>;
Expand Down Expand Up @@ -203,12 +249,34 @@ export abstract class AIterable<T> implements Iterable<T> {
return reduced;
}

/**
* Constructs an iterable containing this iterable's elements in reverse order.
*
* By default this method converts iterable to array and then reverts its elements with `reverseArray()` function.
*
* @return Reversed iterable instance.
*/
reverse(): AIterable<T> {

const elements = this;

return AIterable.from({
[Symbol.iterator] () {
return reverseArray([...elements])[Symbol.iterator]();
}
});
}

}

class NoneIterable extends AIterable<any> {

*[Symbol.iterator](): Iterator<any> {}

reverse() {
return this;
}

}

const NONE = new NoneIterable();
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './a-iterable';
export * from './iteration';
export * from './revertible-iterable';
40 changes: 39 additions & 1 deletion src/iteration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { itsEmpty, itsFirst } from './iteration';
import { itsEmpty, itsFirst, itsLast, reverseArray } from './iteration';
import { RevertibleIterable } from './revertible-iterable';

describe('itsEmpty', () => {
it('detects empty iterable', () => {
Expand All @@ -17,3 +18,40 @@ describe('itsFirst', () => {
expect(itsFirst([])).toBeUndefined();
});
});

describe('itsLast', () => {
it('finds the last element of array', () => {
expect(itsLast([1, 2, 3])).toBe(3);
});
it('finds the last element of revertible iterable', () => {

const it: RevertibleIterable<number> = {
[Symbol.iterator]() {
throw Error('Unsupported');
},
reverse() {
return [3, 2, 1];
},
};

expect(itsLast(it)).toBe(3);
});
it('finds the last element of simple iterable', () => {

const it: Iterable<number> = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
};

expect(itsLast(it)).toBe(3);
});
});

describe('reverseArray', () => {
it('reverts array elements', () => {
expect([...reverseArray([1, 2, 3])]).toEqual([3, 2, 1]);
});
});
Loading

0 comments on commit c3d08ff

Please sign in to comment.