|
| 1 | +// |
| 2 | +// CompactScan.swift |
| 3 | +// CombineExt |
| 4 | +// |
| 5 | +// Created by Thibault Wittemberg on 04/09/2021. |
| 6 | +// Copyright © 2021 Combine Community. All rights reserved. |
| 7 | +// |
| 8 | + |
| 9 | +#if canImport(Combine) |
| 10 | +import Combine |
| 11 | + |
| 12 | +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) |
| 13 | +public extension Publisher { |
| 14 | + |
| 15 | + /// Transforms elements from the upstream publisher by providing the current |
| 16 | + /// element to a closure along with the last value returned by the closure. |
| 17 | + /// |
| 18 | + /// The ``nextPartialResult`` closure might return nil values. In that case the accumulator won't change until the next non-nil upstream publisher value. |
| 19 | + /// |
| 20 | + /// Use ``Publisher/compactScan(_:_:)`` to accumulate all previously-published values into a single |
| 21 | + /// value, which you then combine with each newly-published value. |
| 22 | + /// |
| 23 | + /// The following example logs a running total of all values received |
| 24 | + /// from the sequence publisher. |
| 25 | + /// |
| 26 | + /// let range = (0...5) |
| 27 | + /// let cancellable = range.publisher |
| 28 | + /// .compactScan(0) { |
| 29 | + /// guard $1.isMultiple(of: 2) else { return nil } |
| 30 | + /// return $0 + $1 |
| 31 | + /// } |
| 32 | + /// .sink { print ("\($0)", terminator: " ") } |
| 33 | + /// // Prints: "0 2 6 ". |
| 34 | + /// |
| 35 | + /// - Parameters: |
| 36 | + /// - initialResult: The previous result returned by the `nextPartialResult` closure. |
| 37 | + /// - nextPartialResult: A closure that takes as its arguments the previous value returned by the closure and the next element emitted from the upstream publisher. |
| 38 | + /// - Returns: A publisher that transforms elements by applying a closure that receives its previous return value and the next element from the upstream publisher. |
| 39 | + func compactScan<T>(_ initialResult: T, _ nextPartialResult: @escaping (T, Output) -> T?) -> AnyPublisher<T, Failure> { |
| 40 | + self.scan((initialResult, initialResult)) { accumulator, value -> (T, T?) in |
| 41 | + let lastNonNilAccumulator = accumulator.0 |
| 42 | + let newAccumulator = nextPartialResult(lastNonNilAccumulator, value) |
| 43 | + return (newAccumulator ?? lastNonNilAccumulator, newAccumulator) |
| 44 | + } |
| 45 | + .compactMap { $0.1 } |
| 46 | + .eraseToAnyPublisher() |
| 47 | + } |
| 48 | + |
| 49 | + /// Transforms elements from the upstream publisher by providing the current element to an error-throwing closure along with the last value returned by the closure. |
| 50 | + /// |
| 51 | + /// The ``nextPartialResult`` closure might return nil values. In that case the accumulator won't change until the next non-nil upstream publisher value. |
| 52 | + /// |
| 53 | + /// Use ``Publisher/tryCompactScan(_:_:)`` to accumulate all previously-published values into a single value, which you then combine with each newly-published value. |
| 54 | + /// If your accumulator closure throws an error, the publisher terminates with the error. |
| 55 | + /// |
| 56 | + /// In the example below, ``Publisher/tryCompactScan(_:_:)`` calls a division function on elements of a collection publisher. The resulting publisher publishes each result until the function encounters a `DivisionByZeroError`, which terminates the publisher. |
| 57 | + ///m |
| 58 | + /// struct DivisionByZeroError: Error {} |
| 59 | + /// |
| 60 | + /// /// A function that throws a DivisionByZeroError if `current` provided by the TryScan publisher is zero. |
| 61 | + /// func myThrowingFunction(_ lastValue: Int, _ currentValue: Int) throws -> Int? { |
| 62 | + /// guard currentValue.isMultiple(of: 2) else { return nil } |
| 63 | + /// guard currentValue != 0 else { throw DivisionByZeroError() } |
| 64 | + /// return lastValue / currentValue |
| 65 | + /// } |
| 66 | + /// |
| 67 | + /// let numbers = [1, 2, 3, 4, 5, 0, 6, 7, 8, 9] |
| 68 | + /// let cancellable = numbers.publisher |
| 69 | + /// .tryCompactScan(10) { try myThrowingFunction($0, $1) } |
| 70 | + /// .sink( |
| 71 | + /// receiveCompletion: { print ("\($0)") }, |
| 72 | + /// receiveValue: { print ("\($0)", terminator: " ") } |
| 73 | + /// ) |
| 74 | + /// |
| 75 | + /// // Prints: "6 2 failure(DivisionByZeroError())". |
| 76 | + /// |
| 77 | + /// If the closure throws an error, the publisher fails with the error. |
| 78 | + /// |
| 79 | + /// - Parameters: |
| 80 | + /// - initialResult: The previous result returned by the `nextPartialResult` closure. |
| 81 | + /// - nextPartialResult: An error-throwing closure that takes as its arguments the previous value returned by the closure and the next element emitted from the upstream publisher. |
| 82 | + /// - Returns: A publisher that transforms elements by applying a closure that receives its previous return value and the next element from the upstream publisher. |
| 83 | + func tryCompactScan<T>(_ initialResult: T, _ nextPartialResult: @escaping (T, Output) throws -> T?) -> AnyPublisher<T, Error> { |
| 84 | + self.tryScan((initialResult, initialResult)) { accumulator, value -> (T, T?) in |
| 85 | + let lastNonNilAccumulator = accumulator.0 |
| 86 | + let newAccumulator = try nextPartialResult(lastNonNilAccumulator, value) |
| 87 | + return (newAccumulator ?? lastNonNilAccumulator, newAccumulator) |
| 88 | + } |
| 89 | + .compactMap { $0.1 } |
| 90 | + .eraseToAnyPublisher() |
| 91 | + } |
| 92 | +} |
| 93 | +#endif |
0 commit comments