Skip to content

Commit 4ffbbe5

Browse files
martinsikbenlesh
authored andcommitted
feat(skipLast): add skipLast operator (#2316)
* feat(skipLast): adds skipLast operator Adds skipLast operator from RxJS 4. Its internals and tests are based on takeLast for better performance. Closes #1404 * docs(skipLast): updated decision tree, MIGRATION.md and operators.md * docs(skipLast): fix tree.yml as suggested by @staltz * refactor(skipLast): refactored skipLast operator
1 parent 3066c78 commit 4ffbbe5

File tree

9 files changed

+296
-1
lines changed

9 files changed

+296
-1
lines changed

MIGRATION.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ enabling "composite" subscription behavior.
110110
|`shareValue`|No longer implemented|
111111
|`singleInstance`|`share`|
112112
|`skipLastWithTime`|No longer implemented|
113-
|`skipLast`|No longer implemented|
114113
|`skipUntilWithTime`|No longer implemented|
115114
|`slice(start, end)`|`skip(start).take(end - start)`|
116115
|`some`|`first(fn, () => true, false)`|

doc/decision-tree-widget/tree.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ children:
5959
- label: based on custom logic
6060
children:
6161
- label: skipWhile
62+
- label: from the end of the Observable
63+
children:
64+
- label: skipLast
6265
- label: until another Observable emits a value
6366
children:
6467
- label: skipUntil

doc/operators.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ There are operators for different purposes, and they may be categorized as: crea
178178
- [`sampleTime`](../class/es6/Observable.js~Observable.html#instance-method-sampleTime)
179179
- [`single`](../class/es6/Observable.js~Observable.html#instance-method-single)
180180
- [`skip`](../class/es6/Observable.js~Observable.html#instance-method-skip)
181+
- [`skipLast`](../class/es6/Observable.js~Observable.html#instance-method-skipLast)
181182
- [`skipUntil`](../class/es6/Observable.js~Observable.html#instance-method-skipUntil)
182183
- [`skipWhile`](../class/es6/Observable.js~Observable.html#instance-method-skipWhile)
183184
- [`take`](../class/es6/Observable.js~Observable.html#instance-method-take)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
var RxOld = require('rx');
2+
var RxNew = require('../../../../index');
3+
4+
module.exports = function (suite) {
5+
var oldSkipLastWithImmediateScheduler = RxOld.Observable.range(0, 500, RxOld.Scheduler.currentThread).skipLast(50);
6+
var newSkipLastWithImmediateScheduler = RxNew.Observable.range(0, 500, RxNew.Scheduler.queue).skipLast(50);
7+
8+
function _next(x) { }
9+
function _error(e) { }
10+
function _complete() { }
11+
return suite
12+
.add('old skipLast with current thread scheduler', function () {
13+
oldSkipLastWithImmediateScheduler.subscribe(_next, _error, _complete);
14+
})
15+
.add('new skipLast with current thread scheduler', function () {
16+
newSkipLastWithImmediateScheduler.subscribe(_next, _error, _complete);
17+
});
18+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
var RxOld = require('rx');
2+
var RxNew = require('../../../../index');
3+
4+
module.exports = function (suite) {
5+
var oldSkipLastWithImmediateScheduler = RxOld.Observable.range(0, 500, RxOld.Scheduler.immediate).skipLast(50);
6+
var newSkipLastWithImmediateScheduler = RxNew.Observable.range(0, 500).skipLast(50);
7+
8+
function _next(x) { }
9+
function _error(e) { }
10+
function _complete() { }
11+
return suite
12+
.add('old skipLast with immediate scheduler', function () {
13+
oldSkipLastWithImmediateScheduler.subscribe(_next, _error, _complete);
14+
})
15+
.add('new skipLast with immediate scheduler', function () {
16+
newSkipLastWithImmediateScheduler.subscribe(_next, _error, _complete);
17+
});
18+
};

spec/operators/skipLast-spec.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import {expect} from 'chai';
2+
import * as Rx from '../../dist/cjs/Rx';
3+
declare const {hot, cold, asDiagram, expectObservable, expectSubscriptions};
4+
5+
const Observable = Rx.Observable;
6+
7+
/** @test {takeLast} */
8+
describe('Observable.prototype.skipLast', () => {
9+
asDiagram('skipLast(2)')('should skip two values of an observable with many values', () => {
10+
const e1 = cold('--a-----b----c---d--|');
11+
const e1subs = '^ !';
12+
const expected = '-------------a---b--|';
13+
14+
expectObservable(e1.skipLast(2)).toBe(expected);
15+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
16+
});
17+
18+
it('should skip last three values', () => {
19+
const e1 = cold('--a-----b----c---d--|');
20+
const e1subs = '^ !';
21+
const expected = '-----------------a--|';
22+
23+
expectObservable(e1.skipLast(3)).toBe(expected);
24+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
25+
});
26+
27+
it('should skip all values when trying to take larger then source', () => {
28+
const e1 = cold('--a-----b----c---d--|');
29+
const e1subs = '^ !';
30+
const expected = '--------------------|';
31+
32+
expectObservable(e1.skipLast(5)).toBe(expected);
33+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
34+
});
35+
36+
it('should skip all element when try to take exact', () => {
37+
const e1 = cold('--a-----b----c---d--|');
38+
const e1subs = '^ !';
39+
const expected = '--------------------|';
40+
41+
expectObservable(e1.skipLast(4)).toBe(expected);
42+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
43+
});
44+
45+
it('should not skip any values', () => {
46+
const e1 = cold('--a-----b----c---d--|');
47+
const e1subs = '^ !';
48+
const expected = '--a-----b----c---d--|';
49+
50+
expectObservable(e1.skipLast(0)).toBe(expected);
51+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
52+
});
53+
54+
it('should work with empty', () => {
55+
const e1 = cold('|');
56+
const e1subs = '(^!)';
57+
const expected = '|';
58+
59+
expectObservable(e1.skipLast(42)).toBe(expected);
60+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
61+
});
62+
63+
it('should go on forever on never', () => {
64+
const e1 = cold('-');
65+
const e1subs = '^';
66+
const expected = '-';
67+
68+
expectObservable(e1.skipLast(42)).toBe(expected);
69+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
70+
});
71+
72+
it('should skip one value from an observable with one value', () => {
73+
const e1 = hot('---(a|)');
74+
const e1subs = '^ ! ';
75+
const expected = '---| ';
76+
77+
expectObservable(e1.skipLast(1)).toBe(expected);
78+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
79+
});
80+
81+
it('should skip one value from an observable with many values', () => {
82+
const e1 = hot('--a--^--b----c---d--|');
83+
const e1subs = '^ !';
84+
const expected = '--------b---c--|';
85+
86+
expectObservable(e1.skipLast(1)).toBe(expected);
87+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
88+
});
89+
90+
it('should work with empty and early emission', () => {
91+
const e1 = hot('--a--^----|');
92+
const e1subs = '^ !';
93+
const expected = '-----|';
94+
95+
expectObservable(e1.skipLast(42)).toBe(expected);
96+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
97+
});
98+
99+
it('should propagate error from the source observable', () => {
100+
const e1 = hot('---^---#', null, 'too bad');
101+
const e1subs = '^ !';
102+
const expected = '----#';
103+
104+
expectObservable(e1.skipLast(42)).toBe(expected, null, 'too bad');
105+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
106+
});
107+
108+
it('should propagate error from an observable with values', () => {
109+
const e1 = hot('---^--a--b--#');
110+
const e1subs = '^ !';
111+
const expected = '---------#';
112+
113+
expectObservable(e1.skipLast(42)).toBe(expected);
114+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
115+
});
116+
117+
it('should allow unsubscribing explicitly and early', () => {
118+
const e1 = hot('---^--a--b-----c--d--e--|');
119+
const unsub = ' ! ';
120+
const e1subs = '^ ! ';
121+
const expected = '---------- ';
122+
123+
expectObservable(e1.skipLast(42), unsub).toBe(expected);
124+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
125+
});
126+
127+
it('should work with throw', () => {
128+
const e1 = cold('#');
129+
const e1subs = '(^!)';
130+
const expected = '#';
131+
132+
expectObservable(e1.skipLast(42)).toBe(expected);
133+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
134+
});
135+
136+
it('should throw if total is less than zero', () => {
137+
expect(() => { Observable.range(0, 10).skipLast(-1); })
138+
.to.throw(Rx.ArgumentOutOfRangeError);
139+
});
140+
141+
it('should not break unsubscription chain when unsubscribed explicitly', () => {
142+
const e1 = hot('---^--a--b-----c--d--e--|');
143+
const unsub = ' ! ';
144+
const e1subs = '^ ! ';
145+
const expected = '---------- ';
146+
147+
const result = e1
148+
.mergeMap((x: string) => Observable.of(x))
149+
.skipLast(42)
150+
.mergeMap((x: string) => Observable.of(x));
151+
152+
expectObservable(result, unsub).toBe(expected);
153+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
154+
});
155+
});

src/Rx.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ import './add/operator/sequenceEqual';
112112
import './add/operator/share';
113113
import './add/operator/single';
114114
import './add/operator/skip';
115+
import './add/operator/skipLast';
115116
import './add/operator/skipUntil';
116117
import './add/operator/skipWhile';
117118
import './add/operator/startWith';

src/add/operator/skipLast.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Observable } from '../../Observable';
2+
import { skipLast } from '../../operator/skipLast';
3+
4+
Observable.prototype.skipLast = skipLast;
5+
6+
declare module '../../Observable' {
7+
interface Observable<T> {
8+
skipLast: typeof skipLast;
9+
}
10+
}

src/operator/skipLast.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { Operator } from '../Operator';
2+
import { Subscriber } from '../Subscriber';
3+
import { ArgumentOutOfRangeError } from '../util/ArgumentOutOfRangeError';
4+
import { Observable } from '../Observable';
5+
import { TeardownLogic } from '../Subscription';
6+
7+
/**
8+
* Skip the last `count` values emitted by the source Observable.
9+
*
10+
* <img src="./img/skipLast.png" width="100%">
11+
*
12+
* `skipLast` returns an Observable that accumulates a queue with a length
13+
* enough to store the first `count` values. As more values are received,
14+
* values are taken from the front of the queue and produced on the result
15+
* sequence. This causes values to be delayed.
16+
*
17+
* @example <caption>Skip the last 2 values of an Observable with many values</caption>
18+
* var many = Rx.Observable.range(1, 5);
19+
* var skipLastTwo = many.skipLast(2);
20+
* skipLastTwo.subscribe(x => console.log(x));
21+
*
22+
* // Results in:
23+
* // 1 2 3
24+
*
25+
* @see {@link skip}
26+
* @see {@link skipUntil}
27+
* @see {@link skipWhile}
28+
* @see {@link take}
29+
*
30+
* @throws {ArgumentOutOfRangeError} When using `skipLast(i)`, it throws
31+
* ArgumentOutOrRangeError if `i < 0`.
32+
*
33+
* @param {number} count Number of elements to skip from the end of the source Observable.
34+
* @returns {Observable<T>} An Observable that skips the last count values
35+
* emitted by the source Observable.
36+
* @method skipLast
37+
* @owner Observable
38+
*/
39+
export function skipLast<T>(this: Observable<T>, count: number): Observable<T> {
40+
return this.lift(new SkipLastOperator(count));
41+
}
42+
43+
class SkipLastOperator<T> implements Operator<T, T> {
44+
constructor(private _skipCount: number) {
45+
if (this._skipCount < 0) {
46+
throw new ArgumentOutOfRangeError;
47+
}
48+
}
49+
50+
call(subscriber: Subscriber<T>, source: any): TeardownLogic {
51+
if (this._skipCount === 0) {
52+
// If we don't want to skip any values then just subscribe
53+
// to Subscriber without any further logic.
54+
return source.subscribe(new Subscriber(subscriber));
55+
} else {
56+
return source.subscribe(new SkipLastSubscriber(subscriber, this._skipCount));
57+
}
58+
}
59+
}
60+
61+
/**
62+
* We need this JSDoc comment for affecting ESDoc.
63+
* @ignore
64+
* @extends {Ignored}
65+
*/
66+
class SkipLastSubscriber<T> extends Subscriber<T> {
67+
private _ring: T[];
68+
private _count: number = 0;
69+
70+
constructor(destination: Subscriber<T>, private _skipCount: number) {
71+
super(destination);
72+
this._ring = new Array<T>(_skipCount);
73+
}
74+
75+
protected _next(value: T): void {
76+
const skipCount = this._skipCount;
77+
const count = this._count++;
78+
79+
if (count < skipCount) {
80+
this._ring[count] = value;
81+
} else {
82+
const currentIndex = count % skipCount;
83+
const ring = this._ring;
84+
const oldValue = ring[currentIndex];
85+
86+
ring[currentIndex] = value;
87+
this.destination.next(oldValue);
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)