diff --git a/Guides/Reductions.md b/Guides/Reductions.md new file mode 100644 index 00000000..93b3cb68 --- /dev/null +++ b/Guides/Reductions.md @@ -0,0 +1,139 @@ +# Reductions + +[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Reductions.swift) | + [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/ReductionsTests.swift)] + +Produces a sequence of values. + +This has the behaviour of reduce, but also returns all intermediate results. + +```swift +let exclusiveRunningTotal = (1...5).reductions(0, +) +print(exclusiveRunningTotal) +// prints [0, 1, 3, 6, 10, 15] + +var value = 0 +let intoRunningTotal = (1...5).reductions(into: &value, +=) +print(intoRunningTotal) +// prints [0, 1, 3, 6, 10, 15] +print(value) +// prints 15 + +let inclusiveRunningTotal = (1...5).reductions(+) +print(inclusiveRunningTotal) +// prints [1, 3, 6, 10, 15] +``` + +## Detailed Design + +One trio of methods are added to `LazySequenceProtocol` for a lazily evaluated +sequence and another trio are added to `Sequence` which are eagerly evaluated. + +```swift +extension LazySequenceProtocol { + + public func reductions( + _ initial: Result, + _ transform: @escaping (Result, Element) -> Result + ) -> ExclusiveReductions + + public func reductions( + into initial: inout Result, + _ transform: @escaping (inout Result, Element) -> Void + ) -> ExclusiveReductions + + public func reductions( + _ transform: @escaping (Element, Element) -> Element + ) -> InclusiveReductions +} +``` + +```swift +extension Sequence { + + public func reductions( + _ initial: Result, + _ transform: (Result, Element) throws -> Result + ) rethrows -> [Result] + + public func reductions( + into initial: inout Result, + _ transform: (inout Result, Element) throws -> Void + ) rethrows -> [Result] + + public func reductions( + _ transform: (Element, Element) throws -> Element + ) rethrows -> [Element] +} +``` + +### Complexity + +Calling the lazy methods, those defined on `LazySequenceProtocol`, is O(_1_). +Calling the eager methods, those returning an array, is O(_n_). + +### Naming + +While the name `scan` is the term of art for this function, it has been +discussed that `reductions` aligns better with the existing `reduce` function +and will aid newcomers that might not know the existing `scan` term. + +Deprecated `scan` methods have been added for people who are familiar with the +term, so they can easily discover the `reductions` methods via compiler +deprecation warnings. + +Below are two quotes from the Swift forum [discussion about SE-0045][SE-0045] +which proposed adding `scan` to the standard library and one from +[issue #25][Issue 25] on the swift-algorithms GitHub project. These provide +the reasoning to use the name `reductions`. + +[Brent Royal-Gordon][Brent_Royal-Gordon]: +> I really like the `reduce`/`reductions` pairing instead of `reduce`/`scan`; +it does a really good job of explaining the relationship between the two +functions. + +[David Rönnqvist][David Rönnqvist]: +> As other have already pointed out, I also feel that `scan` is the least +intuitive name among these and that the `reduce`/`reductions` pairing would do +a good job at explaining the relation between the two. + +[Kyle Macomber][Kyle Macomber]: +> As someone unfamiliar with the prior art, `reductions` strikes me as very +approachable—I feel like I can extrapolate the expected behavior purely from my +familiarity with `reduce`. + +As part of early discussions, it was decided to have two variants, one which +takes an initial value to use for the first element in the returned sequence, +and another which uses the first value of the base sequence as the initial +value. C++ calls these variants exclusive and inclusive respectively and so +these terms carry through as the name for the lazy sequences; +`ExclusiveReductions` and `InclusiveReductions`. + +[SE-0045]: https://forums.swift.org/t/review-se-0045-add-scan-prefix-while-drop-while-and-iterate-to-the-stdlib/2382 +[Issue 25]: https://github.com/apple/swift-algorithms/issues/25 +[Brent_Royal-Gordon]: https://forums.swift.org/t/review-se-0045-add-scan-prefix-while-drop-while-and-iterate-to-the-stdlib/2382/6 +[David Rönnqvist]: https://forums.swift.org/t/review-se-0045-add-scan-prefix-while-drop-while-and-iterate-to-the-stdlib/2382/8 +[Kyle Macomber]: https://github.com/apple/swift-algorithms/issues/25#issuecomment-709317894 + +### Comparison with other langauges + +**C++:** As of C++17, the `` library includes both +[`exclusive_scan`][C++ Exclusive] and [`inclusive_scan`][C++ Inclusive] +functions. + +**[Clojure][Clojure]:** Clojure 1.2 added a `reductions` function. + +**[Haskell][Haskell]:** Haskell includes a `scan` function for its +`Traversable` type, which is akin to Swift's `Sequence`. + +**Python:** Python’s `itertools` includes an `accumulate` method. In version +3.3, a function paramenter was added. Version 3.8 added the optional initial +parameter. + +**[Rust][Rust]:** Rust provides a `scan` function. + +[C++ Exclusive]: https://en.cppreference.com/w/cpp/algorithm/exclusive_scan +[C++ Inclusive]: https://en.cppreference.com/w/cpp/algorithm/inclusive_scan +[Clojure]: http://clojure.github.io/clojure/clojure.core-api.html#clojure.core/reductions +[Haskell]: http://hackage.haskell.org/package/base-4.8.2.0/docs/Prelude.html#v:scanl +[Rust]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.scan diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift new file mode 100644 index 00000000..627cea48 --- /dev/null +++ b/Sources/Algorithms/Reductions.swift @@ -0,0 +1,654 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// MARK: - Exclusive Reductions + +extension LazySequenceProtocol { + + /// Returns a sequence containing the accumulated results of combining the + /// elements of the sequence using the given closure. + /// + /// This can be seen as applying the reduce function to each element and + /// providing the initial value followed by these results as a sequence. + /// + /// ``` + /// let runningTotal = [1, 2, 3, 4].lazy.reductions(0, +) + /// print(Array(runningTotal)) + /// + /// // prints [0, 1, 3, 6, 10] + /// ``` + /// + /// - Parameters: + /// - initial: The value to use as the initial value. + /// - transform: A closure that combines the previously reduced result and + /// the next element in the receiving sequence. + /// - Returns: A sequence of the initial value followed by the reduced + /// elements. + /// + /// - Complexity: O(1) + @inlinable + public func reductions( + _ initial: Result, + _ transform: @escaping (Result, Element) -> Result + ) -> ExclusiveReductions { + + var result = initial + return reductions(into: &result) { result, element in + result = transform(result, element) + } + } + + /// Returns a sequence containing the accumulated results of combining the + /// elements of the sequence using the given closure. + /// + /// This can be seen as applying the reduce function to each element and + /// providing the initial value followed by these results as a sequence. + /// + /// ``` + /// let runningTotal = [1, 2, 3, 4].lazy.reductions(into: 0, +) + /// print(Array(runningTotal)) + /// + /// // prints [0, 1, 3, 6, 10] + /// ``` + /// + /// - Parameters: + /// - initial: The value to use as the initial value. + /// - transform: A closure that combines the previously reduced result and + /// the next element in the receiving sequence. + /// - Returns: A sequence of the initial value followed by the reduced + /// elements. + /// + /// - Complexity: O(1) + @inlinable + public func reductions( + into initial: inout Result, + _ transform: @escaping (inout Result, Element) -> Void + ) -> ExclusiveReductions { + ExclusiveReductions(base: self, initial: initial, transform: transform) + } +} + +extension Sequence { + + /// Returns an array containing the accumulated results of combining the + /// elements of the sequence using the given closure. + /// + /// This can be seen as applying the reduce function to each element and + /// providing the initial value followed by these results as a sequence. + /// + /// ``` + /// let runningTotal = [1, 2, 3, 4].reductions(0, +) + /// print(runningTotal) + /// + /// // prints [0, 1, 3, 6, 10] + /// ``` + /// + /// When `reductions(_:_:)` is called, the following steps occur: + /// + /// 1. The `initial` result is added to an array of results. + /// 2. The `transform` closure is called with the `initial` result and the + /// first element of the sequence, appending the result to the array. + /// 3. The closure is called again repeatedly with the updated accumulating + /// result and each element of the sequence, adding each result to the + /// array. + /// 4. When the sequence is exhausted, the results array is returned to the + /// caller. + /// + /// If the sequence has no elements, `transform` is never executed and + /// an array containing only the `initial` result is returned. + /// + /// - Parameters: + /// - initial: The value to use as the initial value. + /// - transform: A closure that combines the previously reduced result and + /// the next element in the receiving sequence. + /// - Returns: An array of the initial value followed by the reduced elements. + /// + /// - Complexity: O(_n_), where _n_ is the length of the sequence. + @inlinable + public func reductions( + _ initial: Result, + _ transform: (Result, Element) throws -> Result + ) rethrows -> [Result] { + + var result = initial + return try reductions(into: &result) { result, element in + result = try transform(result, element) + } + } + + /// Returns an array containing the accumulated results of combining the + /// elements of the sequence using the given closure. + /// + /// This can be seen as applying the reduce function to each element and + /// providing the initial value followed by these results as a sequence. + /// + /// ``` + /// let runningTotal = [1, 2, 3, 4].reductions(into: 0, +) + /// print(runningTotal) + /// + /// // prints [0, 1, 3, 6, 10] + /// ``` + /// + /// When `reductions(into:_:_)` is called, the following steps occur: + /// + /// 1. The `initial` result is added to an array of results. + /// 2. The `transform` closure is called with the `initial` result and the + /// first element of the sequence, appending the result to the array. + /// 3. The closure is called again repeatedly with the updated accumulating + /// result and each element of the sequence, adding each result to the + /// array. + /// 4. When the sequence is exhausted, the results array is returned to the + /// caller. + /// + /// If the sequence has no elements, `transform` is never executed and + /// an array containing only the `initial` result is returned. + /// + /// - Parameters: + /// - initial: The value to use as the initial value. + /// - transform: A closure that combines the previously reduced result and + /// the next element in the receiving sequence. + /// - Returns: An array of the initial value followed by the reduced elements. + /// + /// - Complexity: O(_n_), where _n_ is the length of the sequence. + @inlinable + public func reductions( + into initial: inout Result, + _ transform: (inout Result, Element) throws -> Void + ) rethrows -> [Result] { + + var output = [Result]() + output.reserveCapacity(underestimatedCount + 1) + output.append(initial) + + for element in self { + try transform(&initial, element) + output.append(initial) + } + + return output + } +} + +/// A sequence of applying a transform to the element of a sequence and the +/// previously transformed result. +public struct ExclusiveReductions { + @usableFromInline + internal let base: Base + + @usableFromInline + internal let initial: Result + + @usableFromInline + internal let transform: (inout Result, Base.Element) -> Void + + @usableFromInline + internal init( + base: Base, + initial: Result, + transform: @escaping (inout Result, Base.Element) -> Void + ) { + self.base = base + self.initial = initial + self.transform = transform + } +} + +extension ExclusiveReductions: Sequence { + public struct Iterator: IteratorProtocol { + @usableFromInline + internal var iterator: Base.Iterator + + @usableFromInline + internal var current: Result? + + @usableFromInline + internal let transform: (inout Result, Base.Element) -> Void + + @usableFromInline + internal init( + iterator: Base.Iterator, + current: Result? = nil, + transform: @escaping (inout Result, Base.Element) -> Void + ) { + self.iterator = iterator + self.current = current + self.transform = transform + } + + @inlinable + public mutating func next() -> Result? { + guard let result = current else { return nil } + current = iterator.next().map { element in + var result = result + transform(&result, element) + return result + } + return result + } + } + + @inlinable + public func makeIterator() -> Iterator { + Iterator(iterator: base.makeIterator(), + current: initial, + transform: transform) + } +} + +extension ExclusiveReductions: Collection where Base: Collection { + public struct Index: Comparable { + @usableFromInline + internal let representation: ReductionsIndexRepresentation + + @inlinable + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.representation < rhs.representation + } + + @usableFromInline + internal static func base(index: Base.Index, result: Result) -> Self { + Self(representation: .base(index: index, result: result)) + } + + @usableFromInline + internal static var start: Self { Self(representation: .start) } + + @usableFromInline + internal static var end: Self { Self(representation: .end) } + } + + @inlinable + public var startIndex: Index { .start } + + @inlinable + public var endIndex: Index { .end } + + @inlinable + public subscript(position: Index) -> Result { + switch position.representation { + case .start: return initial + case .base(_, let result): return result + case .end: fatalError("Cannot get element of end index.") + } + } + + @inlinable + public func index(after i: Index) -> Index { + func index(base index: Base.Index, previous: Result) -> Index { + guard index != base.endIndex else { return endIndex } + var previous = previous + transform(&previous, base[index]) + return .base(index: index, result: previous) + } + switch i.representation { + case .start: + return index(base: base.startIndex, previous: initial) + case let .base(i, result): + return index(base: base.index(after: i), previous: result) + case .end: + fatalError("Cannot get index after end index.") + } + } + + @inlinable + public func distance(from start: Index, to end: Index) -> Int { + switch (start.representation, end.representation) { + case (.start, .start): + return 0 + case let (.start, .base(index, _)): + return base.distance(from: base.startIndex, to: index) + 1 + case (.start, .end): + return base.distance(from: base.startIndex, to: base.endIndex) + 1 + case let (.base(index, _), .start): + return base.distance(from: index, to: base.startIndex) - 1 + case let (.base(start, _), .base(end, _)): + return base.distance(from: start, to: end) + case let (.base(index, _), .end): + return base.distance(from: index, to: base.endIndex) + case (.end, .start): + return base.distance(from: base.endIndex, to: base.startIndex) - 1 + case let (.end, .base(index, _)): + return base.distance(from: base.endIndex, to: index) + case (.end, .end): + return 0 + } + } +} + +extension ExclusiveReductions: LazySequenceProtocol + where Base: LazySequenceProtocol {} + +extension ExclusiveReductions: LazyCollectionProtocol + where Base: LazyCollectionProtocol {} + +// MARK: - Inclusive Reductions + +extension LazySequenceProtocol { + + /// Returns a sequence containing the accumulated results of combining the + /// elements of the sequence using the given closure. + /// + /// This can be seen as applying the reduce function to each element and + /// providing the initial value followed by these results as a sequence. + /// + /// ``` + /// let runningTotal = [1, 2, 3, 4].lazy.reductions(+) + /// print(Array(runningTotal)) + /// + /// // prints [1, 3, 6, 10] + /// ``` + /// + /// - Parameters: + /// - transform: A closure that combines the previously reduced result and + /// the next element in the receiving sequence. + /// - Returns: A sequence of the reduced elements. + /// + /// - Complexity: O(1) + @inlinable + public func reductions( + _ transform: @escaping (Element, Element) -> Element + ) -> InclusiveReductions { + InclusiveReductions(base: self, transform: transform) + } +} + +extension Sequence { + + /// Returns an array containing the accumulated results of combining the + /// elements of the sequence using the given closure. + /// + /// This can be seen as applying the reduce function to each element and + /// providing the initial value followed by these results as a sequence. + /// + /// ``` + /// let runningTotal = [1, 2, 3, 4].reductions(+) + /// print(runningTotal) + /// + /// // prints [1, 3, 6, 10] + /// ``` + /// + /// When `reductions(_:)` is called, the following steps occur: + /// + /// 1. The `transform` closure is called with the first and second elements + /// of the sequence, appending the result to an array of results. + /// 2. The closure is called again repeatedly with the updated accumulating + /// result and the next element of the sequence, adding each result to the + /// array. + /// 3. When the sequence is exhausted, the results array is returned to the + /// caller. + /// + /// If the sequence has no elements, `transform` is never executed and + /// an empty array is returned. + /// + /// If the sequence has one element, `transform` is never executed and + /// an array containing only that first element is returned. + /// + /// - Parameters: + /// - transform: A closure that combines the previously reduced result and + /// the next element in the receiving sequence. + /// - Returns: An array of the reduced elements. + /// + /// - Complexity: O(_n_), where _n_ is the length of the sequence. + @inlinable + public func reductions( + _ transform: (Element, Element) throws -> Element + ) rethrows -> [Element] { + var iterator = makeIterator() + guard let initial = iterator.next() else { return [] } + return try IteratorSequence(iterator).reductions(initial, transform) + } +} + +public struct InclusiveReductions { + @usableFromInline + internal let base: Base + + @usableFromInline + internal let transform: (Base.Element, Base.Element) -> Base.Element + + @usableFromInline + internal init( + base: Base, + transform: @escaping (Base.Element, Base.Element) -> Base.Element + ) { + self.base = base + self.transform = transform + } +} + +extension InclusiveReductions: Sequence { + public struct Iterator: IteratorProtocol { + @usableFromInline + internal var iterator: Base.Iterator + + @usableFromInline + internal var element: Base.Element? + + @usableFromInline + internal let transform: (Base.Element, Base.Element) -> Base.Element + + @usableFromInline + internal init( + iterator: Base.Iterator, + element: Base.Element? = nil, + transform: @escaping (Base.Element, Base.Element) -> Base.Element + ) { + self.iterator = iterator + self.element = element + self.transform = transform + } + + @inlinable + public mutating func next() -> Base.Element? { + guard let previous = element else { + element = iterator.next() + return element + } + guard let next = iterator.next() else { return nil } + element = transform(previous, next) + return element + } + } + + @inlinable + public func makeIterator() -> Iterator { + Iterator(iterator: base.makeIterator(), + transform: transform) + } +} + +extension InclusiveReductions: Collection where Base: Collection { + public struct Index: Comparable { + @usableFromInline + internal let representation: ReductionsIndexRepresentation + + @inlinable + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.representation < rhs.representation + } + + @usableFromInline + internal static func base(index: Base.Index, result: Base.Element) -> Self { + Self(representation: .base(index: index, result: result)) + } + + @usableFromInline + internal static var start: Self { Self(representation: .start) } + + @usableFromInline + internal static var end: Self { Self(representation: .end) } + } + + @inlinable + public var startIndex: Index { + guard base.startIndex != base.endIndex else { return endIndex } + return .start + } + + @inlinable + public var endIndex: Index { .end } + + @inlinable + public subscript(position: Index) -> Base.Element { + switch position.representation { + case .start: return base[base.startIndex] + case .base(_, let result): return result + case .end: fatalError("Cannot get element of end index.") + } + } + + @inlinable + public func index(after i: Index) -> Index { + func index(after i: Base.Index, previous: Base.Element) -> Index { + let index = base.index(after: i) + guard index != base.endIndex else { return endIndex } + return .base(index: index, result: transform(previous, base[index])) + } + switch i.representation { + case .start: + return index(after: base.startIndex, previous: base[base.startIndex]) + case let .base(i, element): + return index(after: i, previous: element) + case .end: + fatalError("Cannot get index after end index.") + } + } + + @inlinable + public func distance(from start: Index, to end: Index) -> Int { + switch (start.representation, end.representation) { + case (.start, .start): + return 0 + case let (.start, .base(index, _)): + return base.distance(from: base.startIndex, to: index) + case (.start, .end): + return base.distance(from: base.startIndex, to: base.endIndex) + case let (.base(index, _), .start): + return base.distance(from: index, to: base.startIndex) + case let (.base(start, _), .base(end, _)): + return base.distance(from: start, to: end) + case let (.base(index, _), .end): + return base.distance(from: index, to: base.endIndex) + case (.end, .start): + return base.distance(from: base.endIndex, to: base.startIndex) + case let (.end, .base(index, _)): + return base.distance(from: base.endIndex, to: index) + case (.end, .end): + return 0 + } + } +} + +extension InclusiveReductions: LazySequenceProtocol + where Base: LazySequenceProtocol {} + +extension InclusiveReductions: LazyCollectionProtocol + where Base: LazyCollectionProtocol {} + +// MARK: - ReductionsIndexRepresentation + +@usableFromInline +enum ReductionsIndexRepresentation { + case start + case base(index: BaseIndex, result: Result) + case end +} + +extension ReductionsIndexRepresentation: Equatable { + @usableFromInline + static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.start, .start): return true + case (.end, .end): return true + case let (.base(lhs, _), .base(rhs, _)): return lhs == rhs + default: return false + } + } +} + +extension ReductionsIndexRepresentation: Comparable { + @usableFromInline + static func < (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (_, .start): return false + case (.start, _): return true + case (.end, _): return false + case (_, .end): return true + case let (.base(lhs, _), .base(rhs, _)): return lhs < rhs + } + } +} + +// MARK: - Scan + +extension LazySequenceProtocol { + + @available(*, deprecated, message: "Use reductions(_:_:) instead.") + @inlinable + public func scan( + _ initial: Result, + _ transform: @escaping (Result, Element) -> Result + ) -> ExclusiveReductions { + reductions(initial, transform) + } + + @available(*, deprecated, message: "Use reductions(into:_:) instead.") + @inlinable + public func scan( + into initial: inout Result, + _ transform: @escaping (inout Result, Element) -> Void + ) -> ExclusiveReductions { + reductions(into: &initial, transform) + } +} + +extension Sequence { + + @available(*, deprecated, message: "Use reductions(_:_:) instead.") + @inlinable + public func scan( + _ initial: Result, + _ transform: (Result, Element) throws -> Result + ) rethrows -> [Result] { + try reductions(initial, transform) + } + + @available(*, deprecated, message: "Use reductions(into:_:) instead.") + @inlinable + public func scan( + into initial: inout Result, + _ transform: (inout Result, Element) throws -> Void + ) rethrows -> [Result] { + try reductions(into: &initial, transform) + } +} + +extension LazySequenceProtocol { + + @available(*, deprecated, message: "Use reductions(_:) instead.") + @inlinable + public func scan( + _ transform: @escaping (Element, Element) -> Element + ) -> InclusiveReductions { + reductions(transform) + } +} + +extension Sequence { + + @available(*, deprecated, message: "Use reductions(_:) instead.") + @inlinable + public func scan( + _ transform: (Element, Element) throws -> Element + ) rethrows -> [Element] { + try reductions(transform) + } +} diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift new file mode 100644 index 00000000..60a53709 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -0,0 +1,93 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import Algorithms + +final class ReductionsTests: XCTestCase { + + struct TestError: Error {} + + // MARK: - Exclusive Reductions + + func testExclusiveLazy() { + XCTAssertEqualSequences((1...).prefix(4).lazy.reductions(0, +), [0, 1, 3, 6, 10]) + XCTAssertEqualSequences((1...).prefix(1).lazy.reductions(0, +), [0, 1]) + XCTAssertEqualSequences((1...).prefix(0).lazy.reductions(0, +), [0]) + + XCTAssertEqualCollections([1, 2, 3, 4].lazy.reductions(0, +), [0, 1, 3, 6, 10]) + XCTAssertEqualCollections([1].lazy.reductions(0, +), [0, 1]) + XCTAssertEqualCollections(EmptyCollection().lazy.reductions(0, +), [0]) + + var value = 0 + XCTAssertEqual([1, 2, 3, 4].lazy.reductions(into: &value, +=), [0, 1, 3, 6, 10]) + XCTAssertEqual(value, 10) + + value = 0 + XCTAssertEqual([1].lazy.reductions(into: &value, +=), [0, 1]) + XCTAssertEqual(value, 1) + + value = 0 + XCTAssertEqual(EmptyCollection().lazy.reductions(into: &value, +=), [0]) + XCTAssertEqual(value, 0) + + XCTAssertLazySequence((1...).prefix(1).lazy.reductions(0, +)) + XCTAssertLazySequence([1].lazy.reductions(0, +)) + XCTAssertLazyCollection([1].lazy.reductions(0, +)) + } + + func testExclusiveEager() { + XCTAssertEqual([1, 2, 3, 4].reductions(0, +), [0, 1, 3, 6, 10]) + XCTAssertEqual([1].reductions(0, +), [0, 1]) + XCTAssertEqual(EmptyCollection().reductions(0, +), [0]) + + var value = 0 + XCTAssertEqual([1, 2, 3, 4].reductions(into: &value, +=), [0, 1, 3, 6, 10]) + XCTAssertEqual(value, 10) + + value = 0 + XCTAssertEqual([1].reductions(into: &value, +=), [0, 1]) + XCTAssertEqual(value, 1) + + value = 0 + XCTAssertEqual(EmptyCollection().reductions(into: &value, +=), [0]) + XCTAssertEqual(value, 0) + + XCTAssertNoThrow(try [].reductions(0) { _, _ in throw TestError() }) + XCTAssertThrowsError(try [1].reductions(0) { _, _ in throw TestError() }) + } + + // MARK: - Inclusive Reductions + + func testInclusiveLazy() { + XCTAssertEqualSequences((1...).prefix(4).lazy.reductions(+), [1, 3, 6, 10]) + XCTAssertEqualSequences((1...).prefix(1).lazy.reductions(+), [1]) + XCTAssertEqualSequences((1...).prefix(0).lazy.reductions(+), []) + + XCTAssertEqualCollections([1, 2, 3, 4].lazy.reductions(+), [1, 3, 6, 10]) + XCTAssertEqualCollections([1].lazy.reductions(+), [1]) + XCTAssertEqualCollections(EmptyCollection().lazy.reductions(+), []) + + XCTAssertLazySequence((1...).prefix(1).lazy.reductions(+)) + XCTAssertLazySequence([1].lazy.reductions(+)) + XCTAssertLazyCollection([1].lazy.reductions(+)) + } + + func testInclusiveEager() { + XCTAssertEqual([1, 2, 3, 4].reductions(+), [1, 3, 6, 10]) + XCTAssertEqual([1].reductions(+), [1]) + XCTAssertEqual(EmptyCollection().reductions(+), []) + + XCTAssertNoThrow(try [].reductions { _, _ in throw TestError() }) + XCTAssertNoThrow(try [1].reductions { _, _ in throw TestError() }) + XCTAssertThrowsError(try [1, 1].reductions { _, _ in throw TestError() }) + } +} diff --git a/Tests/SwiftAlgorithmsTests/TestUtilities.swift b/Tests/SwiftAlgorithmsTests/TestUtilities.swift index 11520b93..b160caa6 100644 --- a/Tests/SwiftAlgorithmsTests/TestUtilities.swift +++ b/Tests/SwiftAlgorithmsTests/TestUtilities.swift @@ -68,6 +68,21 @@ func XCTAssertEqualSequences( func XCTAssertLazySequence(_: S) {} func XCTAssertLazyCollection(_: S) {} +/// Asserts two collections are equal by using their indices to access elements. +func XCTAssertEqualCollections( + _ expression1: @autoclosure () throws -> C1, + _ expression2: @autoclosure () throws -> C2, + _ message: @autoclosure () -> String = "", + file: StaticString = #file, line: UInt = #line +) rethrows where C1.Element: Equatable, C1.Element == C2.Element { + let c1 = try expression1() + let c2 = try expression2() + XCTAssertEqual(c1.indices.count, c2.indices.count, message(), file: file, line: line) + for index in zip(c1.indices, c2.indices) { + XCTAssertEqual(c1[index.0], c2[index.1], message(), file: file, line: line) + } +} + /// Tests that all index traversal methods behave as expected. /// /// Verifies the correctness of the implementations of `startIndex`, `endIndex`,