From aa19372294a2dc7a60f9cd4b11eb4d5f0ef0ce6d Mon Sep 17 00:00:00 2001 From: Shubhransh Gupta <54713516+shubhransh-gupta@users.noreply.github.com> Date: Fri, 25 Sep 2026 22:23:36 +0530 Subject: [PATCH] Add uniqued(on:uniquingWith:) and uniqued(uniquingWith:) conflict-resolving overloads --- Guides/Unique.md | 10 ++++ Sources/Algorithms/Unique.swift | 59 ++++++++++++++++++++ Tests/SwiftAlgorithmsTests/UniqueTests.swift | 46 +++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/Guides/Unique.md b/Guides/Unique.md index 67e43776f..ea65ecffa 100644 --- a/Guides/Unique.md +++ b/Guides/Unique.md @@ -26,11 +26,21 @@ the elements. `uniqued(on:)` has a matching lazy version that is added to ```swift extension Sequence where Element: Hashable { func uniqued() -> UniquedSequence + + func uniqued( + uniquingWith combine: (Element, Element) throws -> Element + ) rethrows -> [Element] } extension Sequence { func uniqued(on projection: (Element) throws -> Subject) rethrows -> [Element] where Subject: Hashable + + func uniqued( + on projection: (Element) throws -> Subject, + uniquingWith combine: (Element, Element) throws -> Element + ) rethrows -> [Element] + where Subject: Hashable } extension LazySequenceProtocol { diff --git a/Sources/Algorithms/Unique.swift b/Sources/Algorithms/Unique.swift index 5ac6ef40e..439085837 100644 --- a/Sources/Algorithms/Unique.swift +++ b/Sources/Algorithms/Unique.swift @@ -124,6 +124,65 @@ extension Sequence { } return result } + + /// Returns an array with the unique elements of this sequence (as determined + /// by the given projection), using the provided closure to combine or resolve + /// duplicate elements. + /// + /// As the sequence is iterated, the `combine` closure is called with the + /// current accumulated value and the newly encountered value for any duplicate + /// projection keys. The return value replaces the previous element in the + /// resulting array while preserving the position of its first occurrence. + /// + /// - Parameters: + /// - projection: A closure that transforms an element into the value to use + /// for uniqueness. + /// - combine: A closure called when two elements produce the same projection + /// key. The closure takes the existing element and the duplicate element, + /// returning the combined element to retain. + /// + /// - Returns: An array with the unique elements in first-occurrence order. + /// + /// - Complexity: O(*n*), where *n* is the length of the sequence. + @inlinable + public func uniqued( + on projection: (Element) throws -> Subject, + uniquingWith combine: (Element, Element) throws -> Element + ) rethrows -> [Element] { + var seenIndices: [Subject: Int] = [:] + var result: [Element] = [] + + for element in self { + let key = try projection(element) + if let existingIndex = seenIndices[key] { + result[existingIndex] = try combine(result[existingIndex], element) + } else { + seenIndices[key] = result.count + result.append(element) + } + } + + return result + } +} + +extension Sequence where Element: Hashable { + /// Returns an array with the unique elements of this sequence, using the + /// provided closure to combine or resolve duplicate elements. + /// + /// - Parameter combine: A closure called when two duplicate elements are + /// encountered. The closure takes the existing element and the duplicate + /// element, returning the combined element to retain. + /// + /// - Returns: An array with the unique elements in first-occurrence order. + /// + /// - Complexity: O(*n*), where *n* is the length of the sequence. + @inlinable + public func uniqued( + uniquingWith combine: (Element, Element) throws -> Element + ) rethrows -> [Element] { + try uniqued(on: { $0 }, uniquingWith: combine) + } } //===----------------------------------------------------------------------===// diff --git a/Tests/SwiftAlgorithmsTests/UniqueTests.swift b/Tests/SwiftAlgorithmsTests/UniqueTests.swift index 0489e00f6..f700933ba 100644 --- a/Tests/SwiftAlgorithmsTests/UniqueTests.swift +++ b/Tests/SwiftAlgorithmsTests/UniqueTests.swift @@ -54,4 +54,50 @@ final class UniqueTests: XCTestCase { let d = Array(repeating: "Andromeda", count: 10) expectEqualSequences(d.lazy.uniqued(on: { $0.first }), ["Andromeda"]) } + + func testUniqueOnUniquingWith() { + struct Person: Equatable { + let id: Int + var score: Int + } + + let people = [ + Person(id: 1, score: 10), + Person(id: 2, score: 20), + Person(id: 1, score: 30), + Person(id: 3, score: 5), + Person(id: 2, score: 15), + ] + + // Accumulate scores while preserving first occurrence order (id: 1, 2, 3) + let combined = people.uniqued(on: \.id) { current, incoming in + Person(id: current.id, score: current.score + incoming.score) + } + XCTAssertEqual(combined, [ + Person(id: 1, score: 40), + Person(id: 2, score: 35), + Person(id: 3, score: 5), + ]) + + // Empty sequence + let empty: [Person] = [] + XCTAssertEqual(empty.uniqued(on: \.id, uniquingWith: { _, incoming in incoming }), []) + + // Throwing projection and combine + struct CustomError: Error, Equatable {} + XCTAssertThrowsError( + try [1, 2, 3].uniqued(on: { (_: Int) -> Int in throw CustomError() }, uniquingWith: { a, _ in a }) + ) + XCTAssertThrowsError( + try [1, 1].uniqued(on: { $0 }, uniquingWith: { _, _ in throw CustomError() }) + ) + } + + func testUniqueUniquingWith() { + let numbers = [1, 2, 3, 1, 2, 4] + let result = numbers.uniqued { current, incoming in + current * 10 + incoming + } + XCTAssertEqual(result, [11, 22, 3, 4]) + } }