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 3 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
56 changes: 56 additions & 0 deletions Guides/Windows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Windows

[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Windows.swift) |
[Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/WindowsTests.swift)]

Break a collection into overlapping contiguous window subsequences where
elements are slices from the original collection.

The `windows(size:)` method takes in a integer size and returns a collection of subsequences.

```swift
let swift = "swift"

let windowed = swift.windows(size: 2)
// windowed == [ "sw", "wi", "if", "ft" ]
```

## Detailed Design

The `windows(size:)` is added as a method on an extension of `Collection`

```swift
extension Collection {
public func windows(size: Int) -> Windows<Self> {
Windows(base: self, size: size)
}
}
```

If a size larger than the collection length is specified, an empty collection is returned. Due to this
behaviour the indexes must be calculated on initialisation as we have to be able to compare the
`upperBound` and allow `Collection` correctly calculate `isEmpty` .

```swift
[1, 2, 3].windows(size: 5).isEmpty // true
```

The resulting `Windows` type is a collection, with conditional conformance to the
`BidirectionalCollection`, and `RandomAccessCollection` when the base collection
conforms.

### Complexity

The algorithm is O(_N_) time and O(_1_) space complexity.
Calling `[1, 2, 3].windows(size: k)` would result in O(_k_).

### Naming

The name `window` is adopted from the the commonly known sliding windows problem or algorithm name.
Alternatively this could be named `slidingWindows`, however I did not feel the verbosity here was
necessary.

### 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.
90 changes: 90 additions & 0 deletions Sources/Algorithms/Windows.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

//===----------------------------------------------------------------------===//
// windows(size:)
//===----------------------------------------------------------------------===//

extension Collection {
/// Returns a collection for all contiguous windows of length size. The windows overlap.
/// If the slice is shorter than `size`, the collection returns an empty subsequence.
///
/// - Complexity: O(*n*). When iterating over the resulting collection,
/// accessing each successive window has a complexity of O(*m*), where *m*
/// is the length of the window.
public func windows(size: Int) -> Windows<Self> {
Windows(base: self, size: size)
}
}

public struct Windows<Base: 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 && lhs.upperBound == rhs.upperBound
}
public static func < (lhs: Index, rhs: Index) -> Bool {
lhs.upperBound < rhs.upperBound
}
}

public let base: Base
public let size: Int

private var firstUpperBound: Base.Index?

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 init(base: Base, size: Int) {
precondition(size > 0, "Windows size must be greater than zero")
self.base = base
self.size = size
self.firstUpperBound = base.index(base.startIndex, offsetBy: size, limitedBy: base.endIndex)
}
}

extension Windows: Collection {
public subscript(index: Index) -> Base.SubSequence {
base[index.lowerBound..<index.upperBound]
}

public func index(after index: Index) -> Index {
guard index.upperBound < base.endIndex else { return endIndex }
return Index(lowerBound: base.index(after: index.lowerBound), upperBound: base.index(after: index.upperBound))
}
}

extension Windows: BidirectionalCollection where Base: BidirectionalCollection {
public func index(before index: Index) -> 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 Windows: RandomAccessCollection where Base: RandomAccessCollection {}
extension Windows: Equatable where Base: Equatable {}
extension Windows: Hashable where Base: Hashable, Base.Index: Hashable {}
extension Windows.Index: Hashable where Base.Index: Hashable {}
84 changes: 84 additions & 0 deletions Tests/SwiftAlgorithmsTests/WindowsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//===----------------------------------------------------------------------===//
//
// 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 WindowsTests: XCTestCase {

func testWindows() {
do {
let a = (0...100).map{ $0 }

XCTAssertTrue(a.windows(size: 200).isEmpty)

let w = a.windows(size: 10)

XCTAssertEqualSequences(w.first ?? [], 0..<10)
XCTAssertEqualSequences(w.last ?? [], 91..<101)
}

do {
let s = "swift"
var itr = s.windows(size: 2).makeIterator()

XCTAssertEqual(itr.next(), "sw")
XCTAssertEqual(itr.next(), "wi")
XCTAssertEqual(itr.next(), "if")
XCTAssertEqual(itr.next(), "ft")
XCTAssertNil(itr.next())
}

do {
let a = [ 0, 1, 0, 1 ].windows(size: 2)

XCTAssertEqual(a.count, 3)
XCTAssertEqual(a.map { $0.reduce(0, +) }, [1, 1, 1])

let a2 = [0, 1, 2, 3, 4, 5, 6].windows(size: 3).map {
$0.reduce(0, +)
}.reduce(0, +)

XCTAssertEqual(a2, 3 + 6 + 9 + 12 + 15)
}

do {
let a = [0, 1, 2, 3, 4, 5]
XCTAssertEqual(a.windows(size: 3).count, 4)

let a2 = [0, 1, 2, 3, 4]
XCTAssertEqual(a2.windows(size: 6).count, 0)

let a3 = [Int]()
XCTAssertEqual(a3.windows(size: 2).count, 0)
}

do {
let a = [0, 1, 2, 3, 4, 5]
let w = a.windows(size: 4)
let snd = w[w.index(after: w.startIndex)]
XCTAssertEqualSequences(snd, [1, 2, 3, 4])

let w2 = a.windows(size: 3)
XCTAssertEqualSequences(w2.last ?? [], [3, 4, 5])
}

do {
let a = [0, 1, 2, 3, 4, 5].windows(size: 2)
var i = a.startIndex
a.formIndex(after: &i)
a.formIndex(after: &i)
a.formIndex(before: &i)
XCTAssertEqualSequences(a[i], [1, 2])
}

}
}