From 87d2e916635d0c478b6ecfcc977f445e0e7073c1 Mon Sep 17 00:00:00 2001 From: Jackson Cheek Date: Tue, 4 May 2021 20:48:27 -0400 Subject: [PATCH 1/2] Zip operator --- Snail/Observable.swift | 44 +++++++++++++ SnailTests/ObservableTests.swift | 105 +++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/Snail/Observable.swift b/Snail/Observable.swift index a3f5f64..bd2ad66 100644 --- a/Snail/Observable.swift +++ b/Snail/Observable.swift @@ -460,4 +460,48 @@ public class Observable: ObservableType { return combined } + + public static func zip(_ input1: Observable, _ input2: Observable) -> Observable<(T, U)> { + let combined = Observable<(T, U)>() + + var input1Result: (value: [T], isComplete: Bool) = ([], false) + var input2Result: (value: [U], isComplete: Bool) = ([], false) + + func triggerIfNeeded() { + guard let value1 = input1Result.value.first, + let value2 = input2Result.value.first else { + return + } + input1Result.value.removeFirst() + input2Result.value.removeFirst() + combined.on(.next((value1, value2))) + } + + func finishIfNeeded() { + guard input1Result.isComplete || input2Result.isComplete else { return } + combined.on(.done) + } + + input1.subscribe(onNext: { + input1Result.value.append($0) + triggerIfNeeded() + }, onError: { + combined.on(.error($0)) + }, onDone: { + input1Result.isComplete = true + finishIfNeeded() + }) + + input2.subscribe(onNext: { + input2Result.value.append($0) + triggerIfNeeded() + }, onError: { + combined.on(.error($0)) + }, onDone: { + input2Result.isComplete = true + finishIfNeeded() + }) + + return combined + } } diff --git a/SnailTests/ObservableTests.swift b/SnailTests/ObservableTests.swift index 6825d3e..39c8532 100644 --- a/SnailTests/ObservableTests.swift +++ b/SnailTests/ObservableTests.swift @@ -924,4 +924,109 @@ class ObservableTests: XCTestCase { subject.on(.next("main - async")) } } + + func testZipNonOptional() { + var received: [String] = [] + + let string = Observable() + let int = Observable() + + let subject = Observable.zip(string, int) + + subject.subscribe(onNext: { string, int in + received.append("\(string): \(int)") + }) + + string.on(.next("The value")) + string.on(.next("The number")) + int.on(.next(1)) + int.on(.next(2)) + string.on(.next("The digit")) + int.on(.next(3)) + int.on(.done) + + XCTAssertEqual(received.count, 3) + XCTAssertEqual(received[0], "The value: 1") + XCTAssertEqual(received[1], "The number: 2") + XCTAssertEqual(received[2], "The digit: 3") + } + + func testZipOptional() { + var received: [String] = [] + + let string = Observable() + let int = Observable() + + let subject = Observable.zip(string, int) + + subject.subscribe(onNext: { string, int in + received.append("\(string ?? ""): \(int ?? 0)") + }) + + string.on(.next("The value")) + string.on(.next("The number")) + int.on(.next(1)) + int.on(.next(nil)) + string.on(.next(nil)) + int.on(.next(3)) + string.on(.done) + + XCTAssertEqual(received.count, 3) + XCTAssertEqual(received[0], "The value: 1") + XCTAssertEqual(received[1], "The number: 0") + XCTAssertEqual(received[2], ": 3") + } + + func testZipCountEqualToSourceWithFewestEmissions() { + var received: [String] = [] + + let string = Observable() + let int = Observable() + + let subject = Observable.zip(string, int) + + subject.subscribe(onNext: { string, int in + received.append("\(string): \(int)") + }) + + string.on(.next("The value")) + string.on(.next("The number")) + int.on(.next(1)) + int.on(.next(2)) + string.on(.next("The digit")) + int.on(.next(3)) + int.on(.next(4)) + int.on(.next(5)) + int.on(.next(6)) + int.on(.done) + + XCTAssertEqual(received.count, 3) + XCTAssertEqual(received[0], "The value: 1") + XCTAssertEqual(received[1], "The number: 2") + XCTAssertEqual(received[2], "The digit: 3") + } + + func testZipError() { + let one = Observable() + let two = Observable() + + let subject = Observable.zip(one, two) + + let exp = expectation(description: "zip forwards error from observable") + subject.subscribe(onError: { _ in exp.fulfill() }) + one.on(.error(TestError.test)) + + waitForExpectations(timeout: 1) + } + + func testZipDone() { + let one = Observable() + let two = Observable() + + var isDone = false + Observable.zip(one, two).subscribe(onDone: { isDone = true }) + + one.on(.done) + XCTAssertTrue(isDone) + } } From dd875368a31cf711c6d80fc10fdd5b94f5834e23 Mon Sep 17 00:00:00 2001 From: Jackson Cheek Date: Tue, 4 May 2021 20:56:50 -0400 Subject: [PATCH 2/2] Unnecessary tuple --- Snail/Observable.swift | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/Snail/Observable.swift b/Snail/Observable.swift index bd2ad66..540eb64 100644 --- a/Snail/Observable.swift +++ b/Snail/Observable.swift @@ -464,42 +464,35 @@ public class Observable: ObservableType { public static func zip(_ input1: Observable, _ input2: Observable) -> Observable<(T, U)> { let combined = Observable<(T, U)>() - var input1Result: (value: [T], isComplete: Bool) = ([], false) - var input2Result: (value: [U], isComplete: Bool) = ([], false) + var input1Result: [T] = [] + var input2Result: [U] = [] func triggerIfNeeded() { - guard let value1 = input1Result.value.first, - let value2 = input2Result.value.first else { + guard let value1 = input1Result.first, + let value2 = input2Result.first else { return } - input1Result.value.removeFirst() - input2Result.value.removeFirst() + input1Result.removeFirst() + input2Result.removeFirst() combined.on(.next((value1, value2))) } - func finishIfNeeded() { - guard input1Result.isComplete || input2Result.isComplete else { return } - combined.on(.done) - } - input1.subscribe(onNext: { - input1Result.value.append($0) + input1Result.append($0) triggerIfNeeded() }, onError: { combined.on(.error($0)) }, onDone: { - input1Result.isComplete = true - finishIfNeeded() + combined.on(.done) }) input2.subscribe(onNext: { - input2Result.value.append($0) + input2Result.append($0) triggerIfNeeded() }, onError: { combined.on(.error($0)) }, onDone: { - input2Result.isComplete = true - finishIfNeeded() + combined.on(.done) }) return combined