Skip to content

Commit e3ad390

Browse files
authored
Implement output in (#180)
Implement `output(in:)` similar to Combine - https://developer.apple.com/documentation/combine/publisher/output(in:) Incoming ranges are reduced to `Range<Int>` same way as Apple does it in Combine.
1 parent e7ae58e commit e3ad390

2 files changed

Lines changed: 118 additions & 6 deletions

File tree

Sources/Afluent/SequenceOperators/OutputSequence.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,35 @@ extension AsyncSequences {
3636
}
3737
}
3838

39+
public struct OutputIn<Upstream: AsyncSequence & Sendable>: AsyncSequence, Sendable {
40+
public typealias Element = Upstream.Element
41+
42+
let upstream: Upstream
43+
let range: Range<Int>
44+
45+
public struct AsyncIterator: AsyncIteratorProtocol {
46+
var upstreamIterator: Upstream.AsyncIterator
47+
let range: Range<Int>
48+
var nextIndex = 0
49+
50+
public mutating func next() async throws -> Element? {
51+
guard nextIndex < range.upperBound else { return nil }
52+
while let next = try await upstreamIterator.next() {
53+
if range.contains(nextIndex) {
54+
nextIndex &+= 1
55+
return next
56+
}
57+
nextIndex &+= 1
58+
}
59+
return nil
60+
}
61+
}
62+
63+
public func makeAsyncIterator() -> AsyncIterator {
64+
AsyncIterator(upstreamIterator: upstream.makeAsyncIterator(), range: range)
65+
}
66+
}
67+
3968
}
4069

4170
extension AsyncSequence where Self: Sendable {
@@ -48,4 +77,24 @@ extension AsyncSequence where Self: Sendable {
4877
AsyncSequences.OutputAt(upstream: self, index: index)
4978
}
5079

80+
/// Returns an async sequence that contains, in order, the elements of the base sequence specified by the range.
81+
///
82+
/// ### Discussion:
83+
/// Completes normally after range is exhausted.
84+
///
85+
/// ### Example:
86+
/// ```swift
87+
/// let originalSequence = [0, 3, 5, 7, 9].async
88+
/// for try await element in originalSequence.output(in: 1..<4) {
89+
/// print("\(element)")
90+
/// }
91+
/// // Prints 3, 5, 7
92+
/// ```
93+
///
94+
/// - Parameter range: A range that indicates which elements to include.
95+
/// - Returns: An async sequence that contains, in order, the elements of the base sequence specified by the range.
96+
public func output<R>(in range: R) -> AsyncSequences.OutputIn<Self> where R : RangeExpression, R.Bound == Int, R: Sendable {
97+
AsyncSequences.OutputIn(upstream: self,
98+
range: range.relative(to: 0..<Int.max))
99+
}
51100
}

Tests/AfluentTests/SequenceTests/OutputSequenceTests.swift

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55
// Created by Roman Temchenko on 2025-03-07.
66
//
77

8-
import Afluent
8+
@testable import Afluent
99
import Foundation
1010
import Testing
1111
import ConcurrencyExtras
1212

1313
struct OutputSequenceTests {
1414

1515
@Test func testOutputAt() async throws {
16-
let emptySequence = [0, 3, 5].async
17-
let result = try await emptySequence.output(at: 1).first()
18-
#expect(result == 3)
16+
let originalSequence = [0, 3, 5].async
17+
let result = try await originalSequence.output(at: 1).collect().first()
18+
#expect(result == [3])
1919
}
2020

2121
@Test func testOutputAtWithEmptySequence() async throws {
@@ -25,8 +25,8 @@ struct OutputSequenceTests {
2525
}
2626

2727
@Test func testOutputAtOutOfBounds() async throws {
28-
let emptySequence = [0, 3, 5].async
29-
let result = try await emptySequence.output(at: 5).first()
28+
let originalSequence = [0, 3, 5].async
29+
let result = try await originalSequence.output(at: 5).first()
3030
#expect(result == nil)
3131
}
3232

@@ -60,4 +60,67 @@ struct OutputSequenceTests {
6060
#expect(result == nil)
6161
}
6262

63+
@Test func testOutputIn() async throws {
64+
let originalSequence = [0, 3, 5, 7, 9].async
65+
let result = try await originalSequence.output(in: 1..<4).collect().first()
66+
#expect(result == [3, 5, 7])
67+
}
68+
69+
@Test func testOutputInClosedRange() async throws {
70+
let originalSequence = [0, 3, 5, 7, 9].async
71+
let result = try await originalSequence.output(in: 1...3).collect().first()
72+
#expect(result == [3, 5, 7])
73+
}
74+
75+
@Test func testOutputInPartialRangeUpTo() async throws {
76+
let originalSequence = [0, 3, 5, 7, 9].async
77+
let result = try await originalSequence.output(in: ..<3).collect().first()
78+
#expect(result == [0, 3, 5])
79+
}
80+
81+
@Test func testOutputInPartialRangeThrough() async throws {
82+
let originalSequence = [0, 3, 5, 7, 9].async
83+
let result = try await originalSequence.output(in: ...3).collect().first()
84+
#expect(result == [0, 3, 5, 7])
85+
}
86+
87+
@Test func testOutputInPartialRangeFrom() async throws {
88+
let originalSequence = [0, 3, 5, 7, 9].async
89+
let result = try await originalSequence.output(in: 2...).collect().first()
90+
#expect(result == [5, 7, 9])
91+
}
92+
93+
@Test func testOutputInWithEmptySequence() async throws {
94+
let emptySequence = [Int]().async
95+
let result = try await emptySequence.output(in: 0...).first()
96+
try #require(result == nil)
97+
}
98+
99+
@Test func testOutputInOutOfBounds() async throws {
100+
let originalSequence = [0, 3, 5].async
101+
let result = try await originalSequence.output(in: 5...).first()
102+
#expect(result == nil)
103+
}
104+
105+
@Test func testOutputInCancellation() async throws {
106+
let (stream, continuation) = AsyncThrowingStream.makeStream(of: Int.self)
107+
108+
let task = Task {
109+
let result = try await stream.output(in: 5...).first()
110+
#expect(result == nil)
111+
return result
112+
}
113+
114+
continuation.yield(0)
115+
task.cancel()
116+
117+
// Give task cancellation time to propagate.
118+
await Task.megaYield()
119+
120+
continuation.finish(throwing: GeneralError.e1)
121+
122+
let result = try await task.value
123+
#expect(result == nil)
124+
}
125+
63126
}

0 commit comments

Comments
 (0)