Skip to content

Commit 3c30293

Browse files
committed
feat(sequenceEqual): adds sequenceEqual operator
- adds most basic tests - documentation still required - more tests necessary resolves #1882
1 parent 64ecb9e commit 3c30293

File tree

4 files changed

+315
-0
lines changed

4 files changed

+315
-0
lines changed

spec/operators/sequenceEqual-spec.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//declare const {rxTestScheduler, time, hot, cold, asDiagram, expectObservable, expectSubscriptions, type};
2+
3+
const booleans = { T: true, F: false };
4+
5+
/** @test {sequenceEqual} */
6+
describe('Observable.prototype.sequenceEqual', () => {
7+
it('should return true for two equal sequences', () => {
8+
const s1 = hot('--a--^--b--c--d--e--f--g--|');
9+
const s1subs = '^ !';
10+
const s2 = hot('-----^-----b--c--d-e-f------g-|');
11+
const s2subs = '^ !';
12+
const expected = '-------------------------(T|)';
13+
14+
const source = s1.sequenceEqual(s2);
15+
16+
expectObservable(source).toBe(expected, booleans);
17+
expectSubscriptions(s1.subscriptions).toBe(s1subs);
18+
expectSubscriptions(s2.subscriptions).toBe(s2subs);
19+
});
20+
21+
it('should error if the comparor errors', () => {
22+
const s1 = hot('--a--^--b-----c------d--|');
23+
const s1subs = '^ !';
24+
const s2 = hot('-----^--------x---y---z-------|');
25+
const s2subs = '^ !';
26+
const expected = '-------------#';
27+
28+
let i = 0;
29+
const source = s1.sequenceEqual(s2, (a: any, b: any) => {
30+
if (++i === 2) {
31+
throw new Error('shazbot');
32+
}
33+
return a.value === b.value;
34+
});
35+
36+
const values = {
37+
a: null,
38+
b: { value: 'bees knees' },
39+
c: { value: 'carpy dumb' },
40+
d: { value: 'derp' },
41+
x: { value: 'bees knees', foo: 'lol' },
42+
y: { value: 'carpy dumb', scooby: 'doo' },
43+
z: { value: 'derp', weCouldBe: 'dancin, yeah' }
44+
};
45+
46+
expectObservable(source).toBe(expected, Object.assign({}, booleans, values), new Error('shazbot'));
47+
expectSubscriptions(s1.subscriptions).toBe(s1subs);
48+
expectSubscriptions(s2.subscriptions).toBe(s2subs);
49+
});
50+
51+
it('should use the provided comparor', () => {
52+
const s1 = hot('--a--^--b-----c------d--|');
53+
const s1subs = '^ !';
54+
const s2 = hot('-----^--------x---y---z-------|');
55+
const s2subs = '^ !';
56+
const expected = '-------------------------(T|)';
57+
58+
const source = s1.sequenceEqual(s2, (a: any, b: any) => a.value === b.value);
59+
60+
const values = {
61+
a: null,
62+
b: { value: 'bees knees' },
63+
c: { value: 'carpy dumb' },
64+
d: { value: 'derp' },
65+
x: { value: 'bees knees', foo: 'lol' },
66+
y: { value: 'carpy dumb', scooby: 'doo' },
67+
z: { value: 'derp', weCouldBe: 'dancin, yeah' }
68+
};
69+
70+
expectObservable(source).toBe(expected, Object.assign({}, booleans, values));
71+
expectSubscriptions(s1.subscriptions).toBe(s1subs);
72+
expectSubscriptions(s2.subscriptions).toBe(s2subs);
73+
});
74+
75+
it('should return false for two unequal sequences, compareTo finishing last', () => {
76+
const s1 = hot('--a--^--b--c--d--e--f--g--|');
77+
const s1subs = '^ !';
78+
const s2 = hot('-----^-----b--c--d-e-f------z-|');
79+
const s2subs = '^ !';
80+
const expected = '-----------------------(F|)';
81+
82+
const source = s1.sequenceEqual(s2);
83+
84+
expectObservable(source).toBe(expected, booleans);
85+
expectSubscriptions(s1.subscriptions).toBe(s1subs);
86+
expectSubscriptions(s2.subscriptions).toBe(s2subs);
87+
});
88+
89+
it('should return false for two unequal sequences, early wrong value from source', () => {
90+
const s1 = hot('--a--^--b--c---x-----------|');
91+
const s1subs = '^ !';
92+
const s2 = hot('-----^--b--c--d--e--f--|');
93+
const s2subs = '^ !';
94+
const expected = '----------(F|)';
95+
96+
const source = s1.sequenceEqual(s2);
97+
98+
expectObservable(source).toBe(expected, booleans);
99+
expectSubscriptions(s1.subscriptions).toBe(s1subs);
100+
expectSubscriptions(s2.subscriptions).toBe(s2subs);
101+
});
102+
103+
it('should return false when the source emits an extra value after the compareTo completes', () => {
104+
const s1 = hot('--a--^--b--c--d--e--f--g--h--|');
105+
const s1subs = '^ !';
106+
const s2 = hot('-----^--b--c--d-|');
107+
const s2subs = '^ !';
108+
const expected = '------------(F|)';
109+
110+
const source = s1.sequenceEqual(s2);
111+
112+
expectObservable(source).toBe(expected, booleans);
113+
expectSubscriptions(s1.subscriptions).toBe(s1subs);
114+
expectSubscriptions(s2.subscriptions).toBe(s2subs);
115+
});
116+
117+
it('should return false when the compareTo emits an extra value after the source completes', () => {
118+
const s1 = hot('--a--^--b--c--d-|');
119+
const s1subs = '^ !';
120+
const s2 = hot('-----^--b--c--d--e--f--g--h--|');
121+
const s2subs = '^ !';
122+
const expected = '------------(F|)';
123+
124+
const source = s1.sequenceEqual(s2);
125+
126+
expectObservable(source).toBe(expected, booleans);
127+
expectSubscriptions(s1.subscriptions).toBe(s1subs);
128+
expectSubscriptions(s2.subscriptions).toBe(s2subs);
129+
});
130+
131+
it('should return true for two empty observables', () => {
132+
const s1 = cold('|');
133+
const s2 = cold('|');
134+
const expected = '(T|)';
135+
136+
const source = s1.sequenceEqual(s2);
137+
expectObservable(source).toBe(expected, booleans);
138+
});
139+
140+
it('should return false for an empty observable and an observable that emits', () => {
141+
const s1 = cold('|');
142+
const s2 = cold('---a--|');
143+
const expected = '---(F|)';
144+
145+
const source = s1.sequenceEqual(s2);
146+
expectObservable(source).toBe(expected, booleans);
147+
});
148+
149+
it('should return compare hot and cold observables', () => {
150+
const s1 = hot('---a--^---b---c---d---e---f---g---h---i---j---|');
151+
const s2 = cold( '----b---c-|');
152+
const expected1 = '------------(F|)';
153+
const subs1 = '^ !';
154+
const delay = '-------------------|';
155+
const s3 = cold( '-f---g---h---i---j---|');
156+
const expected2 = ' ---------------------(T|)';
157+
const subs2 = ' ^ !';
158+
159+
const test1 = s1.sequenceEqual(s2);
160+
const test2 = s1.sequenceEqual(s3);
161+
162+
expectObservable(test1).toBe(expected1, booleans);
163+
rxTestScheduler.schedule(() => expectObservable(test2).toBe(expected2, booleans), time(delay));
164+
expectSubscriptions(s2.subscriptions).toBe(subs1);
165+
expectSubscriptions(s3.subscriptions).toBe(subs2);
166+
});
167+
});

src/Rx.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ import './add/operator/retryWhen';
109109
import './add/operator/sample';
110110
import './add/operator/sampleTime';
111111
import './add/operator/scan';
112+
import './add/operator/sequenceEqual';
112113
import './add/operator/share';
113114
import './add/operator/single';
114115
import './add/operator/skip';

src/add/operator/sequenceEqual.ts

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

src/operator/sequenceEqual.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import {Operator} from '../Operator';
2+
import {Observer} from '../Observer';
3+
import {Observable} from '../Observable';
4+
import {Subscriber} from '../Subscriber';
5+
import {tryCatch} from '../util/tryCatch';
6+
import {errorObject} from '../util/errorObject';
7+
8+
export const defaultComparor = <T>(a: T, b: T) => a === b;
9+
10+
export interface SequenceEqualSignature<T> {
11+
(compareTo: Observable<T>, comparor?: (a: T, b: T) => boolean): Observable<boolean>;
12+
}
13+
14+
export function sequenceEqual<T>(compareTo: Observable<T>,
15+
comparor: (a: T, b: T) => boolean = defaultComparor): Observable<boolean> {
16+
return this.lift(new SequenceEqualOperator(compareTo, comparor));
17+
}
18+
19+
export class SequenceEqualOperator<T> implements Operator<T, T> {
20+
constructor(private compareTo: Observable<T>,
21+
private comparor: (a: T, b: T) => boolean) {
22+
}
23+
24+
call(subscriber: Subscriber<T>, source: any): any {
25+
return source._subscribe(new SequenceEqualSubscriber(subscriber, this.compareTo, this.comparor));
26+
}
27+
}
28+
29+
/**
30+
* We need this JSDoc comment for affecting ESDoc.
31+
* @ignore
32+
* @extends {Ignored}
33+
*/
34+
export class SequenceEqualSubscriber<T, R> extends Subscriber<T> {
35+
private _a: T[] = [];
36+
private _b: T[] = [];
37+
private _oneComplete = false;
38+
39+
constructor(destination: Observer<R>,
40+
private compareTo: Observable<T>,
41+
private comparor: (a: T, b: T) => boolean) {
42+
super(destination);
43+
this.add(compareTo.subscribe(new SequenceEqualCompareToSubscriber(destination, this)));
44+
}
45+
46+
protected _next(value: T): void {
47+
if (this._oneComplete && this._b.length === 0) {
48+
this.emit(false);
49+
} else {
50+
this._a.push(value);
51+
this.checkValues();
52+
}
53+
}
54+
55+
public _complete(): void {
56+
if (this._oneComplete) {
57+
const { _a, _b, comparor } = this;
58+
if (_a.length !== _b.length) {
59+
this.emit(false);
60+
} else {
61+
const len = _a.length;
62+
for (let i = 0; i < len; i++) {
63+
let areEqual = false;
64+
let a = _a[i];
65+
let b = _b[i];
66+
if (comparor) {
67+
areEqual = tryCatch(comparor)(a, b);
68+
if (areEqual === errorObject) {
69+
this.destination.error(errorObject.e);
70+
return;
71+
}
72+
}
73+
if (!areEqual) {
74+
this.emit(false);
75+
}
76+
}
77+
this.emit(true);
78+
}
79+
} else {
80+
this._oneComplete = true;
81+
}
82+
}
83+
84+
checkValues() {
85+
const { _a, _b, comparor } = this;
86+
while (_a.length > 0 && _b.length > 0) {
87+
let a = _a.shift();
88+
let b = _b.shift();
89+
let areEqual = false;
90+
if (comparor) {
91+
areEqual = tryCatch(comparor)(a, b);
92+
if (areEqual === errorObject) {
93+
this.destination.error(errorObject.e);
94+
}
95+
} else {
96+
areEqual = a === b;
97+
}
98+
if (!areEqual) {
99+
this.emit(false);
100+
}
101+
}
102+
}
103+
104+
emit(value: boolean) {
105+
const { destination } = this;
106+
destination.next(value);
107+
destination.complete();
108+
}
109+
110+
nextB(value: T) {
111+
if (this._oneComplete && this._a.length === 0) {
112+
this.emit(false);
113+
} else {
114+
this._b.push(value);
115+
this.checkValues();
116+
}
117+
}
118+
}
119+
120+
class SequenceEqualCompareToSubscriber<T, R> extends Subscriber<T> {
121+
constructor(destination: Observer<R>, private parent: SequenceEqualSubscriber<T, R>) {
122+
super(destination);
123+
}
124+
125+
protected _next(value: T): void {
126+
this.parent.nextB(value);
127+
}
128+
129+
protected _error(err: any): void {
130+
this.parent.error(err);
131+
}
132+
133+
protected _complete(): void {
134+
this.parent._complete();
135+
}
136+
}

0 commit comments

Comments
 (0)