Repository: mostjs/core (ts)

Issue:
Return canonical empty in zip(), zipArray()
See #152 

Pull request:
Return canonical empty in zip, zipArray
Close #189 

Patch:
diff --git a/packages/core/src/combinator/zip.js b/packages/core/src/combinator/zip.js
index 972adbd..5400126 100644
--- a/packages/core/src/combinator/zip.js
+++ b/packages/core/src/combinator/zip.js
@@ -1,9 +1,7 @@
-/** @license MIT License (c) copyright 2010-2016 original author or authors */
-/** @author Brian Cavalier */
-/** @author John Hann */
+/** @license MIT License (c) copyright 2010 original author or authors */
 
 import { map } from './transform'
-import { empty } from '../source/empty'
+import { empty, containsCanonicalEmpty } from '../source/empty'
 import Pipe from '../sink/Pipe'
 import IndexSink from '../sink/IndexSink'
 import { disposeAll } from '@most/disposable'
@@ -32,7 +30,7 @@ export function zip (f, stream1, stream2) {
 *  using f
 */
 export const zipArray = (f, streams) =>
-  streams.length === 0 ? empty()
+  streams.length === 0 || containsCanonicalEmpty(streams) ? empty()
     : streams.length === 1 ? map(f, streams[0])
     : new Zip(f, streams)
 
diff --git a/packages/core/src/source/empty.js b/packages/core/src/source/empty.js
index a4c0801..c7c0391 100644
--- a/packages/core/src/source/empty.js
+++ b/packages/core/src/source/empty.js
@@ -8,6 +8,9 @@ export const empty = () => EMPTY
 export const isCanonicalEmpty = stream =>
   stream === EMPTY
 
+export const containsCanonicalEmpty = streams =>
+  streams.some(isCanonicalEmpty)
+
 class Empty {
   run (sink, scheduler) {
     return asap(propagateEndTask(sink), scheduler)


Test patch:
diff --git a/packages/core/test/source/empty-test.js b/packages/core/test/source/empty-test.js
index 6bf0c74..fca2f93 100644
--- a/packages/core/test/source/empty-test.js
+++ b/packages/core/test/source/empty-test.js
@@ -2,21 +2,36 @@
 import { describe, it } from 'mocha'
 import { eq, assert } from '@briancavalier/assert'
 
-import { empty, isCanonicalEmpty } from '../../src/source/empty'
+import { empty, isCanonicalEmpty, containsCanonicalEmpty } from '../../src/source/empty'
+import { now } from '../../src/source/now'
 
 import { collectEventsFor, makeEvents } from '../helper/testEnv'
 
 describe('empty', () => {
-  describe('isEmpty', () => {
-    it('should return true given empty()', () => {
+  describe('isCanonicalEmpty', () => {
+    it('given empty(), should return true', () => {
       assert(isCanonicalEmpty(empty()))
     })
 
-    it('should return false given non-empty()', () => {
+    it('given non-empty(), should return false', () => {
       assert(!isCanonicalEmpty(makeEvents(1, 0)))
     })
   })
 
+  describe('containsCanonicalEmpty', () => {
+    it('given empty array, should return false', () => {
+      assert(!containsCanonicalEmpty([]))
+    })
+
+    it('given array containing  empty(), should return true', () => {
+      assert(containsCanonicalEmpty([now(0), empty(), now(1)]))
+    })
+
+    it('given array containing only non-empty(), should return false', () => {
+      assert(!containsCanonicalEmpty([now(0), now(1)]))
+    })
+  })
+
   describe('empty', () => {
     it('should yield no items before end', () => {
       return collectEventsFor(1, empty()).then(eq([]))
diff --git a/packages/core/test/zip-test.js b/packages/core/test/zip-test.js
index f3d601e..435e88b 100644
--- a/packages/core/test/zip-test.js
+++ b/packages/core/test/zip-test.js
@@ -1,11 +1,66 @@
 import { describe, it } from 'mocha'
-import { eq } from '@briancavalier/assert'
+import { assert, eq } from '@briancavalier/assert'
 
-import { zip } from '../src/combinator/zip'
+import { zip, zipArray } from '../src/combinator/zip'
+import { now } from '../src/source/now'
+import { empty, isCanonicalEmpty } from '../src/source/empty'
 
 import { makeEvents, collectEventsFor } from './helper/testEnv'
 
+describe('zipArray', function () {
+  it('given empty array, should return canonical empty', () => {
+    const s = zipArray(Array, [])
+    assert(isCanonicalEmpty(s))
+  })
+
+  it('given array containing canonical empty, should return canonical empty', () => {
+    const s = zipArray(Array, [now(0), empty(), now(1)])
+    assert(isCanonicalEmpty(s))
+  })
+
+  it('should invoke f for each tuple', function () {
+    const n = 3
+    const s = zipArray(Array, [makeEvents(1, n), makeEvents(2, n), makeEvents(3, n)])
+
+    return collectEventsFor(n * 3, s)
+      .then(eq([
+        { time: 0, value: [0, 0, 0] },
+        { time: 3, value: [1, 1, 1] },
+        { time: 6, value: [2, 2, 2] }
+      ]))
+  })
+
+  it('should end when shortest stream ends', function () {
+    const n = 3
+    const a = makeEvents(1, n + 1)
+    const b = makeEvents(1, n)
+    const c = makeEvents(1, n + 1)
+
+    return collectEventsFor(n, zipArray(Array, [a, b, c]))
+      .then(eq([
+        { time: 0, value: [0, 0, 0] },
+        { time: 1, value: [1, 1, 1] },
+        { time: 2, value: [2, 2, 2] }
+      ]))
+  })
+})
+
 describe('zip', function () {
+  it('given one canonical empty stream, should return canonical empty', () => {
+    const s = zip(Array, now(0), empty())
+    assert(isCanonicalEmpty(s))
+  })
+
+  it('given one canonical empty stream, should return canonical empty', () => {
+    const s = zip(Array, empty(), now(1))
+    assert(isCanonicalEmpty(s))
+  })
+
+  it('given two canonical empty streams, should return canonical empty', () => {
+    const s = zip(Array, empty(), empty())
+    assert(isCanonicalEmpty(s))
+  })
+
   it('should invoke f for each tuple', function () {
     const n = 3
     const s = zip(Array, makeEvents(1, n), makeEvents(2, n))