diff --git a/Guides/SlidingWindows.md b/Guides/SlidingWindows.md new file mode 100644 index 00000000..85263243 --- /dev/null +++ b/Guides/SlidingWindows.md @@ -0,0 +1,64 @@ +# SlidingWindows + +[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/SlidingWindows.swift) | + [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/SlidingWindowsTests.swift)] + +Break a collection into overlapping contiguous window subsequences where +elements are slices from the original collection. + +The `slidingWindows(ofCount:)` method takes in a integer size and returns a collection +of subsequences. + +```swift +let swift = "swift" + +let windowed = swift.slidingWindows(ofCount: 2) +// windowed == [ "sw", "wi", "if", "ft" ] +``` + +## Detailed Design + +The `slidingWindows(ofCount:)` is added as a method on an extension of `Collection` + +```swift +extension Collection { + public func slidingWindows(ofCount count: Int) -> SlidingWindows { + SlidingWindows(base: self, size: count) + } +} +``` + +If a size larger than the collection length is specified, an empty collection is returned. +The first upper bound is computed eagerly because it determines if the collection +`startIndex` returns `endIndex`. + +```swift +[1, 2, 3].slidingWindows(ofCount: 5).isEmpty // true +``` + +The resulting `SlidingWindows` type is a collection, with conditional conformance to the +`BidirectionalCollection`, and `RandomAccessCollection` when the base collection +conforms. + +### Complexity + +The call to `slidingWindows(ofCount: k)` is O(_1_) if the collection conforms to +`RandomAccessCollection`, otherwise O(_k_). Access to the next window is O(_1_). + +### Naming + +The type `SlidingWindows` takes its name from the algorithm, similarly the method takes +it's name from it too `slidingWindows(ofCount: k)`. + +The label on the method `ofCount` was chosen to create a consistent feel to the API +available in swift-algorithms repository. Inspiration was taken from +`combinations(ofCount:)` and `permutations(ofCount:)`. + +Previously the name `windows` was considered but was deemed to potentially create +ambiguity with the Windows operating system. + +### Comparison with other languages + +[rust](https://doc.rust-lang.org/std/slice/struct.Windows.html) has +`std::slice::Windows` which is a method available on slices. It has the same +semantics as described here. diff --git a/Sources/Algorithms/SlidingWindows.swift b/Sources/Algorithms/SlidingWindows.swift new file mode 100644 index 00000000..1efe9604 --- /dev/null +++ b/Sources/Algorithms/SlidingWindows.swift @@ -0,0 +1,112 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2020 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 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// slidingWindows(ofCount:) +//===----------------------------------------------------------------------===// + +extension Collection { + /// A collection for all contiguous windows of length size, the + /// windows overlap. + /// + /// - Complexity: O(*1*) if the collection conforms to + /// `RandomAccessCollection`, otherwise O(*k*) where `k` is `count`. + /// Access to the next window is O(*1*). + /// + /// - Parameter count: The number of elements in each window subsequence. + /// + /// - Returns: If the collection is shorter than `size` the resulting + /// SlidingWindows collection will be empty. + public func slidingWindows(ofCount count: Int) -> SlidingWindows { + SlidingWindows(base: self, size: count) + } +} + +public struct SlidingWindows { + + public let base: Base + public let size: Int + + private var firstUpperBound: Base.Index? + + init(base: Base, size: Int) { + precondition(size > 0, "SlidingWindows size must be greater than zero") + self.base = base + self.size = size + self.firstUpperBound = base.index(base.startIndex, offsetBy: size, limitedBy: base.endIndex) + } +} + +extension SlidingWindows: Collection { + + public struct Index: Comparable { + internal var lowerBound: Base.Index + internal var upperBound: Base.Index + public static func == (lhs: Index, rhs: Index) -> Bool { + lhs.lowerBound == rhs.lowerBound + } + public static func < (lhs: Index, rhs: Index) -> Bool { + lhs.lowerBound < rhs.lowerBound + } + } + + public var startIndex: Index { + if let upperBound = firstUpperBound { + return Index(lowerBound: base.startIndex, upperBound: upperBound) + } else { + return endIndex + } + } + + public var endIndex: Index { + Index(lowerBound: base.endIndex, upperBound: base.endIndex) + } + + public subscript(index: Index) -> Base.SubSequence { + precondition(index.lowerBound != index.upperBound, "SlidingWindows index is out of range") + return base[index.lowerBound.. Index { + precondition(index < endIndex, "Advancing past end index") + guard index.upperBound < base.endIndex else { return endIndex } + return Index( + lowerBound: base.index(after: index.lowerBound), + upperBound: base.index(after: index.upperBound) + ) + } + + // TODO: Implement distance(from:to:), index(_:offsetBy:) and + // index(_:offsetBy:limitedBy:) + +} + +extension SlidingWindows: BidirectionalCollection where Base: BidirectionalCollection { + public func index(before index: Index) -> Index { + precondition(index > startIndex, "Incrementing past start index") + if index == endIndex { + return Index( + lowerBound: base.index(index.lowerBound, offsetBy: -size), + upperBound: index.upperBound + ) + } else { + return Index( + lowerBound: base.index(before: index.lowerBound), + upperBound: base.index(before: index.upperBound) + ) + } + } +} + +extension SlidingWindows: RandomAccessCollection where Base: RandomAccessCollection {} +extension SlidingWindows: Equatable where Base: Equatable {} +extension SlidingWindows: Hashable where Base: Hashable, Base.Index: Hashable {} +extension SlidingWindows.Index: Hashable where Base.Index: Hashable {} diff --git a/Tests/SwiftAlgorithmsTests/SlidingWindowsTests.swift b/Tests/SwiftAlgorithmsTests/SlidingWindowsTests.swift new file mode 100644 index 00000000..ec7f2937 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/SlidingWindowsTests.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2020 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 SlidingWindowsTests: XCTestCase { + + func testWindowsOfString() { + + let s = "swift" + let w = s.slidingWindows(ofCount: 2) + var i = w.startIndex + + XCTAssertEqualSequences(w[i], "sw") + w.formIndex(after: &i) + XCTAssertEqualSequences(w[i], "wi") + w.formIndex(after: &i) + XCTAssertEqualSequences(w[i], "if") + w.formIndex(after: &i) + XCTAssertEqualSequences(w[i], "ft") + +// w.index(after: w.endIndex) // ← Precondition failed: SlidingWindows index is out of range +// w.index(before: w.startIndex) // ← Precondition failed: SlidingWindows index is out of range +// w.formIndex(after: &i); w[i] // ← Precondition failed: SlidingWindows index is out of range + } + + func testWindowsOfRange() { + let a = 0...100 + + XCTAssertTrue(a.slidingWindows(ofCount: 200).isEmpty) + + let w = a.slidingWindows(ofCount: 10) + + XCTAssertEqualSequences(w.first!, 0..<10) + XCTAssertEqualSequences(w.last!, 91..<101) + } + + func testWindowsOfInt() { + + let a = [ 0, 1, 0, 1 ].slidingWindows(ofCount: 2) + + XCTAssertEqual(a.count, 3) + XCTAssertEqual(a.map { $0.reduce(0, +) }, [1, 1, 1]) + + let a2 = [0, 1, 2, 3, 4, 5, 6].slidingWindows(ofCount: 3).map { + $0.reduce(0, +) + }.reduce(0, +) + + XCTAssertEqual(a2, 3 + 6 + 9 + 12 + 15) + } + + func testWindowsCount() { + let a = [0, 1, 2, 3, 4, 5] + XCTAssertEqual(a.slidingWindows(ofCount: 3).count, 4) + + let a2 = [0, 1, 2, 3, 4] + XCTAssertEqual(a2.slidingWindows(ofCount: 6).count, 0) + + let a3 = [Int]() + XCTAssertEqual(a3.slidingWindows(ofCount: 2).count, 0) + } + + func testWindowsSecondAndLast() { + let a = [0, 1, 2, 3, 4, 5] + let w = a.slidingWindows(ofCount: 4) + let snd = w[w.index(after: w.startIndex)] + XCTAssertEqualSequences(snd, [1, 2, 3, 4]) + + let w2 = a.slidingWindows(ofCount: 3) + XCTAssertEqualSequences(w2.last!, [3, 4, 5]) + } + + func testWindowsIndexAfterAndBefore() { + let a = [0, 1, 2, 3, 4, 5].slidingWindows(ofCount: 2) + var i = a.startIndex + a.formIndex(after: &i) + a.formIndex(after: &i) + a.formIndex(before: &i) + XCTAssertEqualSequences(a[i], [1, 2]) + } + +}