Skip to content

Commit 94d9ae0

Browse files
committed
Add sliding windows algorithm
1 parent 864d460 commit 94d9ae0

File tree

3 files changed

+211
-0
lines changed

3 files changed

+211
-0
lines changed

Guides/Windows.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Windows
2+
3+
[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Windows.swift) |
4+
[Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/WindowsTests.swift)]
5+
6+
Break a collection into overlapping contiguous window subsequences where
7+
elements are slices from the original collection.
8+
9+
The `windows(size:)` method takes in a integer size and returns a collection of subsequences.
10+
11+
```swift
12+
let swift = "swift"
13+
14+
let windowed = swift.windows(size: 2)
15+
// windowed == [ "sw", "wi", "if", "ft" ]
16+
```
17+
18+
## Detailed Design
19+
20+
The `windows(size:)` is added as a method on an extension of `Collection`
21+
22+
```swift
23+
extension Collection {
24+
public func windows(size: Int) -> WindowsSubSequence<Self> {
25+
WindowsSubSequence(base: self, size: size)
26+
}
27+
}
28+
```
29+
30+
If a size larger than the collection length is specified, an empty collection is returned. Due to this
31+
behaviour the indexes must be calculated on initialisation as we have to be able to compare the
32+
`upperBound` in each window slice.
33+
34+
```swift
35+
[1, 2, 3].windows(size: 5).isEmpty // true
36+
```
37+
38+
### Complexity
39+
40+
O(_N_) time and O(_1_) space complexity.
41+
42+
### Naming
43+
44+
The name `window` is adopted from the the commonly known sliding windows problem or algorithm name.
45+
Alternatively this could be named `slidingWindows`, however I did not feel the verbosity here was
46+
necessary.
47+
48+
### Comparison with other languages
49+
50+
[rust](https://doc.rust-lang.org/std/slice/struct.Windows.html) has `std::slice::Windows` which is
51+
a method available on slices. It has the same semantics as described here.

Sources/Algorithms/Windows.swift

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Algorithms open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
//===----------------------------------------------------------------------===//
13+
// windows(size:)
14+
//===----------------------------------------------------------------------===//
15+
16+
extension Collection {
17+
/// Returns a collection for all contiguous windows of length size. The windows overlap.
18+
/// If the slice is shorter than `size`, the collection returns an empty subsequence.
19+
public func windows(size: Int) -> WindowsSubSequence<Self> {
20+
WindowsSubSequence(base: self, size: size)
21+
}
22+
}
23+
24+
public struct WindowsSubSequence<Base: Collection> {
25+
26+
public struct Index: Comparable {
27+
28+
internal var lowerBound: Base.Index
29+
internal var upperBound: Base.Index
30+
31+
public static func == (lhs: Index, rhs: Index) -> Bool {
32+
lhs.lowerBound == rhs.upperBound && lhs.upperBound == rhs.upperBound
33+
}
34+
35+
public static func < (lhs: Index, rhs: Index) -> Bool {
36+
lhs.upperBound < rhs.upperBound
37+
}
38+
}
39+
40+
public let base: Base
41+
public let size: Int
42+
43+
public let startIndex: Index
44+
public let endIndex: Index
45+
46+
public init(base: Base, size: Int) {
47+
precondition(size > 0, "Windows size must be greater than zero")
48+
self.base = base
49+
self.size = size
50+
let limit = base.count - size
51+
if limit > 0, let firstUpperBound = base.index(base.startIndex, offsetBy: size, limitedBy: base.endIndex) {
52+
startIndex = Index(lowerBound: base.startIndex, upperBound: firstUpperBound)
53+
endIndex = Index(lowerBound: base.endIndex, upperBound: base.endIndex)
54+
} else {
55+
startIndex = Index(lowerBound: base.startIndex, upperBound: base.startIndex)
56+
endIndex = startIndex
57+
}
58+
59+
}
60+
}
61+
62+
extension WindowsSubSequence: Collection {
63+
64+
public subscript(index: Index) -> Base.SubSequence {
65+
base[index.lowerBound..<index.upperBound]
66+
}
67+
68+
public func index(after index: Index) -> Index {
69+
guard index.upperBound < base.endIndex else { return endIndex }
70+
return Index(lowerBound: base.index(after: index.lowerBound), upperBound: base.index(after: index.upperBound))
71+
}
72+
}
73+
74+
extension WindowsSubSequence: BidirectionalCollection where Base: BidirectionalCollection {
75+
76+
public func index(before index: Index) -> Index {
77+
guard let lowerBound = base.index(index.lowerBound, offsetBy: -size, limitedBy: base.startIndex) else { return startIndex }
78+
return Index(lowerBound: lowerBound, upperBound: index == endIndex ? index.upperBound : base.index(before: index.upperBound))
79+
}
80+
}
81+
82+
extension WindowsSubSequence: RandomAccessCollection where Base: RandomAccessCollection {}
83+
84+
extension WindowsSubSequence: Equatable where Base: Equatable {}
85+
extension WindowsSubSequence: Hashable where Base: Hashable, Base.Index: Hashable {}
86+
extension WindowsSubSequence.Index: Hashable where Base.Index: Hashable {}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Algorithms open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
import XCTest
13+
import Algorithms
14+
15+
final class WindowsTests: XCTestCase {
16+
17+
func testWindows() {
18+
do {
19+
let a = (0...100).map{ $0 }
20+
21+
XCTAssertTrue(a.windows(size: 200).isEmpty)
22+
23+
let w = a.windows(size: 10)
24+
25+
XCTAssertEqualSequences(w.first ?? [], 0..<10)
26+
XCTAssertEqualSequences(w.last ?? [], 91..<101)
27+
}
28+
29+
do {
30+
let s = "swift"
31+
var itr = s.windows(size: 2).makeIterator()
32+
33+
XCTAssertEqual(itr.next(), "sw")
34+
XCTAssertEqual(itr.next(), "wi")
35+
XCTAssertEqual(itr.next(), "if")
36+
XCTAssertEqual(itr.next(), "ft")
37+
XCTAssertNil(itr.next())
38+
}
39+
40+
do {
41+
let a = [ 0, 1, 0, 1 ].windows(size: 2)
42+
43+
XCTAssertEqual(a.count, 3)
44+
XCTAssertEqual(a.map { $0.reduce(0, +) }, [1, 1, 1])
45+
46+
let a2 = [0, 1, 2, 3, 4, 5, 6].windows(size: 3).map {
47+
$0.reduce(0, +)
48+
}.reduce(0, +)
49+
50+
XCTAssertEqual(a2, 3 + 6 + 9 + 12 + 15)
51+
}
52+
53+
do {
54+
let a = [0, 1, 2, 3, 4, 5]
55+
XCTAssertEqual(a.windows(size: 3).count, 4)
56+
57+
let a2 = [0, 1, 2, 3, 4]
58+
XCTAssertEqual(a2.windows(size: 6).count, 0)
59+
60+
let a3 = [Int]()
61+
XCTAssertEqual(a3.windows(size: 2).count, 0)
62+
}
63+
64+
do {
65+
let a = [0, 1, 2, 3, 4, 5]
66+
let w = a.windows(size: 4)
67+
let snd = w[w.index(after: w.startIndex)]
68+
XCTAssertEqualSequences(snd, [1, 2, 3, 4])
69+
70+
let w2 = a.windows(size: 3)
71+
XCTAssertEqualSequences(w2.last ?? [], [3, 4, 5])
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)