diff --git a/Guides/Compacted.md b/Guides/Compacted.md new file mode 100644 index 00000000..2e361839 --- /dev/null +++ b/Guides/Compacted.md @@ -0,0 +1,39 @@ +# Compacted + +[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Compacted.swift) | + [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/CompactedTests.swift)] + +Convenience method that flatten the `nil`s out of a sequence or collection. +That behaves exactly one of the most common uses of `compactMap` which is `collection.lazy.compactMap { $0 }` +which is only remove `nil`s without transforming the elements. + +```swift +let array: [Int?] = [10, nil, 30, nil, 2, 3, nil, 5] +let withNoNils = array.compacted() +// Array(withNoNils) == [10, 30, 2, 3, 5] + +``` + +The most convenient part of `compacted()` is that we avoid the usage of a closure. + +## Detailed Design + +The `compacted()` methods has two overloads: + +```swift +extension Sequence { + public func compacted() -> CompactedSequence { ... } +} + +extension Collection { + public func compacted() -> CompactedCollection { ... } +} +``` + +One is a more general `CompactedSequence` for any `Sequence` base. And the other a more specialized `CompactedCollection` +where base is a `Collection` and with conditional conformance to `BidirectionalCollection`, `RandomAccessCollection`, +`LazyCollectionProtocol`, `Equatable` and `Hashable` when base collection conforms to them. + +### Naming + +The naming method name `compacted()` matches the current method `compactMap` that one of the most common usages `compactMap { $0 }` is abstracted by it. diff --git a/README.md b/README.md index 7cb65872..0261f714 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Read more about the package, and the intent behind it, in the [announcement on s - [`reductions(_:)`, `reductions(_:_:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Reductions.md): Returns all the intermediate states of reducing the elements of a sequence or collection. - [`split(maxSplits:omittingEmptySubsequences:whereSeparator)`, `split(separator:maxSplits:omittingEmptySubsequences)`](https://github.com/apple/swift-algorithms/blob/main/Guides/LazySplit.md): Lazy versions of the Standard Library's eager operations that split sequences and collections into subsequences separated by the specified separator element. - [`windows(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Windows.md): Breaks a collection into overlapping subsequences where elements are slices from the original collection. +- [`compacted()`](https://github.com/apple/swift-algorithms/blob/main/Guides/Compacted.md): Flatten the `nil`s out of a sequence or collection. ## Adding Swift Algorithms as a Dependency diff --git a/Sources/Algorithms/Compacted.swift b/Sources/Algorithms/Compacted.swift new file mode 100644 index 00000000..304287d2 --- /dev/null +++ b/Sources/Algorithms/Compacted.swift @@ -0,0 +1,226 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2021 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 +// +//===----------------------------------------------------------------------===// + +/// A `Sequence` that iterates over every non-nil element from the original `Sequence`. +public struct CompactedSequence: Sequence + where Base.Element == Element? { + + @usableFromInline + let base: Base + + @inlinable + init(base: Base) { + self.base = base + } + + public struct Iterator: IteratorProtocol { + @usableFromInline + var base: Base.Iterator + + @inlinable + init(base: Base.Iterator) { + self.base = base + } + + @inlinable + public mutating func next() -> Element? { + while let wrapped = base.next() { + guard let some = wrapped else { continue } + return some + } + return nil + } + } + + @inlinable + public func makeIterator() -> Iterator { + return Iterator(base: base.makeIterator()) + } +} + +extension Sequence { + /// Returns a new `Sequence` that iterates over every non-nil element + /// from the original `Sequence`. + /// It produces the same result as `c.compactMap { $0 }`. + /// + /// let c = [1, nil, 2, 3, nil] + /// for num in c.compacted() { + /// print(num) + /// } + /// // 1 + /// // 2 + /// // 3 + /// + /// - Returns: A `Sequence` where the element is the unwrapped original + /// element and iterates over every non-nil element from the original + /// `Sequence`. + /// + /// Complexity: O(1) + @inlinable + public func compacted() -> CompactedSequence + where Element == Unwrapped? { + CompactedSequence(base: self) + } +} + +/// A `Collection` that iterates over every non-nil element from the original `Collection`. +public struct CompactedCollection: Collection + where Base.Element == Element? { + + @usableFromInline + let base: Base + + @inlinable + init(base: Base) { + self.base = base + let idx = base.firstIndex(where: { $0 != nil }) ?? base.endIndex + self.startIndex = Index(base: idx) + } + + public struct Index { + @usableFromInline + let base: Base.Index + + @inlinable + init(base: Base.Index) { + self.base = base + } + } + + public var startIndex: Index + + @inlinable + public var endIndex: Index { Index(base: base.endIndex) } + + @inlinable + public subscript(position: Index) -> Element { + base[position.base]! + } + + @inlinable + public func index(after i: Index) -> Index { + precondition(i != endIndex, "Index out of bounds") + + let baseIdx = base.index(after: i.base) + guard let idx = base[baseIdx...].firstIndex(where: { $0 != nil }) + else { return endIndex } + return Index(base: idx) + } +} + +extension CompactedCollection: BidirectionalCollection + where Base: BidirectionalCollection { + + @inlinable + public func index(before i: Index) -> Index { + precondition(i != startIndex, "Index out of bounds") + + guard let idx = + base[startIndex.base.. Bool { + lhs.base < rhs.base + } +} + +extension CompactedCollection.Index: Hashable + where Base.Index: Hashable {} + +extension Collection { + /// Returns a new `Collection` that iterates over every non-nil element + /// from the original `Collection`. + /// It produces the same result as `c.compactMap { $0 }`. + /// + /// let c = [1, nil, 2, 3, nil] + /// for num in c.compacted() { + /// print(num) + /// } + /// // 1 + /// // 2 + /// // 3 + /// + /// - Returns: A `Collection` where the element is the unwrapped original + /// element and iterates over every non-nil element from the original + /// `Collection`. + /// + /// Complexity: O(*n*) where *n* is the number of elements in the + /// original `Collection`. + @inlinable + public func compacted() -> CompactedCollection + where Element == Unwrapped? { + CompactedCollection(base: self) + } +} + +//===----------------------------------------------------------------------===// +// Protocol Conformances +//===----------------------------------------------------------------------===// + +extension CompactedSequence: LazySequenceProtocol + where Base: LazySequenceProtocol {} + +extension CompactedCollection: LazySequenceProtocol + where Base: LazySequenceProtocol {} +extension CompactedCollection: LazyCollectionProtocol + where Base: LazyCollectionProtocol {} + + +// Hashable and Equatable conformance are based on each non-nil +// element on base collection. +extension CompactedSequence: Equatable + where Base.Element: Equatable { + + @inlinable + public static func ==(lhs: CompactedSequence, + rhs: CompactedSequence) -> Bool { + lhs.elementsEqual(rhs) + } +} + +extension CompactedSequence: Hashable + where Element: Hashable { + + @inlinable + public func hash(into hasher: inout Hasher) { + for element in self { + hasher.combine(element) + } + } +} + +extension CompactedCollection: Equatable + where Base.Element: Equatable { + + @inlinable + public static func ==(lhs: CompactedCollection, + rhs: CompactedCollection) -> Bool { + lhs.elementsEqual(rhs) + } +} + +extension CompactedCollection: Hashable + where Element: Hashable { + + @inlinable + public func hash(into hasher: inout Hasher) { + for element in self { + hasher.combine(element) + } + } +} diff --git a/Tests/SwiftAlgorithmsTests/CompactedTests.swift b/Tests/SwiftAlgorithmsTests/CompactedTests.swift new file mode 100644 index 00000000..476d5bba --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/CompactedTests.swift @@ -0,0 +1,81 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2021 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 CompactedTests: XCTestCase { + + let tests: [[Int?]] = + [nil, nil, nil, 0, 1, 2] + .uniquePermutations(ofCount: 0...) + .map(Array.init) + + func testCompactedCompacted() { + for collection in self.tests { + let seq = AnySequence(collection) + XCTAssertEqualSequences( + seq.compactMap({ $0 }), seq.compacted()) + XCTAssertEqualSequences( + collection.compactMap({ $0 }), collection.compacted()) + } + } + + func testCompactedBidirectionalCollection() { + for array in self.tests { + XCTAssertEqualSequences(array.compactMap({ $0 }).reversed(), + array.compacted().reversed()) + } + } + + func testCollectionTraversals() { + for array in self.tests { + validateIndexTraversals(array.compacted()) + } + } + + func testCollectionEquatableConformances() { + for array in self.tests { + XCTAssertEqual( + array.eraseToAnyHashableSequence().compacted(), + array.compactMap({ $0 }).eraseToAnyHashableSequence().compacted() + ) + XCTAssertEqual( + array.compacted(), array.compactMap({ $0 }).compacted() + ) + } + } + + func testCollectionHashableConformances() { + for array1 in self.tests { + for array2 in self.tests { + // For non-equal Collections and Sequences that produce the same + // compacted, the compacted wrapper should produce the same hash. + // e.g. [1, 2, 3, nil, nil, 4].compacted() should produce the + // same hash as [1, nil, 2, nil, 3, 4].compacted() + guard !array1.elementsEqual(array2) && + array1.compacted() == array2.compacted() else { + continue + } + + let seq = array1.eraseToAnyHashableSequence() + let seq2 = array2.eraseToAnyHashableSequence() + + XCTAssertEqualHashValue( + seq.compacted(), seq2.compacted() + ) + XCTAssertEqualHashValue( + array1.compacted(), array2.compacted() + ) + } + } + } +} diff --git a/Tests/SwiftAlgorithmsTests/TestUtilities.swift b/Tests/SwiftAlgorithmsTests/TestUtilities.swift index 6322a997..b4068ddf 100644 --- a/Tests/SwiftAlgorithmsTests/TestUtilities.swift +++ b/Tests/SwiftAlgorithmsTests/TestUtilities.swift @@ -46,6 +46,27 @@ struct SplitMix64: RandomNumberGenerator { } } +// An eraser helper to any hashable sequence. +struct AnyHashableSequence + where Base: Sequence, Base: Hashable { + var base: Base +} + +extension AnyHashableSequence: Hashable {} +extension AnyHashableSequence: Sequence { + typealias Iterator = Base.Iterator + + func makeIterator() -> Iterator { + base.makeIterator() + } +} + +extension Sequence where Self: Hashable { + func eraseToAnyHashableSequence() -> AnyHashableSequence { + AnyHashableSequence(base: self) + } +} + // An eraser helper to any mutable collection struct AnyMutableCollection where Base: MutableCollection { var base: Base @@ -163,6 +184,25 @@ func XCTAssertEqualCollections( } } +func hash(_ value: T) -> Int { + var hasher = Hasher() + value.hash(into: &hasher) + return hasher.finalize() +} + +/// Asserts that two hashable instances produce the same hash value. +func XCTAssertEqualHashValue( + _ expression1: @autoclosure () throws -> T, + _ expression2: @autoclosure () throws -> U, + _ message: @autoclosure () -> String = "", + file: StaticString = #file, line: UInt = #line +) { + XCTAssertEqual( + hash(try expression1()), hash(try expression2()), + message(), file: file, line: line + ) +} + /// Tests that all index traversal methods behave as expected. /// /// Verifies the correctness of the implementations of `startIndex`, `endIndex`,