Skip to content

Commit ad6dc7a

Browse files
authored
Add random-access methods to SlidingWindows (#42)
1 parent fd189aa commit ad6dc7a

File tree

2 files changed

+214
-3
lines changed

2 files changed

+214
-3
lines changed

Sources/Algorithms/SlidingWindows.swift

Lines changed: 195 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,202 @@ extension SlidingWindows: Collection {
8484
)
8585
}
8686

87-
// TODO: Implement distance(from:to:), index(_:offsetBy:) and
88-
// index(_:offsetBy:limitedBy:)
87+
public func index(_ i: Index, offsetBy distance: Int) -> Index {
88+
guard distance != 0 else { return i }
89+
90+
return distance > 0
91+
? offsetForward(i, by: distance)
92+
: offsetBackward(i, by: -distance)
93+
}
94+
95+
public func index(
96+
_ i: Index,
97+
offsetBy distance: Int,
98+
limitedBy limit: Index
99+
) -> Index? {
100+
guard distance != 0 else { return i }
101+
guard limit != i else { return nil }
102+
103+
if distance > 0 {
104+
return limit > i
105+
? offsetForward(i, by: distance, limitedBy: limit)
106+
: offsetForward(i, by: distance)
107+
} else {
108+
return limit < i
109+
? offsetBackward(i, by: -distance, limitedBy: limit)
110+
: offsetBackward(i, by: -distance)
111+
}
112+
}
113+
114+
private func offsetForward(_ i: Index, by distance: Int) -> Index {
115+
guard let index = offsetForward(i, by: distance, limitedBy: endIndex)
116+
else { fatalError("Index is out of bounds") }
117+
return index
118+
}
119+
120+
private func offsetBackward(_ i: Index, by distance: Int) -> Index {
121+
guard let index = offsetBackward(i, by: distance, limitedBy: startIndex)
122+
else { fatalError("Index is out of bounds") }
123+
return index
124+
}
125+
126+
private func offsetForward(
127+
_ i: Index, by distance: Int, limitedBy limit: Index
128+
) -> Index? {
129+
assert(distance > 0)
130+
assert(limit > i)
131+
132+
// `endIndex` and the index before it both have `base.endIndex` as their
133+
// upper bound, so we first advance to the base index _before_ the upper
134+
// bound of the output, in order to avoid advancing past the end of `base`
135+
// when advancing to `endIndex`.
136+
//
137+
// Advancing by 4:
138+
//
139+
// input: [x|x x x x x|x x x x] [x x|x x x x x|x x x]
140+
// |> > >|>| or |> > >|
141+
// output: [x x x x x|x x x x x] [x x x x x x x x x x] (`endIndex`)
142+
143+
if distance >= size {
144+
// Avoid traversing `self[i.lowerBound..<i.upperBound]` when the lower
145+
// bound of the output is greater than or equal to the upper bound of the
146+
// input.
147+
148+
// input: [x|x x x x|x x x x x x x]
149+
// |> >|> > >|>|
150+
// output: [x x x x x x x|x x x x|x]
151+
152+
guard limit.lowerBound >= i.upperBound,
153+
let lowerBound = base.index(
154+
i.upperBound,
155+
offsetBy: distance - size,
156+
limitedBy: limit.lowerBound),
157+
let indexBeforeUpperBound = base.index(
158+
lowerBound,
159+
offsetBy: size - 1,
160+
limitedBy: limit.upperBound)
161+
else { return nil }
162+
163+
// If `indexBeforeUpperBound` equals `base.endIndex`, we're advancing to
164+
// `endIndex`.
165+
guard indexBeforeUpperBound != base.endIndex else { return endIndex }
166+
167+
return Index(
168+
lowerBound: lowerBound,
169+
upperBound: base.index(after: indexBeforeUpperBound))
170+
} else {
171+
// input: [x|x x x x x x|x x x x x]
172+
// |> > > >| |> > >|>|
173+
// output: [x x x x x|x x x x x x|x]
174+
175+
guard let indexBeforeUpperBound = base.index(
176+
i.upperBound,
177+
offsetBy: distance - 1,
178+
limitedBy: limit.upperBound)
179+
else { return nil }
180+
181+
// If `indexBeforeUpperBound` equals the limit, the upper bound itself
182+
// exceeds it.
183+
guard indexBeforeUpperBound != limit.upperBound || limit == endIndex
184+
else { return nil }
185+
186+
// If `indexBeforeUpperBound` equals `base.endIndex`, we're advancing to
187+
// `endIndex`.
188+
guard indexBeforeUpperBound != base.endIndex else { return endIndex }
189+
190+
return Index(
191+
lowerBound: base.index(i.lowerBound, offsetBy: distance),
192+
upperBound: base.index(after: indexBeforeUpperBound))
193+
}
194+
}
195+
196+
private func offsetBackward(
197+
_ i: Index, by distance: Int, limitedBy limit: Index
198+
) -> Index? {
199+
assert(distance > 0)
200+
assert(limit < i)
201+
202+
if i == endIndex {
203+
// Advance `base.endIndex` by `distance - 1`, because the index before
204+
// `endIndex` also has `base.endIndex` as its upper bound.
205+
//
206+
// Advancing by 4:
207+
//
208+
// input: [x x x x x x x x x x] (`endIndex`)
209+
// |< < < < <|< < <|
210+
// output: [x x|x x x x x|x x x]
211+
212+
guard let upperBound = base.index(
213+
base.endIndex,
214+
offsetBy: -(distance - 1),
215+
limitedBy: limit.upperBound)
216+
else { return nil }
217+
218+
return Index(
219+
lowerBound: base.index(upperBound, offsetBy: -size),
220+
upperBound: upperBound)
221+
} else if distance >= size {
222+
// Avoid traversing `self[i.lowerBound..<i.upperBound]` when the upper
223+
// bound of the output is less than or equal to the lower bound of the
224+
// input.
225+
//
226+
// input: [x x x x x x x|x x x x|x]
227+
// |< < < <|< <|
228+
// output: [x|x x x x|x x x x x x x]
229+
230+
guard limit.upperBound <= i.lowerBound,
231+
let upperBound = base.index(
232+
i.lowerBound,
233+
offsetBy: -(distance - size),
234+
limitedBy: limit.upperBound)
235+
else { return nil }
236+
237+
return Index(
238+
lowerBound: base.index(upperBound, offsetBy: -size),
239+
upperBound: upperBound)
240+
} else {
241+
// input: [x x x x x|x x x x x x|x]
242+
// |< < < <| |< < < <|
243+
// output: [x|x x x x x x|x x x x x]
244+
245+
guard let lowerBound = base.index(
246+
i.lowerBound,
247+
offsetBy: -distance,
248+
limitedBy: limit.lowerBound)
249+
else { return nil }
250+
251+
return Index(
252+
lowerBound: lowerBound,
253+
upperBound: base.index(i.lowerBound, offsetBy: -distance))
254+
}
255+
}
256+
257+
public func distance(from start: Index, to end: Index) -> Int {
258+
guard start <= end else { return -distance(from: end, to: start) }
259+
guard start != end else { return 0 }
260+
guard end < endIndex else {
261+
// We add 1 here because the index before `endIndex` also has
262+
// `base.endIndex` as its upper bound.
263+
return base[start.upperBound...].count + 1
264+
}
89265

266+
if start.upperBound <= end.lowerBound {
267+
// The distance between `start.lowerBound` and `start.upperBound` is
268+
// already known.
269+
//
270+
// start: [x|x x x x|x x x x x x x]
271+
// |- - - -|> >|
272+
// end: [x x x x x x x|x x x x|x]
273+
274+
return size + base[start.upperBound..<end.lowerBound].count
275+
} else {
276+
// start: [x|x x x x x x|x x x x x]
277+
// |> > > >|
278+
// end: [x x x x x|x x x x x x|x]
279+
280+
return base[start.lowerBound..<end.lowerBound].count
281+
}
282+
}
90283
}
91284

92285
extension SlidingWindows: BidirectionalCollection where Base: BidirectionalCollection {

Tests/SwiftAlgorithmsTests/SlidingWindowsTests.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
import XCTest
13-
import Algorithms
13+
@testable import Algorithms
1414

1515
final class SlidingWindowsTests: XCTestCase {
1616

@@ -88,4 +88,22 @@ final class SlidingWindowsTests: XCTestCase {
8888
XCTAssertEqualSequences(a[i], [1, 2])
8989
}
9090

91+
func testWindowsIndexTraversals() {
92+
validateIndexTraversals(
93+
"".slidingWindows(ofCount: 1),
94+
"a".slidingWindows(ofCount: 1),
95+
"ab".slidingWindows(ofCount: 1),
96+
"abc".slidingWindows(ofCount: 1),
97+
"".slidingWindows(ofCount: 3),
98+
"a".slidingWindows(ofCount: 3),
99+
"abc".slidingWindows(ofCount: 3),
100+
"abcdefgh".slidingWindows(ofCount: 3),
101+
indices: { windows in
102+
let endIndex = windows.base.endIndex
103+
let indices = windows.base.indices + [endIndex]
104+
return zip(indices, indices.dropFirst(windows.size))
105+
.map { .init(lowerBound: $0, upperBound: $1) }
106+
+ [.init(lowerBound: endIndex, upperBound: endIndex)]
107+
})
108+
}
91109
}

0 commit comments

Comments
 (0)