Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Guides/Unique.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, Element>

func uniqued(
uniquingWith combine: (Element, Element) throws -> Element
) rethrows -> [Element]
}

extension Sequence {
func uniqued<Subject>(on projection: (Element) throws -> Subject) rethrows -> [Element]
where Subject: Hashable

func uniqued<Subject>(
on projection: (Element) throws -> Subject,
uniquingWith combine: (Element, Element) throws -> Element
) rethrows -> [Element]
where Subject: Hashable
}

extension LazySequenceProtocol {
Expand Down
59 changes: 59 additions & 0 deletions Sources/Algorithms/Unique.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Subject: Hashable>(
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)
}
}

//===----------------------------------------------------------------------===//
Expand Down
46 changes: 46 additions & 0 deletions Tests/SwiftAlgorithmsTests/UniqueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
}