Skip to content

Commit fe4d57f

Browse files
justinwoobenlesh
authored andcommitted
feat(distinctKey): add distinctKey operator
1 parent 94a034d commit fe4d57f

File tree

5 files changed

+310
-0
lines changed

5 files changed

+310
-0
lines changed

doc/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
- [defaultIfEmpty](function/index.html#static-function-defaultIfEmpty)
4343
- [delay](function/index.html#static-function-delay)
4444
- [distinct](function/index.html#static-function-distinct)
45+
- [distinctKey](function/index.html#static-function-distinctKey)
4546
- [distinctUntilChanged](function/index.html#static-function-distinctUntilChanged)
4647
- [distinctUntilKeyChanged](function/index.html#static-function-distinctUntilKeyChanged)
4748
- [do](function/index.html#static-function-do)

spec/operators/distinctKey-spec.js

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/* globals describe, it, expect, expectObservable, expectSubscriptions, cold, hot */
2+
var Rx = require('../../dist/cjs/Rx');
3+
var Observable = Rx.Observable;
4+
5+
describe('Observable.prototype.distinctKey()', function () {
6+
it.asDiagram('distinctKey(\'k\')')('should distinguish between values', function () {
7+
var values = {a: {k: 1}, b: {k: 2}, c: {k: 3}};
8+
var e1 = hot('-a--b-b----a-c-|', values);
9+
var expected = '-a--b--------c-|';
10+
11+
var result = e1.distinctKey('k');
12+
13+
expectObservable(result).toBe(expected, values);
14+
});
15+
16+
it('should distinguish between values', function () {
17+
var values = {a: {val: 1}, b: {val: 2}};
18+
var e1 = hot('--a--a--a--b--b--a--|', values);
19+
var e1subs = '^ !';
20+
var expected = '--a--------b--------|';
21+
22+
expectObservable(e1.distinctKey('val')).toBe(expected, values);
23+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
24+
});
25+
26+
it('should distinguish between values and does not completes', function () {
27+
var values = {a: {val: 1}, b: {val: 2}};
28+
var e1 = hot('--a--a--a--b--b--a-', values);
29+
var e1subs = '^ ';
30+
var expected = '--a--------b-------';
31+
32+
expectObservable(e1.distinctKey('val')).toBe(expected, values);
33+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
34+
});
35+
36+
it('should distinguish between values with key', function () {
37+
var values = {a: {val: 1}, b: {valOther: 1}, c: {valOther: 3}, d: {val: 1}, e: {val: 5}};
38+
var e1 = hot('--a--b--c--d--e--|', values);
39+
var e1subs = '^ !';
40+
var expected = '--a--b--------e--|';
41+
42+
expectObservable(e1.distinctKey('val')).toBe(expected, values);
43+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
44+
});
45+
46+
it('should not compare if source does not have element with key', function () {
47+
var values = {a: {valOther: 1}, b: {valOther: 1}, c: {valOther: 3}, d: {valOther: 1}, e: {valOther: 5}};
48+
var e1 = hot('--a--b--c--d--e--|', values);
49+
var e1subs = '^ !';
50+
var expected = '--a--------------|';
51+
52+
expectObservable(e1.distinctKey('val')).toBe(expected, values);
53+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
54+
});
55+
56+
it('should not completes if source never completes', function () {
57+
var e1 = cold('-');
58+
var e1subs = '^';
59+
var expected = '-';
60+
61+
expectObservable(e1.distinctKey('val')).toBe(expected);
62+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
63+
});
64+
65+
it('should not completes if source does not completes', function () {
66+
var e1 = hot('-');
67+
var e1subs = '^';
68+
var expected = '-';
69+
70+
expectObservable(e1.distinctKey('val')).toBe(expected);
71+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
72+
});
73+
74+
it('should complete if source is empty', function () {
75+
var e1 = cold('|');
76+
var e1subs = '(^!)';
77+
var expected = '|';
78+
79+
expectObservable(e1.distinctKey('val')).toBe(expected);
80+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
81+
});
82+
83+
it('should complete if source does not emit', function () {
84+
var e1 = hot('------|');
85+
var e1subs = '^ !';
86+
var expected = '------|';
87+
88+
expectObservable(e1.distinctKey('val')).toBe(expected);
89+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
90+
});
91+
92+
it('should emit if source emits single element only', function () {
93+
var values = {a: {val: 1}};
94+
var e1 = hot('--a--|', values);
95+
var e1subs = '^ !';
96+
var expected = '--a--|';
97+
98+
expectObservable(e1.distinctKey('val')).toBe(expected, values);
99+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
100+
});
101+
102+
it('should emit if source is scalar', function () {
103+
var values = {a: {val: 1}};
104+
var e1 = Observable.of(values.a);
105+
var expected = '(a|)';
106+
107+
expectObservable(e1.distinctKey('val')).toBe(expected, values);
108+
});
109+
110+
it('should raises error if source raises error', function () {
111+
var values = {a: {val: 1}};
112+
var e1 = hot('--a--a--#', values);
113+
var e1subs = '^ !';
114+
var expected = '--a-----#';
115+
116+
expectObservable(e1.distinctKey('val')).toBe(expected, values);
117+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
118+
});
119+
120+
it('should raises error if source throws', function () {
121+
var e1 = cold('#');
122+
var e1subs = '(^!)';
123+
var expected = '#';
124+
125+
expectObservable(e1.distinctKey('val')).toBe(expected);
126+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
127+
});
128+
129+
it('should not omit if source elements are all different', function () {
130+
var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
131+
var e1 = hot('--a--b--c--d--e--|', values);
132+
var e1subs = '^ !';
133+
var expected = '--a--b--c--d--e--|';
134+
135+
expectObservable(e1.distinctKey('val')).toBe(expected, values);
136+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
137+
});
138+
139+
it('should allow unsubscribing early and explicitly', function () {
140+
var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
141+
var e1 = hot('--a--b--b--d--a--e--|', values);
142+
var e1subs = '^ ! ';
143+
var expected = '--a--b----- ';
144+
var unsub = ' ! ';
145+
146+
var result = e1.distinctKey('val');
147+
148+
expectObservable(result, unsub).toBe(expected, values);
149+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
150+
});
151+
152+
it('should not break unsubscription chains when unsubscribed explicitly', function () {
153+
var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
154+
var e1 = hot('--a--b--b--d--a--e--|', values);
155+
var e1subs = '^ ! ';
156+
var expected = '--a--b----- ';
157+
var unsub = ' ! ';
158+
159+
var result = e1
160+
.mergeMap(function (x) { return Observable.of(x); })
161+
.distinctKey('val')
162+
.mergeMap(function (x) { return Observable.of(x); });
163+
164+
expectObservable(result, unsub).toBe(expected, values);
165+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
166+
});
167+
168+
it('should emit once if source elements are all same', function () {
169+
var values = {a: {val: 1}};
170+
var e1 = hot('--a--a--a--a--a--a--|', values);
171+
var e1subs = '^ !';
172+
var expected = '--a-----------------|';
173+
174+
expectObservable(e1.distinctKey('val')).toBe(expected, values);
175+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
176+
});
177+
178+
it('should emit once if comparer returns true always regardless of source emits', function () {
179+
var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
180+
var e1 = hot('--a--b--c--d--e--|', values);
181+
var e1subs = '^ !';
182+
var expected = '--a--------------|';
183+
184+
expectObservable(e1.distinctKey('val', function () { return true; })).toBe(expected, values);
185+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
186+
});
187+
188+
it('should emit all if comparer returns false always regardless of source emits', function () {
189+
var values = {a: {val: 1}};
190+
var e1 = hot('--a--a--a--a--a--a--|', values);
191+
var e1subs = '^ !';
192+
var expected = '--a--a--a--a--a--a--|';
193+
194+
expectObservable(e1.distinctKey('val', function () { return false; })).toBe(expected, values);
195+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
196+
});
197+
198+
it('should distinguish values by selector', function () {
199+
var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
200+
var e1 = hot('--a--b--c--d--e--|', values);
201+
var e1subs = '^ !';
202+
var expected = '--a-----c-----e--|';
203+
var selector = function (x, y) {
204+
return y % 2 === 0;
205+
};
206+
207+
expectObservable(e1.distinctKey('val', selector)).toBe(expected, values);
208+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
209+
});
210+
211+
it('should raises error when comparer throws', function () {
212+
var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
213+
var e1 = hot('--a--b--c--d--e--|', values);
214+
var e1subs = '^ ! ';
215+
var expected = '--a--b--c--# ';
216+
var selector = function (x, y) {
217+
if (y === 4) {
218+
throw 'error';
219+
}
220+
return x === y;
221+
};
222+
223+
expectObservable(e1.distinctKey('val', selector)).toBe(expected, values);
224+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
225+
});
226+
227+
it('should support a flushing stream', function () {
228+
var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
229+
var e1 = hot('--a--b--a--b--a--b--|', values);
230+
var e1subs = '^ !';
231+
var e2 = hot('-----------x--------|');
232+
var e2subs = '^ !';
233+
var expected = '--a--b--------a--b--|';
234+
var selector = function (x, y) {
235+
return x === y;
236+
};
237+
238+
expectObservable(e1.distinct(selector, e2)).toBe(expected, values);
239+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
240+
expectSubscriptions(e2.subscriptions).toBe(e2subs);
241+
});
242+
243+
it('should unsubscribe from the flushing stream when the main stream is unsubbed', function () {
244+
var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
245+
var e1 = hot('--a--b--a--b--a--b--|', values);
246+
var e1subs = '^ ! ';
247+
var e2 = hot('-----------x--------|');
248+
var e2subs = '^ ! ';
249+
var unsub = ' ! ';
250+
var expected = '--a--b------';
251+
var selector = function (x, y) {
252+
return x === y;
253+
};
254+
255+
expectObservable(e1.distinct(selector, e2), unsub).toBe(expected, values);
256+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
257+
expectSubscriptions(e2.subscriptions).toBe(e2subs);
258+
});
259+
260+
it('should allow opting in to default comparator with flush', function () {
261+
var values = {a: {val: 1}, b: {val: 2}, c: {val: 3}, d: {val: 4}, e: {val: 5}};
262+
var e1 = hot('--a--b--a--b--a--b--|', values);
263+
var e1subs = '^ !';
264+
var e2 = hot('-----------x--------|');
265+
var e2subs = '^ !';
266+
var expected = '--a--b--------a--b--|';
267+
268+
expectObservable(e1.distinct(null, e2)).toBe(expected, values);
269+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
270+
expectSubscriptions(e2.subscriptions).toBe(e2subs);
271+
});
272+
});

src/Rx.KitchenSink.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface KitchenSinkOperators<T> extends CoreOperators<T> {
99
isEmpty?: () => Observable<boolean>;
1010
elementAt?: (index: number, defaultValue?: any) => Observable<T>;
1111
distinct?: (compare?: (x: T, y: T) => boolean, flushes?: Observable<any>) => Observable<T>;
12+
distinctKey?: (key: string, compare?: (x: T, y: T) => boolean, flushes?: Observable<any>) => Observable<T>;
1213
distinctUntilKeyChanged?: (key: string, compare?: (x: any, y: any) => boolean) => Observable<T>;
1314
find?: (predicate: (value: T, index: number, source: Observable<T>) => boolean, thisArg?: any) => Observable<T>;
1415
findIndex?: (predicate: (value: T, index: number, source: Observable<T>) => boolean, thisArg?: any) => Observable<number>;
@@ -65,6 +66,7 @@ import './add/operator/debounceTime';
6566
import './add/operator/defaultIfEmpty';
6667
import './add/operator/delay';
6768
import './add/operator/distinct';
69+
import './add/operator/distinctKey';
6870
import './add/operator/distinctUntilChanged';
6971
import './add/operator/distinctUntilKeyChanged';
7072
import './add/operator/do';

src/add/operator/distinctKey.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Everything in this file is generated by the 'tools/generate-operator-patches.ts' script.
3+
* Any manual edits to this file will be lost next time the script is run.
4+
**/
5+
import {Observable} from '../../Observable';
6+
import {distinctKey} from '../../operator/distinctKey';
7+
import {KitchenSinkOperators} from '../../Rx.KitchenSink';
8+
9+
const observableProto = (<KitchenSinkOperators<any>>Observable.prototype);
10+
observableProto.distinctKey = distinctKey;
11+
12+
export var _void: void;

src/operator/distinctKey.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {distinct} from './distinct';
2+
import {Observable} from '../Observable';
3+
4+
/**
5+
* Returns an Observable that emits all items emitted by the source Observable that are distinct by comparison from previous items,
6+
* using a property accessed by using the key provided to check if the two items are distinct.
7+
* If a comparator function is provided, then it will be called for each item to test for whether or not that value should be emitted.
8+
* If a comparator function is not provided, an equality check is used by default.
9+
* As the internal HashSet of this operator grows larger and larger, care should be taken in the domain of inputs this operator may see.
10+
* An optional paramter is also provided such that an Observable can be provided to queue the internal HashSet to flush the values it holds.
11+
* @param {string} key string key for object property lookup on each item.
12+
* @param {function} [compare] optional comparison function called to test if an item is distinct from previous items in the source.
13+
* @param {Observable} [flushes] optional Observable for flushing the internal HashSet of the operator.
14+
* @returns {Observable} an Observable that emits items from the source Observable with distinct values.
15+
*/
16+
export function distinctKey<T>(key: string, compare?: (x: T, y: T) => boolean, flushes?: Observable<any>): Observable<T> {
17+
return distinct.call(this, function(x: T, y: T) {
18+
if (compare) {
19+
return compare(x[key], y[key]);
20+
}
21+
return x[key] === y[key];
22+
}, flushes);
23+
}

0 commit comments

Comments
 (0)