Skip to content

Add sliding windows algorithm #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Oct 30, 2020
Merged
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
64 changes: 64 additions & 0 deletions Guides/SlidingWindows.md
Original file line number Diff line number Diff line change
@@ -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<Self> {
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.
112 changes: 112 additions & 0 deletions Sources/Algorithms/SlidingWindows.swift
Original file line number Diff line number Diff line change
@@ -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<Self> {
SlidingWindows(base: self, size: count)
}
}

public struct SlidingWindows<Base: Collection> {

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.upperBound]
}

public func index(after index: Index) -> 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 {}
91 changes: 91 additions & 0 deletions Tests/SwiftAlgorithmsTests/SlidingWindowsTests.swift
Original file line number Diff line number Diff line change
@@ -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])
}

}