Skip to content

Commit fee7585

Browse files
authored
fix(reduce/scan): both scan/reduce operators now accepts undefined itself as a valid seed (#2050)
* fix(scan): scan operator now accepts `undefined` itself as a valid seed value Array#reduce supports `undefined` as a valid seed value, so it seems natural that we would too for scan ```js of(1, 2, 3).scan((acc, x) => acc + ' ' + x, undefined); // "undefined 1" // "undefined 1 2" // "undefined 1 2 3" ``` fixes #2047 * fix(reduce): reduce operator now accepts `undefined` itself as a valid seed value Array#reduce supports `undefined` as a valid seed value, so it seems natural that we would too. ```js of(1, 2, 3).reduce((acc, x) => acc + ' ' + x, undefined); // "undefined 1 2 3" ```
1 parent fea08e9 commit fee7585

File tree

4 files changed

+81
-21
lines changed

4 files changed

+81
-21
lines changed

spec/operators/reduce-spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,36 @@ describe('Observable.prototype.reduce', () => {
3535
expectSubscriptions(e1.subscriptions).toBe(e1subs);
3636
});
3737

38+
it('should reduce with a seed of undefined', () => {
39+
const e1 = hot('--a--^--b--c--d--e--f--g--|');
40+
const e1subs = '^ !';
41+
const expected = '---------------------(x|)';
42+
43+
const values = {
44+
x: 'undefined b c d e f g'
45+
};
46+
47+
const source = e1.reduce((acc: any, x: string) => acc + ' ' + x, undefined);
48+
49+
expectObservable(source).toBe(expected, values);
50+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
51+
});
52+
53+
it('should reduce without a seed', () => {
54+
const e1 = hot('--a--^--b--c--d--e--f--g--|');
55+
const e1subs = '^ !';
56+
const expected = '---------------------(x|)';
57+
58+
const values = {
59+
x: 'b c d e f g'
60+
};
61+
62+
const source = e1.reduce((acc: any, x: string) => acc + ' ' + x);
63+
64+
expectObservable(source).toBe(expected, values);
65+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
66+
});
67+
3868
it('should reduce with seed if source is empty', () => {
3969
const e1 = hot('--a--^-------|');
4070
const e1subs = '^ !';

spec/operators/scan-spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,26 @@ describe('Observable.prototype.scan', () => {
4343
expectSubscriptions(e1.subscriptions).toBe(e1subs);
4444
});
4545

46+
it('should scan with a seed of undefined', () => {
47+
const e1 = hot('--a--^--b--c--d--e--f--g--|');
48+
const e1subs = '^ !';
49+
const expected = '---u--v--w--x--y--z--|';
50+
51+
const values = {
52+
u: 'undefined b',
53+
v: 'undefined b c',
54+
w: 'undefined b c d',
55+
x: 'undefined b c d e',
56+
y: 'undefined b c d e f',
57+
z: 'undefined b c d e f g'
58+
};
59+
60+
const source = e1.scan((acc: any, x: string) => acc + ' ' + x, undefined);
61+
62+
expectObservable(source).toBe(expected, values);
63+
expectSubscriptions(e1.subscriptions).toBe(e1subs);
64+
});
65+
4666
it('should scan without seed', () => {
4767
const e1 = hot('--a--^--b--c--d--|');
4868
const e1subs = '^ !';

src/operator/reduce.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,24 @@ export function reduce<T>(this: Observable<T>, accumulator: (acc: T[], value: T,
5353
export function reduce<T, R>(this: Observable<T>, accumulator: (acc: R, value: T, index: number) => R, seed?: R): Observable<R>;
5454
/* tslint:disable:max-line-length */
5555
export function reduce<T, R>(this: Observable<T>, accumulator: (acc: R, value: T) => R, seed?: R): Observable<R> {
56-
return this.lift(new ReduceOperator(accumulator, seed));
56+
let hasSeed = false;
57+
// providing a seed of `undefined` *should* be valid and trigger
58+
// hasSeed! so don't use `seed !== undefined` checks!
59+
// For this reason, we have to check it here at the original call site
60+
// otherwise inside Operator/Subscriber we won't know if `undefined`
61+
// means they didn't provide anything or if they literally provided `undefined`
62+
if (arguments.length >= 2) {
63+
hasSeed = true;
64+
}
65+
66+
return this.lift(new ReduceOperator(accumulator, seed, hasSeed));
5767
}
5868

5969
export class ReduceOperator<T, R> implements Operator<T, R> {
60-
61-
constructor(private accumulator: (acc: R, value: T) => R, private seed?: R) {
62-
}
70+
constructor(private accumulator: (acc: R, value: T) => R, private seed?: R, private hasSeed: boolean = false) {}
6371

6472
call(subscriber: Subscriber<R>, source: any): any {
65-
return source._subscribe(new ReduceSubscriber(subscriber, this.accumulator, this.seed));
73+
return source._subscribe(new ReduceSubscriber(subscriber, this.accumulator, this.seed, this.hasSeed));
6674
}
6775
}
6876

@@ -72,18 +80,15 @@ export class ReduceOperator<T, R> implements Operator<T, R> {
7280
* @extends {Ignored}
7381
*/
7482
export class ReduceSubscriber<T, R> extends Subscriber<T> {
75-
7683
acc: T | R;
77-
hasSeed: boolean;
7884
hasValue: boolean = false;
7985

8086
constructor(destination: Subscriber<R>,
8187
private accumulator: (acc: R, value: T) => R,
82-
seed?: R) {
88+
seed: R,
89+
private hasSeed: boolean) {
8390
super(destination);
8491
this.acc = seed;
85-
this.accumulator = accumulator;
86-
this.hasSeed = typeof seed !== 'undefined';
8792
}
8893

8994
protected _next(value: T) {

src/operator/scan.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,24 @@ export function scan<T>(this: Observable<T>, accumulator: (acc: T[], value: T, i
4545
export function scan<T, R>(this: Observable<T>, accumulator: (acc: R, value: T, index: number) => R, seed?: R): Observable<R>;
4646
/* tslint:disable:max-line-length */
4747
export function scan<T, R>(this: Observable<T>, accumulator: (acc: R, value: T, index: number) => R, seed?: T | R): Observable<R> {
48-
return this.lift(new ScanOperator(accumulator, seed));
48+
let hasSeed = false;
49+
// providing a seed of `undefined` *should* be valid and trigger
50+
// hasSeed! so don't use `seed !== undefined` checks!
51+
// For this reason, we have to check it here at the original call site
52+
// otherwise inside Operator/Subscriber we won't know if `undefined`
53+
// means they didn't provide anything or if they literally provided `undefined`
54+
if (arguments.length >= 2) {
55+
hasSeed = true;
56+
}
57+
58+
return this.lift(new ScanOperator(accumulator, seed, hasSeed));
4959
}
5060

5161
class ScanOperator<T, R> implements Operator<T, R> {
52-
constructor(private accumulator: (acc: R, value: T, index: number) => R, private seed?: T | R) {
53-
}
62+
constructor(private accumulator: (acc: R, value: T, index: number) => R, private seed?: T | R, private hasSeed: boolean = false) {}
5463

5564
call(subscriber: Subscriber<R>, source: any): any {
56-
return source._subscribe(new ScanSubscriber(subscriber, this.accumulator, this.seed));
65+
return source._subscribe(new ScanSubscriber(subscriber, this.accumulator, this.seed, this.hasSeed));
5766
}
5867
}
5968

@@ -64,26 +73,22 @@ class ScanOperator<T, R> implements Operator<T, R> {
6473
*/
6574
class ScanSubscriber<T, R> extends Subscriber<T> {
6675
private index: number = 0;
67-
private accumulatorSet: boolean = false;
68-
private _seed: T | R;
6976

7077
get seed(): T | R {
7178
return this._seed;
7279
}
7380

7481
set seed(value: T | R) {
75-
this.accumulatorSet = true;
82+
this.hasSeed = true;
7683
this._seed = value;
7784
}
7885

79-
constructor(destination: Subscriber<R>, private accumulator: (acc: R, value: T, index: number) => R, seed?: T | R) {
86+
constructor(destination: Subscriber<R>, private accumulator: (acc: R, value: T, index: number) => R, private _seed: T | R, private hasSeed: boolean) {
8087
super(destination);
81-
this.seed = seed;
82-
this.accumulatorSet = typeof seed !== 'undefined';
8388
}
8489

8590
protected _next(value: T): void {
86-
if (!this.accumulatorSet) {
91+
if (!this.hasSeed) {
8792
this.seed = value;
8893
this.destination.next(value);
8994
} else {

0 commit comments

Comments
 (0)