When an application analyzes a series of measurements, events, or characters, one value is often not enough. To spot a short rise in temperature, for example, we need to compare several neighboring readings. To understand a state transition in a log, the order of adjacent entries matters. These tasks involve examining small, overlapping groups of elements as we move through a collection.
The straightforward approach is to create a new array for each position. But copying is unnecessary when we only want to read neighboring values. Swift Algorithms provides windows(ofCount:), which represents each group as a slice of an existing collection instead of an array copied in advance.
In this article, we’ll look at how the library uses an index with two bounds, why the final window needs special handling, and when slices help avoid extra copies. The examples will also show where copying still happens and when a simpler approach is enough.
A Window Is Not a New Group of Data
import Algorithms
let readings = [12, 18, 15, 21, 19]
let windows = readings.windows(ofCount: 3)
for window in windows { print(Array(window)) }
print("window count:", windows.count)
print("too wide:", readings.windows(ofCount: 6).isEmpty)
[12, 18, 15]
[18, 15, 21]
[15, 21, 19]
window count: 3
too wide: true
The first three starting positions produce complete windows of three elements. A fourth would be too short. That is why the count is 3, while asking for six elements produces an empty collection and prints true.
The result is a WindowsOfCountCollection<[Int]>, not a [[Int]]. Each element has the base collection’s SubSequence type, which here is ArraySlice<Int>. The example converts slices to arrays only to print them. The library does not create a separate array for every window: we can process a slice first and decide later whether a copy is needed.
One Index, Two Bounds
The implementation in Windows.swift stores the original base collection, windowSize, and an optional endOfFirstWindow. The first upper bound comes from base.index(base.startIndex, offsetBy: windowSize, limitedBy: base.endIndex). If the base collection is too short, that operation returns nil and the windows collection starts at its end index.
Each position in WindowsOfCountCollection carries two values of type Base.Index: lowerBound and upperBound. The subscript returns base[index.lowerBound..<index.upperBound]. Moving to the next position advances the bounds rather than rebuilding an array.
We can observe the movement using the public collection interface:
import Algorithms
let values = [2, 4, 6, 8]
let windows = values.windows(ofCount: 2)
var index = windows.startIndex
while index != windows.endIndex {
print(Array(windows[index]))
index = windows.index(after: index)
}
[2, 4]
[4, 6]
[6, 8]
For this four-element array, the internal bounds correspond to 0..<2, 1..<3, and 2..<4. The pair 4..<4 represents the end, not a fourth window. These bounds are implementation details; the public Index does not expose them. Its comparison operators compare the lower bounds, because the upper bound of a valid window follows from its size.
The public method documents O(1) initial work for a RandomAccessCollection and O(k) for other collections, where k is the requested window size. Access to successive windows is documented as O(1). These are index-navigation costs, not a promise that examining all the elements in each window is free.
The Last Window and the End Index
The upper bound of the final valid window can already equal base.endIndex. That is why the wrapper cannot treat this upper bound alone as a signal to stop. Its own endIndex uses both bounds set to base.endIndex. The distinction lets iteration return the last slice before reaching the end marker.
The public interface exposes this behavior even though the bounds are internal:
import Algorithms
let pair = [4, 5]
let exact = pair.windows(ofCount: 2)
print("exact:", exact.map { Array($0) })
print("oversized:", pair.windows(ofCount: 3).isEmpty)
print("single:", pair.windows(ofCount: 1).map { Array($0) })
print("back from end:", Array(exact[exact.index(before: exact.endIndex)]))
print("after last is end:", exact.index(after: exact.startIndex) == exact.endIndex)
exact: [[4, 5]]
oversized: true
single: [[4], [5]]
back from end: [4, 5]
after last is end: true
A window as large as the base produces one slice. A larger request produces none; size one produces two slices. The last two lines show that stepping backward from the end recovers the final window, while stepping forward from that window reaches the end marker.
There is also an invalid case we deliberately do not execute: a size of zero. The initializer has precondition(windowSize > 0, ...), so that call would trap rather than return an empty collection. In the source, index(before:) has a special branch for the end marker, and WindowsTests.swift covers forward and backward traversal, empty inputs, oversized windows, and String collections.
The Cost of Working with Slices
The method extends Collection, not an arbitrary Sequence or AsyncSequence. A live stream therefore needs its own bounded buffer before this API can work on its elements. The returned slices also keep the base collection’s index rules. For arrays, a slice of the middle of an array does not start at index zero.
import Algorithms
let numbers = [10, 20, 30, 40]
let windows = numbers.windows(ofCount: 2)
let second = windows[windows.index(after: windows.startIndex)]
print("slice:", Array(second))
print("slice first index:", second.startIndex)
print("slice first value:", second[second.startIndex])
let copy = Array(second)
print("copy first index:", copy.startIndex)
slice: [20, 30]
slice first index: 1
slice first value: 20
copy first index: 0
The second window contains 20 and 30, but its ArraySlice begins at index 1, inherited from the original array. Accessing second[0] would be invalid. The example uses second.startIndex instead. Converting the slice to an Array creates a collection whose first index is 0. The conversions used for printing also create arrays; this snippet demonstrates index behavior, not an allocation benchmark.
Avoiding an immediate copy does not mean zero memory cost. As Apple’s ArraySlice documentation notes, retaining a small ArraySlice can keep the storage of a much larger original array alive. When a window must outlive the original data, making an independent array may be the better choice.
The wrapper supports backward iteration and random access when its base collection does. Windows over String contain Swift Character values, not UTF-8 bytes. For simple neighboring pairs, the standard zip may be sufficient; for groups that must not overlap, chunking is a better fit. Use windows when overlapping slices are the point.
Version and Project Status
Swift Algorithms uses the Apache 2.0 license with the Swift runtime library exception noted in source headers. Release 1.2.1 was published on February 10, 2025. Its Package.swift declares Swift tools 5.7, the Algorithms product, and a dependency on swift-numerics through RealModule. The window implementation itself does not use RealModule.
The manifest does not declare minimum Apple platform versions, so Swift tools 5.7 alone does not guarantee support for a particular iOS version. The repository has commits from 2026, but that does not by itself mean a release newer than 1.2.1 exists. Read Windows.swift alongside WindowsTests.swift: the paired bounds, end index, and iteration tests explain the design better than a feature list.
Practical Use Cases
These examples work with collections already in memory, not streaming data.
Detecting Rising Measurements
A monitoring dashboard needs to find three consecutive temperature readings that are strictly increasing:
import Algorithms
let temperatures = [20, 21, 23, 22, 24]
let risingTriples = temperatures.windows(ofCount: 3)
.filter { window in
let first = window.startIndex
let second = window.index(after: first)
let third = window.index(after: second)
return window[first] < window[second] && window[second] < window[third]
}
.map { Array($0) }
print("rising:", risingTriples)
rising: [[20, 21, 23]]
Only 20, 21, 23 passes both comparisons. The next candidate, 21, 23, 22, fails the second comparison, and 23, 22, 24 fails the first. Using each slice’s own indices avoids copying every candidate just to compare its values. The final map makes an array only for the matching window. A continuous sensor stream would still need a separate moving buffer.
Character Pairs in a Text Tool
A search component can inspect adjacent characters without first creating an array of characters:
import Algorithms
let text = "café"
let pairs = text.windows(ofCount: 2).map { String($0) }
print("pairs:", pairs)
pairs: ["ca", "af", "fé"]
The result contains "ca", "af", and "fé". These are pairs of Swift Character values, not two-byte fragments. String($0) creates independent strings. Unicode normalization and case handling remain the responsibility of the search tool.
State Transitions in a Job Log
A developer tool can format neighboring states from a log already loaded into memory:
import Algorithms
let states = ["queued", "running", "failed", "retrying"]
let transitions = states.windows(ofCount: 2)
.map { $0.joined(separator: " -> ") }
print("transitions:", transitions)
transitions: ["queued -> running", "running -> failed", "failed -> retrying"]
The middle transition, "running -> failed", is now visible alongside "queued -> running" and "failed -> retrying". Here joined(separator:) reads each slice directly and creates a result string without an intermediate array of its elements. The library groups values but does not validate whether transitions are allowed; that remains the application’s responsibility. If only adjacent pairs are needed, zip may be simpler and avoids adding a dependency.
The Takeaway
windows(ofCount:) does not need a new array for each overlapping group. The wrapper stores the base collection, the window size, and the end of the first window; each position carries the bounds of its own slice. The separate end marker makes forward and backward navigation work even when the last valid slice already reaches the end of the base collection.
The application still decides what to calculate and whether to copy. That choice matters: slices avoid eager duplication, but their indices and storage lifetime follow the original collection. Sometimes a useful algorithm is not a new container, but a careful view of data that already exists.
