From 718220d0f8aa5fae85952d0844961700dbf5bb27 Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Wed, 7 Apr 2021 21:51:11 +0200 Subject: [PATCH] Make `uniqued()` lazy by default (#71) * Make `uniqued()` lazy by default --- Guides/Unique.md | 21 +++-- Sources/Algorithms/Unique.swift | 89 ++++++++++++++++++-- Tests/SwiftAlgorithmsTests/UniqueTests.swift | 23 ++++- 3 files changed, 117 insertions(+), 16 deletions(-) diff --git a/Guides/Unique.md b/Guides/Unique.md index 0b51686a..13de268d 100644 --- a/Guides/Unique.md +++ b/Guides/Unique.md @@ -5,7 +5,7 @@ Methods to strip repeated elements from a sequence or collection. -The `uniqued()` method returns an array, dropping duplicate elements +The `uniqued()` method returns a sequence, dropping duplicate elements from a sequence. The `uniqued(on:)` method does the same, using the result of the given closure to determine the "uniqueness" of each element. @@ -14,29 +14,36 @@ element. let numbers = [1, 2, 3, 3, 2, 3, 3, 2, 2, 2, 1] let unique = numbers.uniqued() -// unique == [1, 2, 3] +// Array(unique) == [1, 2, 3] ``` ## Detailed Design Both methods are available for sequences, with the simplest limited to when the element type conforms to `Hashable`. Both methods preserve -the relative order of the elements. +the relative order of the elements. `uniqued(on:)` has a matching lazy +version that is added to `LazySequenceProtocol`. ```swift extension Sequence where Element: Hashable { - func uniqued() -> [Element] + func uniqued() -> Uniqued } extension Sequence { - func uniqued(on: (Element) throws -> T) rethrows -> [Element] - where T: Hashable + func uniqued(on projection: (Element) throws -> Subject) rethrows -> [Element] + where Subject: Hashable +} + +extension LazySequenceProtocol { + func uniqued(on projection: @escaping (Element) -> Subject) -> Uniqued + where Subject: Hashable } ``` ### Complexity -The `uniqued` methods are O(_n_) in both time and space complexity. +The eager `uniqued(on:)` method is O(_n_) in both time and space complexity. +The lazy versions are O(_1_). ### Comparison with other languages diff --git a/Sources/Algorithms/Unique.swift b/Sources/Algorithms/Unique.swift index 5addd889..cb722894 100644 --- a/Sources/Algorithms/Unique.swift +++ b/Sources/Algorithms/Unique.swift @@ -9,25 +9,82 @@ // //===----------------------------------------------------------------------===// +/// A sequence wrapper that leaves out duplicate elements of a base sequence. +public struct Uniqued { + /// The base collection. + @usableFromInline + internal let base: Base + + /// The projection function. + @usableFromInline + internal let projection: (Base.Element) -> Subject + + @usableFromInline + internal init(base: Base, projection: @escaping (Base.Element) -> Subject) { + self.base = base + self.projection = projection + } +} + +extension Uniqued: Sequence { + /// The iterator for a `Uniqued` sequence. + public struct Iterator: IteratorProtocol { + @usableFromInline + internal var base: Base.Iterator + + @usableFromInline + internal let projection: (Base.Element) -> Subject + + @usableFromInline + internal var seen: Set = [] + + @usableFromInline + internal init( + base: Base.Iterator, + projection: @escaping (Base.Element) -> Subject + ) { + self.base = base + self.projection = projection + } + + @inlinable + public mutating func next() -> Base.Element? { + while let element = base.next() { + if seen.insert(projection(element)).inserted { + return element + } + } + return nil + } + } + + @inlinable + public func makeIterator() -> Iterator { + Iterator(base: base.makeIterator(), projection: projection) + } +} + +extension Uniqued: LazySequenceProtocol where Base: LazySequenceProtocol {} + //===----------------------------------------------------------------------===// // uniqued() //===----------------------------------------------------------------------===// extension Sequence where Element: Hashable { - /// Returns an array with only the unique elements of this sequence, in the + /// Returns a sequence with only the unique elements of this sequence, in the /// order of the first occurrence of each unique element. /// /// let animals = ["dog", "pig", "cat", "ox", "dog", "cat"] /// let uniqued = animals.uniqued() - /// print(uniqued) + /// print(Array(uniqued)) /// // Prints '["dog", "pig", "cat", "ox"]' /// - /// - Returns: An array with only the unique elements of this sequence. + /// - Returns: A sequence with only the unique elements of this sequence. /// . - /// - Complexity: O(*n*), where *n* is the length of the sequence. + /// - Complexity: O(1). @inlinable - public func uniqued() -> [Element] { - uniqued(on: { $0 }) + public func uniqued() -> Uniqued { + Uniqued(base: self, projection: { $0 }) } } @@ -40,7 +97,7 @@ extension Sequence { /// first characters: /// /// let animals = ["dog", "pig", "cat", "ox", "cow", "owl"] - /// let uniqued = animals.uniqued(on: {$0.first}) + /// let uniqued = animals.uniqued(on: { $0.first }) /// print(uniqued) /// // Prints '["dog", "pig", "cat", "ox"]' /// @@ -67,3 +124,21 @@ extension Sequence { return result } } + +//===----------------------------------------------------------------------===// +// lazy.uniqued() +//===----------------------------------------------------------------------===// + +extension LazySequenceProtocol { + /// Returns a lazy sequence with the unique elements of this sequence (as + /// determined by the given projection), in the order of the first occurrence + /// of each unique element. + /// + /// - Complexity: O(1). + @inlinable + public func uniqued( + on projection: @escaping (Element) -> Subject + ) -> Uniqued { + Uniqued(base: self, projection: projection) + } +} diff --git a/Tests/SwiftAlgorithmsTests/UniqueTests.swift b/Tests/SwiftAlgorithmsTests/UniqueTests.swift index 3be41a86..96372af6 100644 --- a/Tests/SwiftAlgorithmsTests/UniqueTests.swift +++ b/Tests/SwiftAlgorithmsTests/UniqueTests.swift @@ -17,10 +17,13 @@ final class UniqueTests: XCTestCase { let a = repeatElement(1...10, count: 15).joined().shuffled() let b = a.uniqued() XCTAssertEqual(b.sorted(), Set(a).sorted()) - XCTAssertEqual(10, b.count) + XCTAssertEqual(10, Array(b).count) let c: [Int] = [] - XCTAssertEqual(c.uniqued(), []) + XCTAssertEqualSequences(c.uniqued(), []) + + let d = Array(repeating: 1, count: 10) + XCTAssertEqualSequences(d.uniqued(), [1]) } func testUniqueOn() { @@ -30,5 +33,21 @@ final class UniqueTests: XCTestCase { let c: [Int] = [] XCTAssertEqual(c.uniqued(on: { $0.bitWidth }), []) + + let d = Array(repeating: "Andromeda", count: 10) + XCTAssertEqualSequences(d.uniqued(on: { $0.first }), ["Andromeda"]) + } + + func testLazyUniqueOn() { + let a = ["Albemarle", "Abeforth", "Astrology", "Brandywine", "Beatrice", "Axiom"] + let b = a.lazy.uniqued(on: { $0.first }) + XCTAssertEqualSequences(b, ["Albemarle", "Brandywine"]) + XCTAssertLazySequence(b) + + let c: [Int] = [] + XCTAssertEqualSequences(c.lazy.uniqued(on: { $0.bitWidth }), []) + + let d = Array(repeating: "Andromeda", count: 10) + XCTAssertEqualSequences(d.lazy.uniqued(on: { $0.first }), ["Andromeda"]) } }