Skip to content

Commit b28d248

Browse files
Add sliding windows algorithm (#20)
1 parent f11d398 commit b28d248

File tree

3 files changed

+267
-0
lines changed

3 files changed

+267
-0
lines changed

Guides/SlidingWindows.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# SlidingWindows
2+
3+
[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/SlidingWindows.swift) |
4+
[Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/SlidingWindowsTests.swift)]
5+
6+
Break a collection into overlapping contiguous window subsequences where
7+
elements are slices from the original collection.
8+
9+
The `slidingWindows(ofCount:)` method takes in a integer size and returns a collection
10+
of subsequences.
11+
12+
```swift
13+
let swift = "swift"
14+
15+
let windowed = swift.slidingWindows(ofCount: 2)
16+
// windowed == [ "sw", "wi", "if", "ft" ]
17+
```
18+
19+
## Detailed Design
20+
21+
The `slidingWindows(ofCount:)` is added as a method on an extension of `Collection`
22+
23+
```swift
24+
extension Collection {
25+
public func slidingWindows(ofCount count: Int) -> SlidingWindows<Self> {
26+
SlidingWindows(base: self, size: count)
27+
}
28+
}
29+
```
30+
31+
If a size larger than the collection length is specified, an empty collection is returned.
32+
The first upper bound is computed eagerly because it determines if the collection
33+
`startIndex` returns `endIndex`.
34+
35+
```swift
36+
[1, 2, 3].slidingWindows(ofCount: 5).isEmpty // true
37+
```
38+
39+
The resulting `SlidingWindows` type is a collection, with conditional conformance to the
40+
`BidirectionalCollection`, and `RandomAccessCollection` when the base collection
41+
conforms.
42+
43+
### Complexity
44+
45+
The call to `slidingWindows(ofCount: k)` is O(_1_) if the collection conforms to
46+
`RandomAccessCollection`, otherwise O(_k_). Access to the next window is O(_1_).
47+
48+
### Naming
49+
50+
The type `SlidingWindows` takes its name from the algorithm, similarly the method takes
51+
it's name from it too `slidingWindows(ofCount: k)`.
52+
53+
The label on the method `ofCount` was chosen to create a consistent feel to the API
54+
available in swift-algorithms repository. Inspiration was taken from
55+
`combinations(ofCount:)` and `permutations(ofCount:)`.
56+
57+
Previously the name `windows` was considered but was deemed to potentially create
58+
ambiguity with the Windows operating system.
59+
60+
### Comparison with other languages
61+
62+
[rust](https://doc.rust-lang.org/std/slice/struct.Windows.html) has
63+
`std::slice::Windows` which is a method available on slices. It has the same
64+
semantics as described here.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
// slidingWindows(ofCount:)
14+
//===----------------------------------------------------------------------===//
15+
16+
extension Collection {
17+
/// A collection for all contiguous windows of length size, the
18+
/// windows overlap.
19+
///
20+
/// - Complexity: O(*1*) if the collection conforms to
21+
/// `RandomAccessCollection`, otherwise O(*k*) where `k` is `count`.
22+
/// Access to the next window is O(*1*).
23+
///
24+
/// - Parameter count: The number of elements in each window subsequence.
25+
///
26+
/// - Returns: If the collection is shorter than `size` the resulting
27+
/// SlidingWindows collection will be empty.
28+
public func slidingWindows(ofCount count: Int) -> SlidingWindows<Self> {
29+
SlidingWindows(base: self, size: count)
30+
}
31+
}
32+
33+
public struct SlidingWindows<Base: Collection> {
34+
35+
public let base: Base
36+
public let size: Int
37+
38+
private var firstUpperBound: Base.Index?
39+
40+
init(base: Base, size: Int) {
41+
precondition(size > 0, "SlidingWindows size must be greater than zero")
42+
self.base = base
43+
self.size = size
44+
self.firstUpperBound = base.index(base.startIndex, offsetBy: size, limitedBy: base.endIndex)
45+
}
46+
}
47+
48+
extension SlidingWindows: Collection {
49+
50+
public struct Index: Comparable {
51+
internal var lowerBound: Base.Index
52+
internal var upperBound: Base.Index
53+
public static func == (lhs: Index, rhs: Index) -> Bool {
54+
lhs.lowerBound == rhs.lowerBound
55+
}
56+
public static func < (lhs: Index, rhs: Index) -> Bool {
57+
lhs.lowerBound < rhs.lowerBound
58+
}
59+
}
60+
61+
public var startIndex: Index {
62+
if let upperBound = firstUpperBound {
63+
return Index(lowerBound: base.startIndex, upperBound: upperBound)
64+
} else {
65+
return endIndex
66+
}
67+
}
68+
69+
public var endIndex: Index {
70+
Index(lowerBound: base.endIndex, upperBound: base.endIndex)
71+
}
72+
73+
public subscript(index: Index) -> Base.SubSequence {
74+
precondition(index.lowerBound != index.upperBound, "SlidingWindows index is out of range")
75+
return base[index.lowerBound..<index.upperBound]
76+
}
77+
78+
public func index(after index: Index) -> Index {
79+
precondition(index < endIndex, "Advancing past end index")
80+
guard index.upperBound < base.endIndex else { return endIndex }
81+
return Index(
82+
lowerBound: base.index(after: index.lowerBound),
83+
upperBound: base.index(after: index.upperBound)
84+
)
85+
}
86+
87+
// TODO: Implement distance(from:to:), index(_:offsetBy:) and
88+
// index(_:offsetBy:limitedBy:)
89+
90+
}
91+
92+
extension SlidingWindows: BidirectionalCollection where Base: BidirectionalCollection {
93+
public func index(before index: Index) -> Index {
94+
precondition(index > startIndex, "Incrementing past start index")
95+
if index == endIndex {
96+
return Index(
97+
lowerBound: base.index(index.lowerBound, offsetBy: -size),
98+
upperBound: index.upperBound
99+
)
100+
} else {
101+
return Index(
102+
lowerBound: base.index(before: index.lowerBound),
103+
upperBound: base.index(before: index.upperBound)
104+
)
105+
}
106+
}
107+
}
108+
109+
extension SlidingWindows: RandomAccessCollection where Base: RandomAccessCollection {}
110+
extension SlidingWindows: Equatable where Base: Equatable {}
111+
extension SlidingWindows: Hashable where Base: Hashable, Base.Index: Hashable {}
112+
extension SlidingWindows.Index: Hashable where Base.Index: Hashable {}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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 SlidingWindowsTests: XCTestCase {
16+
17+
func testWindowsOfString() {
18+
19+
let s = "swift"
20+
let w = s.slidingWindows(ofCount: 2)
21+
var i = w.startIndex
22+
23+
XCTAssertEqualSequences(w[i], "sw")
24+
w.formIndex(after: &i)
25+
XCTAssertEqualSequences(w[i], "wi")
26+
w.formIndex(after: &i)
27+
XCTAssertEqualSequences(w[i], "if")
28+
w.formIndex(after: &i)
29+
XCTAssertEqualSequences(w[i], "ft")
30+
31+
// w.index(after: w.endIndex) // ← Precondition failed: SlidingWindows index is out of range
32+
// w.index(before: w.startIndex) // ← Precondition failed: SlidingWindows index is out of range
33+
// w.formIndex(after: &i); w[i] // ← Precondition failed: SlidingWindows index is out of range
34+
}
35+
36+
func testWindowsOfRange() {
37+
let a = 0...100
38+
39+
XCTAssertTrue(a.slidingWindows(ofCount: 200).isEmpty)
40+
41+
let w = a.slidingWindows(ofCount: 10)
42+
43+
XCTAssertEqualSequences(w.first!, 0..<10)
44+
XCTAssertEqualSequences(w.last!, 91..<101)
45+
}
46+
47+
func testWindowsOfInt() {
48+
49+
let a = [ 0, 1, 0, 1 ].slidingWindows(ofCount: 2)
50+
51+
XCTAssertEqual(a.count, 3)
52+
XCTAssertEqual(a.map { $0.reduce(0, +) }, [1, 1, 1])
53+
54+
let a2 = [0, 1, 2, 3, 4, 5, 6].slidingWindows(ofCount: 3).map {
55+
$0.reduce(0, +)
56+
}.reduce(0, +)
57+
58+
XCTAssertEqual(a2, 3 + 6 + 9 + 12 + 15)
59+
}
60+
61+
func testWindowsCount() {
62+
let a = [0, 1, 2, 3, 4, 5]
63+
XCTAssertEqual(a.slidingWindows(ofCount: 3).count, 4)
64+
65+
let a2 = [0, 1, 2, 3, 4]
66+
XCTAssertEqual(a2.slidingWindows(ofCount: 6).count, 0)
67+
68+
let a3 = [Int]()
69+
XCTAssertEqual(a3.slidingWindows(ofCount: 2).count, 0)
70+
}
71+
72+
func testWindowsSecondAndLast() {
73+
let a = [0, 1, 2, 3, 4, 5]
74+
let w = a.slidingWindows(ofCount: 4)
75+
let snd = w[w.index(after: w.startIndex)]
76+
XCTAssertEqualSequences(snd, [1, 2, 3, 4])
77+
78+
let w2 = a.slidingWindows(ofCount: 3)
79+
XCTAssertEqualSequences(w2.last!, [3, 4, 5])
80+
}
81+
82+
func testWindowsIndexAfterAndBefore() {
83+
let a = [0, 1, 2, 3, 4, 5].slidingWindows(ofCount: 2)
84+
var i = a.startIndex
85+
a.formIndex(after: &i)
86+
a.formIndex(after: &i)
87+
a.formIndex(before: &i)
88+
XCTAssertEqualSequences(a[i], [1, 2])
89+
}
90+
91+
}

0 commit comments

Comments
 (0)