diff --git a/Sources/Algorithms/Documentation.docc/Reductions.md b/Sources/Algorithms/Documentation.docc/Reductions.md index 50068218b..78375f2e5 100644 --- a/Sources/Algorithms/Documentation.docc/Reductions.md +++ b/Sources/Algorithms/Documentation.docc/Reductions.md @@ -16,8 +16,21 @@ print(inclusiveRunningTotal) // prints [1, 3, 6, 10, 15] ``` +If you only need the final value, but the combining operation has no natural +initial result, use the `reduce(_:)` method, which seeds the operation with the +first element and returns `nil` for an empty sequence: + +```swift +let total = (1...5).reduce(+) +// total == 15 + +let none = EmptyCollection().reduce(+) +// none == nil +``` + ## Topics +- ``Swift/Sequence/reduce(_:)`` - ``Swift/Sequence/reductions(_:)`` - ``Swift/Sequence/reductions(_:_:)`` - ``Swift/Sequence/reductions(into:_:)`` diff --git a/Sources/Algorithms/Reduce.swift b/Sources/Algorithms/Reduce.swift new file mode 100644 index 000000000..c14614547 --- /dev/null +++ b/Sources/Algorithms/Reduce.swift @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2026 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 +// +//===----------------------------------------------------------------------===// + +extension Sequence { + /// Returns the result of combining the elements of the sequence using the + /// given closure, or `nil` if the sequence has no elements. + /// + /// Use this method when the elements themselves are the values being + /// combined and there is no natural initial result. The first element of + /// the sequence is used as the initial result, and the closure combines + /// the running result with each subsequent element: + /// + /// ```swift + /// let numbers = [1, 2, 3, 4] + /// let sum = numbers.reduce(+) + /// // sum == 10 + /// + /// let none = EmptyCollection().reduce(+) + /// // none == nil + /// ``` + /// + /// This method is the single-value counterpart of `reductions(_:)`, which + /// additionally returns all of the intermediate results. + /// + /// - Parameter nextPartialResult: A closure that combines an accumulating + /// result and an element of the sequence into a new accumulating result. + /// - Returns: The final accumulated result, or `nil` if the sequence is + /// empty. + /// + /// - Complexity: O(*n*), where *n* is the length of the sequence. + @inlinable + public func reduce( + _ nextPartialResult: (Element, Element) throws -> Element + ) rethrows -> Element? { + var iterator = makeIterator() + guard var result = iterator.next() else { return nil } + while let element = iterator.next() { + result = try nextPartialResult(result, element) + } + return result + } +} diff --git a/Tests/SwiftAlgorithmsTests/ReduceTests.swift b/Tests/SwiftAlgorithmsTests/ReduceTests.swift new file mode 100644 index 000000000..da093b529 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/ReduceTests.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2026 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 Algorithms +import XCTest + +final class ReduceTests: XCTestCase { + struct TestError: Error {} + + func testReduce() { + XCTAssertEqual([1, 2, 3, 4].reduce(+), 10) + XCTAssertEqual([4].reduce(+), 4) + XCTAssertNil(EmptyCollection().reduce(+)) + + // matches the final element of the corresponding reductions + let sequence = [3, 1, 4, 1, 5] + XCTAssertEqual(sequence.reduce(+), sequence.reductions(+).last) + } + + func testReduceNonCommutative() { + // combines left-to-right, seeded with the first element + XCTAssertEqual([100, 10, 5].reduce(-), 85) + XCTAssertEqual(["a", "b", "c"].reduce(+), "abc") + } + + func testReduceSinglePassSequence() { + // consumes a single-pass sequence exactly once + XCTAssertEqual((1...).prefix(4).reduce(+), 10) + } + + func testReduceThrows() { + XCTAssertThrowsError( + try [1, 2].reduce { _, _ in throw TestError() } + ) + + // the closure is never called for empty or single-element sequences + XCTAssertNil(try EmptyCollection().reduce { _, _ in throw TestError() }) + XCTAssertEqual(try [7].reduce { _, _ in throw TestError() }, 7) + } +}