Skip to content

Commit

Permalink
Merge 05d483e into 730d272
Browse files Browse the repository at this point in the history
  • Loading branch information
jasdev authored Apr 11, 2021
2 parents 730d272 + 05d483e commit 9d7032e
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 2 deletions.
8 changes: 8 additions & 0 deletions CombineExt.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
BF43CC1725008C45005AFA28 /* IgnoreOutputSetOutputTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF43CC1625008C45005AFA28 /* IgnoreOutputSetOutputTypeTests.swift */; };
BFADDC8125BCE4C200465E9B /* FlatMapBatches.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFADDC8025BCE4C200465E9B /* FlatMapBatches.swift */; };
BFADDC8B25BCE91E00465E9B /* FlatMapBatchesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFADDC8A25BCE91E00465E9B /* FlatMapBatchesTests.swift */; };
BF45214E259C0C610065E60E /* PrefixWhileBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF45214D259C0C610065E60E /* PrefixWhileBehavior.swift */; };
BF452158259C110C0065E60E /* PrefixWhileBehaviorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF452157259C110C0065E60E /* PrefixWhileBehaviorTests.swift */; };
C387777C24E6BBE900FAD2D8 /* Nwise.swift in Sources */ = {isa = PBXBuildFile; fileRef = C387777B24E6BBE900FAD2D8 /* Nwise.swift */; };
C387777F24E6BF8F00FAD2D8 /* NwiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C387777D24E6BF6C00FAD2D8 /* NwiseTests.swift */; };
D836234824EA9446002353AC /* MergeMany.swift in Sources */ = {isa = PBXBuildFile; fileRef = D836234724EA9446002353AC /* MergeMany.swift */; };
Expand Down Expand Up @@ -116,6 +118,8 @@
BF43CC1625008C45005AFA28 /* IgnoreOutputSetOutputTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IgnoreOutputSetOutputTypeTests.swift; sourceTree = "<group>"; };
BFADDC8025BCE4C200465E9B /* FlatMapBatches.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlatMapBatches.swift; sourceTree = "<group>"; };
BFADDC8A25BCE91E00465E9B /* FlatMapBatchesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlatMapBatchesTests.swift; sourceTree = "<group>"; };
BF45214D259C0C610065E60E /* PrefixWhileBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefixWhileBehavior.swift; sourceTree = "<group>"; };
BF452157259C110C0065E60E /* PrefixWhileBehaviorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefixWhileBehaviorTests.swift; sourceTree = "<group>"; };
C387777B24E6BBE900FAD2D8 /* Nwise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Nwise.swift; sourceTree = "<group>"; };
C387777D24E6BF6C00FAD2D8 /* NwiseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NwiseTests.swift; sourceTree = "<group>"; };
"CombineExt::CombineExt::Product" /* CombineExt.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CombineExt.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -251,6 +255,7 @@
C387777B24E6BBE900FAD2D8 /* Nwise.swift */,
OBJ_26 /* Partition.swift */,
OBJ_27 /* PrefixDuration.swift */,
BF45214D259C0C610065E60E /* PrefixWhileBehavior.swift */,
OBJ_28 /* RemoveAllDuplicates.swift */,
OBJ_29 /* SetOutputType.swift */,
BF43CC1425008B4F005AFA28 /* IgnoreOutputSetOutputType.swift */,
Expand Down Expand Up @@ -303,6 +308,7 @@
OBJ_52 /* PartitionTests.swift */,
OBJ_53 /* PassthroughRelayTests.swift */,
OBJ_54 /* PrefixDurationTests.swift */,
BF452157259C110C0065E60E /* PrefixWhileBehaviorTests.swift */,
OBJ_55 /* RemoveAllDuplicatesTests.swift */,
OBJ_56 /* ReplaySubjectTests.swift */,
OBJ_57 /* SetOutputTypeTests.swift */,
Expand Down Expand Up @@ -553,6 +559,7 @@
D836234A24EA9888002353AC /* MergeManyTests.swift in Sources */,
OBJ_125 /* CombineLatestManyTests.swift in Sources */,
BF3D3B67253B88E500D830ED /* IgnoreFailureTests.swift in Sources */,
BF452158259C110C0065E60E /* PrefixWhileBehaviorTests.swift in Sources */,
OBJ_126 /* CreateTests.swift in Sources */,
OBJ_127 /* CurrentValueRelayTests.swift in Sources */,
C387777F24E6BF8F00FAD2D8 /* NwiseTests.swift in Sources */,
Expand Down Expand Up @@ -606,6 +613,7 @@
OBJ_95 /* RemoveAllDuplicates.swift in Sources */,
OBJ_96 /* SetOutputType.swift in Sources */,
OBJ_97 /* ShareReplay.swift in Sources */,
BF45214E259C0C610065E60E /* PrefixWhileBehavior.swift in Sources */,
OBJ_98 /* Toggle.swift in Sources */,
OBJ_99 /* WithLatestFrom.swift in Sources */,
OBJ_100 /* ZipMany.swift in Sources */,
Expand Down
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ All operators, utilities and helpers respect Combine's publisher contract, inclu
* [removeAllDuplicates and removeAllDuplicates(by:) ](#removeAllDuplicates)
* [share(replay:)](#sharereplay)
* [prefix(duration:tolerance:​on:options:)](#prefixduration)
* [toggle()](#toggle)
* [prefix(while:behavior:​)](#prefixwhilebehavior)
* [toggle()](#toggle)
* [nwise(_:) and pairwise()](#nwise)
* [ignoreOutput(setOutputType:)](#ignoreOutputsetOutputType)
* [ignoreFailure](#ignoreFailure)
Expand Down Expand Up @@ -553,7 +554,7 @@ second subscriber: 4

### prefix(duration:)

An overload on `Publisher.prefix` that that republishes values for a provided `duration` (in seconds), and then completes.
An overload on `Publisher.prefix` that republishes values for a provided `duration` (in seconds), and then completes.

```swift
let subject = PassthroughSubject<Int, Never>()
Expand All @@ -579,6 +580,37 @@ DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
3
```

### prefix(while:behavior:)

An overload on `Publisher.prefix(while:)` that allows for inclusion of the first element that doesn’t pass the `while` predicate.

```swift
let subject = PassthroughSubject<Int, Never>()

subscription = subject
.prefix(
while: { $0 % 2 == 0 },
behavior: .inclusive
)
.sink(
receivecompletion: { print($0) },
receiveValue: { print($0) }
)

subject.send(0)
subject.send(2)
subject.send(4)
subject.send(5)
```

```none
0
2
4
5
finished
```

### toggle()

Toggle each boolean element of a publisher collection.
Expand Down
77 changes: 77 additions & 0 deletions Sources/Operators/PrefixWhileBehavior.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// PrefixWhileBehavior.swift
// CombineExt
//
// Created by Jasdev Singh on 29/12/2020.
// Copyright © 2020 Combine Community. All rights reserved.
//

#if canImport(Combine)
import Combine
import Foundation

/// Whether to include the first element that doesn’t pass
/// the `while` predicate passed to `Combine.Publisher.prefix(while:behavior:)`.
public enum PrefixWhileBehavior {
/// Include the first element that doesn’t pass the `while` predicate.
case inclusive

/// Exclude the first element that doesn’t pass the `while` predicate.
case exclusive
}

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public extension Publisher {
/// An overload on `Publisher.prefix(while:)` that allows for inclusion of the first element that doesn’t pass the `while` predicate.
///
/// - parameters:
/// - predicate: A closure that takes an element as its parameter and returns a Boolean value that indicates whether publishing should continue.
/// - behavior: Whether or not to include the first element that doesn’t pass `predicate`.
///
/// - returns: A publisher that passes through elements until the predicate indicates publishing should finish — and optionally that first `predicate`-failing element.
func prefix(
while predicate: @escaping (Output) -> Bool,
behavior: PrefixWhileBehavior = .exclusive
) -> AnyPublisher<Output, Failure> {
switch behavior {
case .exclusive:
return prefix(while: predicate)
.eraseToAnyPublisher()
case .inclusive:
return flatMap { next in
Just(PrefixInclusiveEvent.whileValueOrIncluded(next))
.append(!predicate(next) ? [.end] : [])
.setFailureType(to: Failure.self)
}
.prefix(while: \.isWhileValueOrIncluded)
.compactMap(\.value)
.eraseToAnyPublisher()
}
}
}
#endif

// MARK: - Helpers

private enum PrefixInclusiveEvent<Output> {
case end
case whileValueOrIncluded(Output)

var isWhileValueOrIncluded: Bool {
switch self {
case .end:
return false
case .whileValueOrIncluded:
return true
}
}

var value: Output? {
switch self {
case .end:
return nil
case let .whileValueOrIncluded(inner):
return inner
}
}
}
166 changes: 166 additions & 0 deletions Tests/PrefixWhileBehaviorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
//
// PrefixWhileBehaviorTests.swift
// CombineExt
//
// Created by Jasdev Singh on 29/12/2020.
// Copyright © 2020 Combine Community. All rights reserved.
//

#if !os(watchOS)
import Combine
import CombineExt
import XCTest

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
final class PrefixWhileBehaviorTests: XCTestCase {
private struct SomeError: Error, Equatable {}

private var cancellable: AnyCancellable!

func testExclusiveValueEventsWithFinished() {
let intSubject = PassthroughSubject<Int, Never>()

var values = [Int]()
var completions = [Subscribers.Completion<Never>]()

cancellable = intSubject
.prefix(
while: { $0 % 2 == 0 },
behavior: .exclusive
)
.sink(
receiveCompletion: { completions.append($0) },
receiveValue: { values.append($0) }
)

[0, 2, 4, 5]
.forEach(intSubject.send)

XCTAssertEqual(values, [0, 2, 4])
XCTAssertEqual(completions, [.finished])
}

func testExclusiveValueEventsWithError() {
let intSubject = PassthroughSubject<Int, SomeError>()

var values = [Int]()
var completions = [Subscribers.Completion<SomeError>]()

cancellable = intSubject
.prefix(
while: { $0 % 2 == 0 },
behavior: .exclusive
)
.sink(
receiveCompletion: { completions.append($0) },
receiveValue: { values.append($0) }
)

[0, 2, 4]
.forEach(intSubject.send)

intSubject.send(completion: .failure(.init()))

XCTAssertEqual(values, [0, 2, 4])
XCTAssertEqual(completions, [.failure(.init())])
}

func testInclusiveValueEventsWithStopElement() {
let intSubject = PassthroughSubject<Int, Never>()

var values = [Int]()
var completions = [Subscribers.Completion<Never>]()

cancellable = intSubject
.prefix(
while: { $0 % 2 == 0 },
behavior: .inclusive
)
.sink(
receiveCompletion: { completions.append($0) },
receiveValue: { values.append($0) }
)

[0, 2, 4, 5]
.forEach(intSubject.send)

XCTAssertEqual(values, [0, 2, 4, 5])
XCTAssertEqual(completions, [.finished])
}

func testInclusiveValueEventsWithErrorAfterStopElement() {
let intSubject = PassthroughSubject<Int, SomeError>()

var values = [Int]()
var completions = [Subscribers.Completion<SomeError>]()

cancellable = intSubject
.prefix(
while: { $0 % 2 == 0 },
behavior: .inclusive
)
.sink(
receiveCompletion: { completions.append($0) },
receiveValue: { values.append($0) }
)

[0, 2, 4, 5]
.forEach(intSubject.send)

intSubject.send(completion: .failure(.init()))

XCTAssertEqual(values, [0, 2, 4, 5])
XCTAssertEqual(completions, [.finished])
}

func testInclusiveValueEventsWithErrorBeforeStop() {
let intSubject = PassthroughSubject<Int, SomeError>()

var values = [Int]()
var completions = [Subscribers.Completion<SomeError>]()

cancellable = intSubject
.prefix(
while: { $0 % 2 == 0 },
behavior: .inclusive
)
.sink(
receiveCompletion: { completions.append($0) },
receiveValue: { values.append($0) }
)

[0, 2, 4]
.forEach(intSubject.send)

intSubject.send(completion: .failure(.init()))

XCTAssertEqual(values, [0, 2, 4])
XCTAssertEqual(completions, [.failure(.init())])
}

func testInclusiveEarlyCompletion() {
let intSubject = PassthroughSubject<Int, SomeError>()

var values = [Int]()
var completions = [Subscribers.Completion<SomeError>]()

cancellable = intSubject
.prefix(
while: { $0 % 2 == 0 },
behavior: .inclusive
)
.sink(
receiveCompletion: { completions.append($0) },
receiveValue: { values.append($0) }
)

[0, 2, 4]
.forEach(intSubject.send)

intSubject.send(completion: .finished)

XCTAssertEqual(values, [0, 2, 4])
XCTAssertEqual(completions, [.finished])
}
}
#endif

0 comments on commit 9d7032e

Please sign in to comment.