From 255f0ba1504a7b921c5debbff463419d81002118 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Fri, 19 Apr 2024 16:38:09 +0100 Subject: [PATCH 1/3] async chunked sequences can produce empty chunks async chunked sequences can produce empty chunks if signals are received before primary chunk elements. This commit also includes an accompanying evolution proposal. --- Evolution/0012-produce-empty-chunks.md | 96 +++++++++++++++++++ .../AsyncChunksOfCountOrSignalSequence.swift | 45 +++++---- Sources/AsyncAlgorithms/Deprecated.swift | 63 ++++++++++++ Sources/AsyncSequenceValidation/Event.swift | 6 +- Sources/AsyncSequenceValidation/Theme.swift | 7 +- Tests/AsyncAlgorithmsTests/TestChunk.swift | 45 +++++++++ 6 files changed, 241 insertions(+), 21 deletions(-) create mode 100644 Evolution/0012-produce-empty-chunks.md create mode 100644 Sources/AsyncAlgorithms/Deprecated.swift diff --git a/Evolution/0012-produce-empty-chunks.md b/Evolution/0012-produce-empty-chunks.md new file mode 100644 index 00000000..7edd0de6 --- /dev/null +++ b/Evolution/0012-produce-empty-chunks.md @@ -0,0 +1,96 @@ +# Produce Empty Chunks + +* Proposal: [0012](0012-produce-empty-chunks.md) +* Author: [Rick Newton-Rogers](https://github.com/rnro) +* Review Manager: TBD +* Status: **Implemented** + +* Implementation: + [Source](https://github.com/rnewtonrogers/swift-async-algorithms/blob/allow_empty_chunks/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift) | + [Tests](https://github.com/rnewtonrogers/swift-async-algorithms/blob/allow_empty_chunks/Tests/AsyncAlgorithmsTests/TestChunk.swift) + +## Introduction + +At the moment it is possible to use a signal `AsyncSequence` to provide marks at which elements of a primary +`AsyncSequence` should be 'chunked' into collections. However if one or more signals arrive when there are no elements +from the primary sequence to vend as output then they will be ignored. + +## Motivation + +As noted in [a GitHub Issue](https://github.com/apple/swift-async-algorithms/issues/247) it could be useful to output empty +chunks in the outlined case to provide information of a lack of activity on the primary sequence. This would likely be +particularly useful when combined with a timer as a signaling source. + +## Proposed solution + +Modify the API of the `AsyncSequence` `chunks` and `chunked` extensions to allow specifying of a new parameter +(`produceEmptyChunks`) which determines if the output sequence produces empty chunks. The new parameter will retain the +previous behavior by default. + +## Detailed design + +The modified API will look as follows: +```swift +extension AsyncSequence { + /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when a signal `AsyncSequence` produces an element. + public func chunks(ofCount count: Int, or signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + AsyncChunksOfCountOrSignalSequence(self, count: count, signal: signal, produceEmptyChunks: produceEmptyChunks) + } + + /// Creates an asynchronous sequence that creates chunks of a given count or when a signal `AsyncSequence` produces an element. + public func chunks(ofCount count: Int, or signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence { + chunks(ofCount: count, or: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks) + } + + /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when a signal `AsyncSequence` produces an element. + public func chunked(by signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: signal, produceEmptyChunks: produceEmptyChunks) + } + + /// Creates an asynchronous sequence that creates chunks when a signal `AsyncSequence` produces an element. + public func chunked(by signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence { + chunked(by: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks) + } + + /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when an `AsyncTimerSequence` fires. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + AsyncChunksOfCountOrSignalSequence(self, count: count, signal: timer, produceEmptyChunks: produceEmptyChunks) + } + + /// Creates an asynchronous sequence that creates chunks of a given count or when an `AsyncTimerSequence` fires. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> { + chunks(ofCount: count, or: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks) + } + + /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when an `AsyncTimerSequence` fires. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public func chunked(by timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: timer, produceEmptyChunks: produceEmptyChunks) + } + + /// Creates an asynchronous sequence that creates chunks when an `AsyncTimerSequence` fires. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public func chunked(by timer: AsyncTimerSequence, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> { + chunked(by: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks) + } +} +``` +The previous API will be marked as deprecated and `@_disfavoredOverload` to avoid ambiguity with the new versions. + + +## Effect on API resilience + +This change is API-safe due to the default value but ABI-unsafe. + +## Alternatives considered + +- Providing a config struct to future-proof the API against further changes in the future was rejected because it +seems to add overhead defending against an unlikely event. +- Providing entirely new API without deprecating the old one was ruled out to avoid an explosion in API complexity +which increases maintenance burden and reduces readability of code. + +## Acknowledgments + +- [@tachyonics](https://github.com/tachyonics) for the initial GitHub issue describing the requirement. diff --git a/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift b/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift index e322faea..bba12316 100644 --- a/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift @@ -11,47 +11,47 @@ extension AsyncSequence { /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when a signal `AsyncSequence` produces an element. - public func chunks(ofCount count: Int, or signal: Signal, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { - AsyncChunksOfCountOrSignalSequence(self, count: count, signal: signal) + public func chunks(ofCount count: Int, or signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + AsyncChunksOfCountOrSignalSequence(self, count: count, signal: signal, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given count or when a signal `AsyncSequence` produces an element. - public func chunks(ofCount count: Int, or signal: Signal) -> AsyncChunksOfCountOrSignalSequence { - chunks(ofCount: count, or: signal, into: [Element].self) + public func chunks(ofCount count: Int, or signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence { + chunks(ofCount: count, or: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when a signal `AsyncSequence` produces an element. - public func chunked(by signal: Signal, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { - AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: signal) + public func chunked(by signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: signal, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks when a signal `AsyncSequence` produces an element. - public func chunked(by signal: Signal) -> AsyncChunksOfCountOrSignalSequence { - chunked(by: signal, into: [Element].self) + public func chunked(by signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence { + chunked(by: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { - AsyncChunksOfCountOrSignalSequence(self, count: count, signal: timer) + public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + AsyncChunksOfCountOrSignalSequence(self, count: count, signal: timer, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given count or when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunks(ofCount count: Int, or timer: AsyncTimerSequence) -> AsyncChunksOfCountOrSignalSequence> { - chunks(ofCount: count, or: timer, into: [Element].self) + public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> { + chunks(ofCount: count, or: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunked(by timer: AsyncTimerSequence, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { - AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: timer) + public func chunked(by timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: timer, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunked(by timer: AsyncTimerSequence) -> AsyncChunksOfCountOrSignalSequence> { - chunked(by: timer, into: [Element].self) + public func chunked(by timer: AsyncTimerSequence, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> { + chunked(by: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } } @@ -74,12 +74,14 @@ public struct AsyncChunksOfCountOrSignalSequence let count: Int? + let produceEmptyChunks: Bool var iterator: Merged.AsyncIterator var terminated = false - init(iterator: Merged.AsyncIterator, count: Int?) { + init(iterator: Merged.AsyncIterator, count: Int?, produceEmptyChunks: Bool) { self.count = count self.iterator = iterator + self.produceEmptyChunks = produceEmptyChunks } public mutating func next() async rethrows -> Collected? { @@ -104,6 +106,9 @@ public struct AsyncChunksOfCountOrSignalSequence 0) } @@ -121,11 +127,12 @@ public struct AsyncChunksOfCountOrSignalSequence Iterator { - return Iterator(iterator: merge(chain(base.map { Either.element($0) }, [.terminal].async), signal.map { _ in Either.signal }).makeAsyncIterator(), count: count) + return Iterator(iterator: merge(chain(base.map { Either.element($0) }, [.terminal].async), signal.map { _ in Either.signal }).makeAsyncIterator(), count: count, produceEmptyChunks: produceEmptyChunks) } } diff --git a/Sources/AsyncAlgorithms/Deprecated.swift b/Sources/AsyncAlgorithms/Deprecated.swift new file mode 100644 index 00000000..ec68422d --- /dev/null +++ b/Sources/AsyncAlgorithms/Deprecated.swift @@ -0,0 +1,63 @@ + + +extension AsyncSequence { + /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when a signal `AsyncSequence` produces an element. + @_disfavoredOverload + @available(*, deprecated, renamed: "chunks(ofCount:or:into:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.") + public func chunks(ofCount count: Int, or signal: Signal, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + AsyncChunksOfCountOrSignalSequence(self, count: count, signal: signal, produceEmptyChunks: false) + } + + /// Creates an asynchronous sequence that creates chunks of a given count or when a signal `AsyncSequence` produces an element. + @_disfavoredOverload + @available(*, deprecated, renamed: "chunks(ofCount:or:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.") + public func chunks(ofCount count: Int, or signal: Signal) -> AsyncChunksOfCountOrSignalSequence { + chunks(ofCount: count, or: signal, into: [Element].self) + } + + /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when a signal `AsyncSequence` produces an element. + @_disfavoredOverload + @available(*, deprecated, renamed: "chunked(by:into:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.") + public func chunked(by signal: Signal, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: signal, produceEmptyChunks: false) + } + + /// Creates an asynchronous sequence that creates chunks when a signal `AsyncSequence` produces an element. + @_disfavoredOverload + @available(*, deprecated, renamed: "chunked(by:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.") + public func chunked(by signal: Signal) -> AsyncChunksOfCountOrSignalSequence { + chunked(by: signal, into: [Element].self) + } + + /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when an `AsyncTimerSequence` fires. + @_disfavoredOverload + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(*, deprecated, renamed: "chunks(ofCount:or:into:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.") + public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + AsyncChunksOfCountOrSignalSequence(self, count: count, signal: timer, produceEmptyChunks: false) + } + + /// Creates an asynchronous sequence that creates chunks of a given count or when an `AsyncTimerSequence` fires. + @_disfavoredOverload + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(*, deprecated, renamed: "chunks(ofCount:or:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.") + public func chunks(ofCount count: Int, or timer: AsyncTimerSequence) -> AsyncChunksOfCountOrSignalSequence> { + chunks(ofCount: count, or: timer, into: [Element].self) + } + + /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when an `AsyncTimerSequence` fires. + @_disfavoredOverload + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(*, deprecated, renamed: "chunked(by:into:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.") + public func chunked(by timer: AsyncTimerSequence, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: timer, produceEmptyChunks: false) + } + + /// Creates an asynchronous sequence that creates chunks when an `AsyncTimerSequence` fires. + @_disfavoredOverload + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(*, deprecated, renamed: "chunked(by:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.") + public func chunked(by timer: AsyncTimerSequence) -> AsyncChunksOfCountOrSignalSequence> { + chunked(by: timer, into: [Element].self) + } +} diff --git a/Sources/AsyncSequenceValidation/Event.swift b/Sources/AsyncSequenceValidation/Event.swift index 70101475..27615605 100644 --- a/Sources/AsyncSequenceValidation/Event.swift +++ b/Sources/AsyncSequenceValidation/Event.swift @@ -149,7 +149,11 @@ extension AsyncSequenceValidationDiagram { if grouping == 0 { when = when.advanced(by: .steps(1)) } - emissions.append((when, .value(String(ch), index))) + if ch != "_" { + emissions.append((when, .value(String(ch), index))) + } else { + emissions.append((when, .value(String(), index))) + } } else { string?.append(str) } diff --git a/Sources/AsyncSequenceValidation/Theme.swift b/Sources/AsyncSequenceValidation/Theme.swift index 19e80419..201fea34 100644 --- a/Sources/AsyncSequenceValidation/Theme.swift +++ b/Sources/AsyncSequenceValidation/Theme.swift @@ -48,6 +48,7 @@ extension AsyncSequenceValidationDiagram { case "[": return .beginGroup case "]": return .endGroup case " ": return .skip + case "_": return .value(String()) default: return .value(String(character)) } } @@ -64,7 +65,11 @@ extension AsyncSequenceValidationDiagram { case .beginGroup: return "[" case .endGroup: return "]" case .skip: return " " - case .value(let value): return value + case .value(let value): + if value.count == 0 { + return "_" + } + return value } } } diff --git a/Tests/AsyncAlgorithmsTests/TestChunk.swift b/Tests/AsyncAlgorithmsTests/TestChunk.swift index 7845b1a0..3a754821 100644 --- a/Tests/AsyncAlgorithmsTests/TestChunk.swift +++ b/Tests/AsyncAlgorithmsTests/TestChunk.swift @@ -59,6 +59,15 @@ final class TestChunk: XCTestCase { } } + func test_signal_emptyChunks_produceEmptyChunks() { + validate { + "--1--|" + "XX-XX|" + $0.inputs[0].chunked(by: $0.inputs[1], produceEmptyChunks: true).map(concatCharacters) + "__-1_|" + } + } + func test_signal_error() { validate { "AB^" @@ -95,6 +104,15 @@ final class TestChunk: XCTestCase { } } + func test_signalAndCount_countAlwaysPrevails_produceEmptyChunks() { + validate { + "AB --A-B -|" + "-- X---- X|" + $0.inputs[0].chunks(ofCount: 2, or: $0.inputs[1], produceEmptyChunks: true).map(concatCharacters) + "-'AB'_---'AB'_|" + } + } + func test_signalAndCount_countResetsAfterCount() { validate { "ABCDE -ABCDE |" @@ -149,6 +167,15 @@ final class TestChunk: XCTestCase { } } + func test_time_emptyChunks_produceEmptyChunks() throws { + guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } + validate { + "-- 1- --|" + $0.inputs[0].chunked(by: .repeating(every: .steps(2), clock: $0.clock), produceEmptyChunks: true).map(concatCharacters) + "-_ -1 -_|" + } + } + func test_time_error() throws { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { @@ -185,6 +212,15 @@ final class TestChunk: XCTestCase { } } + func test_timeAndCount_countAlwaysPrevails_produceEmptyChunks() throws { + guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } + validate { + "AB --A-B -|" + $0.inputs[0].chunks(ofCount: 2, or: .repeating(every: .steps(8), clock: $0.clock), produceEmptyChunks: true).map(concatCharacters) + "-'AB'----'AB'_|" + } + } + func test_timeAndCount_countResetsAfterCount() throws { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { @@ -194,6 +230,15 @@ final class TestChunk: XCTestCase { } } + func test_timeAndCount_countResetsAfterCount_produceEmptyChunks() throws { + guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } + validate { + "ABCDE --- ABCDE |" + $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock), produceEmptyChunks: true).map(concatCharacters) + "----'ABCDE'--_ ----'ABCDE'|" + } + } + func test_timeAndCount_countResetsAfterSignal() throws { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { From f63aa8c353c9580e51d37f14a8fe536368565746 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Mon, 22 Apr 2024 09:55:16 +0100 Subject: [PATCH 2/3] produceEmptyChunks defaults to true, remove DSL additions --- Evolution/0012-produce-empty-chunks.md | 24 +++++++-------- .../AsyncChunksOfCountOrSignalSequence.swift | 16 +++++----- Sources/AsyncSequenceValidation/Event.swift | 6 +--- Sources/AsyncSequenceValidation/Theme.swift | 7 +---- Tests/AsyncAlgorithmsTests/TestChunk.swift | 30 +++++++++---------- 5 files changed, 37 insertions(+), 46 deletions(-) diff --git a/Evolution/0012-produce-empty-chunks.md b/Evolution/0012-produce-empty-chunks.md index 7edd0de6..2ac230db 100644 --- a/Evolution/0012-produce-empty-chunks.md +++ b/Evolution/0012-produce-empty-chunks.md @@ -5,10 +5,10 @@ * Review Manager: TBD * Status: **Implemented** -* Implementation: +* Implementation: [Source](https://github.com/rnewtonrogers/swift-async-algorithms/blob/allow_empty_chunks/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift) | [Tests](https://github.com/rnewtonrogers/swift-async-algorithms/blob/allow_empty_chunks/Tests/AsyncAlgorithmsTests/TestChunk.swift) - + ## Introduction At the moment it is possible to use a signal `AsyncSequence` to provide marks at which elements of a primary @@ -24,8 +24,8 @@ particularly useful when combined with a timer as a signaling source. ## Proposed solution Modify the API of the `AsyncSequence` `chunks` and `chunked` extensions to allow specifying of a new parameter -(`produceEmptyChunks`) which determines if the output sequence produces empty chunks. The new parameter will retain the -previous behavior by default. +(`produceEmptyChunks`) which determines if the output sequence produces empty chunks. The new parameter will adopt the +new behavior by default but may be opted-out of. ## Detailed design @@ -33,46 +33,46 @@ The modified API will look as follows: ```swift extension AsyncSequence { /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when a signal `AsyncSequence` produces an element. - public func chunks(ofCount count: Int, or signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + public func chunks(ofCount count: Int, or signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: count, signal: signal, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given count or when a signal `AsyncSequence` produces an element. - public func chunks(ofCount count: Int, or signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence { + public func chunks(ofCount count: Int, or signal: Signal, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence { chunks(ofCount: count, or: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when a signal `AsyncSequence` produces an element. - public func chunked(by signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + public func chunked(by signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: signal, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks when a signal `AsyncSequence` produces an element. - public func chunked(by signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence { + public func chunked(by signal: Signal, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence { chunked(by: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: count, signal: timer, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given count or when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> { + public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> { chunks(ofCount: count, or: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunked(by timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + public func chunked(by timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: timer, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunked(by timer: AsyncTimerSequence, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> { + public func chunked(by timer: AsyncTimerSequence, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> { chunked(by: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } } diff --git a/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift b/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift index bba12316..1ab58e99 100644 --- a/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift @@ -11,46 +11,46 @@ extension AsyncSequence { /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when a signal `AsyncSequence` produces an element. - public func chunks(ofCount count: Int, or signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + public func chunks(ofCount count: Int, or signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: count, signal: signal, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given count or when a signal `AsyncSequence` produces an element. - public func chunks(ofCount count: Int, or signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence { + public func chunks(ofCount count: Int, or signal: Signal, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence { chunks(ofCount: count, or: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when a signal `AsyncSequence` produces an element. - public func chunked(by signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + public func chunked(by signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: signal, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks when a signal `AsyncSequence` produces an element. - public func chunked(by signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence { + public func chunked(by signal: Signal, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence { chunked(by: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: count, signal: timer, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given count or when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> { + public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> { chunks(ofCount: count, or: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunked(by timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + public func chunked(by timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: timer, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunked(by timer: AsyncTimerSequence, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> { + public func chunked(by timer: AsyncTimerSequence, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> { chunked(by: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } } diff --git a/Sources/AsyncSequenceValidation/Event.swift b/Sources/AsyncSequenceValidation/Event.swift index 27615605..70101475 100644 --- a/Sources/AsyncSequenceValidation/Event.swift +++ b/Sources/AsyncSequenceValidation/Event.swift @@ -149,11 +149,7 @@ extension AsyncSequenceValidationDiagram { if grouping == 0 { when = when.advanced(by: .steps(1)) } - if ch != "_" { - emissions.append((when, .value(String(ch), index))) - } else { - emissions.append((when, .value(String(), index))) - } + emissions.append((when, .value(String(ch), index))) } else { string?.append(str) } diff --git a/Sources/AsyncSequenceValidation/Theme.swift b/Sources/AsyncSequenceValidation/Theme.swift index 201fea34..19e80419 100644 --- a/Sources/AsyncSequenceValidation/Theme.swift +++ b/Sources/AsyncSequenceValidation/Theme.swift @@ -48,7 +48,6 @@ extension AsyncSequenceValidationDiagram { case "[": return .beginGroup case "]": return .endGroup case " ": return .skip - case "_": return .value(String()) default: return .value(String(character)) } } @@ -65,11 +64,7 @@ extension AsyncSequenceValidationDiagram { case .beginGroup: return "[" case .endGroup: return "]" case .skip: return " " - case .value(let value): - if value.count == 0 { - return "_" - } - return value + case .value(let value): return value } } } diff --git a/Tests/AsyncAlgorithmsTests/TestChunk.swift b/Tests/AsyncAlgorithmsTests/TestChunk.swift index 3a754821..d94aa62a 100644 --- a/Tests/AsyncAlgorithmsTests/TestChunk.swift +++ b/Tests/AsyncAlgorithmsTests/TestChunk.swift @@ -54,7 +54,7 @@ final class TestChunk: XCTestCase { validate { "--1--|" "XX-XX|" - $0.inputs[0].chunked(by: $0.inputs[1]).map(concatCharacters) + $0.inputs[0].chunked(by: $0.inputs[1], produceEmptyChunks: false).map(concatCharacters) "---1-|" } } @@ -63,8 +63,8 @@ final class TestChunk: XCTestCase { validate { "--1--|" "XX-XX|" - $0.inputs[0].chunked(by: $0.inputs[1], produceEmptyChunks: true).map(concatCharacters) - "__-1_|" + $0.inputs[0].chunked(by: $0.inputs[1]).map(concatCharacters) + "''''-1''|" } } @@ -99,7 +99,7 @@ final class TestChunk: XCTestCase { validate { "AB --A-B -|" "-- X---- X|" - $0.inputs[0].chunks(ofCount: 2, or: $0.inputs[1]).map(concatCharacters) + $0.inputs[0].chunks(ofCount: 2, or: $0.inputs[1], produceEmptyChunks: false).map(concatCharacters) "-'AB'----'AB'-|" } } @@ -108,8 +108,8 @@ final class TestChunk: XCTestCase { validate { "AB --A-B -|" "-- X---- X|" - $0.inputs[0].chunks(ofCount: 2, or: $0.inputs[1], produceEmptyChunks: true).map(concatCharacters) - "-'AB'_---'AB'_|" + $0.inputs[0].chunks(ofCount: 2, or: $0.inputs[1]).map(concatCharacters) + "-'AB'''---'AB'''|" } } @@ -162,7 +162,7 @@ final class TestChunk: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "-- 1- --|" - $0.inputs[0].chunked(by: .repeating(every: .steps(2), clock: $0.clock)).map(concatCharacters) + $0.inputs[0].chunked(by: .repeating(every: .steps(2), clock: $0.clock), produceEmptyChunks: false).map(concatCharacters) "-- -1 --|" } } @@ -171,8 +171,8 @@ final class TestChunk: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "-- 1- --|" - $0.inputs[0].chunked(by: .repeating(every: .steps(2), clock: $0.clock), produceEmptyChunks: true).map(concatCharacters) - "-_ -1 -_|" + $0.inputs[0].chunked(by: .repeating(every: .steps(2), clock: $0.clock)).map(concatCharacters) + "-'' -1 -''|" } } @@ -207,7 +207,7 @@ final class TestChunk: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "AB --A-B -|" - $0.inputs[0].chunks(ofCount: 2, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) + $0.inputs[0].chunks(ofCount: 2, or: .repeating(every: .steps(8), clock: $0.clock), produceEmptyChunks: false).map(concatCharacters) "-'AB'----'AB'-|" } } @@ -216,8 +216,8 @@ final class TestChunk: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "AB --A-B -|" - $0.inputs[0].chunks(ofCount: 2, or: .repeating(every: .steps(8), clock: $0.clock), produceEmptyChunks: true).map(concatCharacters) - "-'AB'----'AB'_|" + $0.inputs[0].chunks(ofCount: 2, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) + "-'AB'----'AB'''|" } } @@ -225,7 +225,7 @@ final class TestChunk: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "ABCDE --- ABCDE |" - $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) + $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock), produceEmptyChunks: false).map(concatCharacters) "----'ABCDE'--- ----'ABCDE'|" } } @@ -234,8 +234,8 @@ final class TestChunk: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "ABCDE --- ABCDE |" - $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock), produceEmptyChunks: true).map(concatCharacters) - "----'ABCDE'--_ ----'ABCDE'|" + $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) + "----'ABCDE'--'' ----'ABCDE'|" } } From e441bc00d326c9657d3cc52399fd76d02ef7fedc Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Tue, 18 Jun 2024 14:01:09 +0100 Subject: [PATCH 3/3] Return to produceEmptyChunks defaults to false --- Evolution/0012-produce-empty-chunks.md | 20 ++++++------- .../AsyncChunksOfCountOrSignalSequence.swift | 16 +++++----- Tests/AsyncAlgorithmsTests/TestChunk.swift | 30 +++++++++---------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/Evolution/0012-produce-empty-chunks.md b/Evolution/0012-produce-empty-chunks.md index 2ac230db..fe65104f 100644 --- a/Evolution/0012-produce-empty-chunks.md +++ b/Evolution/0012-produce-empty-chunks.md @@ -24,8 +24,8 @@ particularly useful when combined with a timer as a signaling source. ## Proposed solution Modify the API of the `AsyncSequence` `chunks` and `chunked` extensions to allow specifying of a new parameter -(`produceEmptyChunks`) which determines if the output sequence produces empty chunks. The new parameter will adopt the -new behavior by default but may be opted-out of. +(`produceEmptyChunks`) which determines if the output sequence produces empty chunks. The new parameter will retain the +previous behavior by default. ## Detailed design @@ -33,46 +33,46 @@ The modified API will look as follows: ```swift extension AsyncSequence { /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when a signal `AsyncSequence` produces an element. - public func chunks(ofCount count: Int, or signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + public func chunks(ofCount count: Int, or signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: count, signal: signal, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given count or when a signal `AsyncSequence` produces an element. - public func chunks(ofCount count: Int, or signal: Signal, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence { + public func chunks(ofCount count: Int, or signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence { chunks(ofCount: count, or: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when a signal `AsyncSequence` produces an element. - public func chunked(by signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + public func chunked(by signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: signal, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks when a signal `AsyncSequence` produces an element. - public func chunked(by signal: Signal, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence { + public func chunked(by signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence { chunked(by: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: count, signal: timer, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given count or when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> { + public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> { chunks(ofCount: count, or: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunked(by timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + public func chunked(by timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: timer, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunked(by timer: AsyncTimerSequence, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> { + public func chunked(by timer: AsyncTimerSequence, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> { chunked(by: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } } diff --git a/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift b/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift index 1ab58e99..bba12316 100644 --- a/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift @@ -11,46 +11,46 @@ extension AsyncSequence { /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when a signal `AsyncSequence` produces an element. - public func chunks(ofCount count: Int, or signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + public func chunks(ofCount count: Int, or signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: count, signal: signal, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given count or when a signal `AsyncSequence` produces an element. - public func chunks(ofCount count: Int, or signal: Signal, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence { + public func chunks(ofCount count: Int, or signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence { chunks(ofCount: count, or: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when a signal `AsyncSequence` produces an element. - public func chunked(by signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + public func chunked(by signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: signal, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks when a signal `AsyncSequence` produces an element. - public func chunked(by signal: Signal, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence { + public func chunked(by signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence { chunked(by: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: count, signal: timer, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given count or when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> { + public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> { chunks(ofCount: count, or: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunked(by timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + public func chunked(by timer: AsyncTimerSequence, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: timer, produceEmptyChunks: produceEmptyChunks) } /// Creates an asynchronous sequence that creates chunks when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunked(by timer: AsyncTimerSequence, produceEmptyChunks: Bool = true) -> AsyncChunksOfCountOrSignalSequence> { + public func chunked(by timer: AsyncTimerSequence, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence> { chunked(by: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks) } } diff --git a/Tests/AsyncAlgorithmsTests/TestChunk.swift b/Tests/AsyncAlgorithmsTests/TestChunk.swift index d94aa62a..c88ec9fe 100644 --- a/Tests/AsyncAlgorithmsTests/TestChunk.swift +++ b/Tests/AsyncAlgorithmsTests/TestChunk.swift @@ -54,7 +54,7 @@ final class TestChunk: XCTestCase { validate { "--1--|" "XX-XX|" - $0.inputs[0].chunked(by: $0.inputs[1], produceEmptyChunks: false).map(concatCharacters) + $0.inputs[0].chunked(by: $0.inputs[1]).map(concatCharacters) "---1-|" } } @@ -63,8 +63,8 @@ final class TestChunk: XCTestCase { validate { "--1--|" "XX-XX|" - $0.inputs[0].chunked(by: $0.inputs[1]).map(concatCharacters) - "''''-1''|" + $0.inputs[0].chunked(by: $0.inputs[1], produceEmptyChunks: true).map(concatCharacters) + "''''-1''|" } } @@ -99,7 +99,7 @@ final class TestChunk: XCTestCase { validate { "AB --A-B -|" "-- X---- X|" - $0.inputs[0].chunks(ofCount: 2, or: $0.inputs[1], produceEmptyChunks: false).map(concatCharacters) + $0.inputs[0].chunks(ofCount: 2, or: $0.inputs[1]).map(concatCharacters) "-'AB'----'AB'-|" } } @@ -108,8 +108,8 @@ final class TestChunk: XCTestCase { validate { "AB --A-B -|" "-- X---- X|" - $0.inputs[0].chunks(ofCount: 2, or: $0.inputs[1]).map(concatCharacters) - "-'AB'''---'AB'''|" + $0.inputs[0].chunks(ofCount: 2, or: $0.inputs[1], produceEmptyChunks: true).map(concatCharacters) + "-'AB'''---'AB'''|" } } @@ -162,7 +162,7 @@ final class TestChunk: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "-- 1- --|" - $0.inputs[0].chunked(by: .repeating(every: .steps(2), clock: $0.clock), produceEmptyChunks: false).map(concatCharacters) + $0.inputs[0].chunked(by: .repeating(every: .steps(2), clock: $0.clock)).map(concatCharacters) "-- -1 --|" } } @@ -171,8 +171,8 @@ final class TestChunk: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "-- 1- --|" - $0.inputs[0].chunked(by: .repeating(every: .steps(2), clock: $0.clock)).map(concatCharacters) - "-'' -1 -''|" + $0.inputs[0].chunked(by: .repeating(every: .steps(2), clock: $0.clock), produceEmptyChunks: true).map(concatCharacters) + "-'' -1 -''|" } } @@ -207,7 +207,7 @@ final class TestChunk: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "AB --A-B -|" - $0.inputs[0].chunks(ofCount: 2, or: .repeating(every: .steps(8), clock: $0.clock), produceEmptyChunks: false).map(concatCharacters) + $0.inputs[0].chunks(ofCount: 2, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) "-'AB'----'AB'-|" } } @@ -216,8 +216,8 @@ final class TestChunk: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "AB --A-B -|" - $0.inputs[0].chunks(ofCount: 2, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) - "-'AB'----'AB'''|" + $0.inputs[0].chunks(ofCount: 2, or: .repeating(every: .steps(8), clock: $0.clock), produceEmptyChunks: true).map(concatCharacters) + "-'AB'----'AB'''|" } } @@ -225,7 +225,7 @@ final class TestChunk: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "ABCDE --- ABCDE |" - $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock), produceEmptyChunks: false).map(concatCharacters) + $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) "----'ABCDE'--- ----'ABCDE'|" } } @@ -234,8 +234,8 @@ final class TestChunk: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "ABCDE --- ABCDE |" - $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) - "----'ABCDE'--'' ----'ABCDE'|" + $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock), produceEmptyChunks: true).map(concatCharacters) + "----'ABCDE'--'' ----'ABCDE'|" } }