Expressing a range of values is an extremely common concept when programming, which is why I love that Swift includes a robust collection of types to represent ranges as part of its standard library. No more ad-hoc utility types for ranges with slightly different semantics scattered around your codebase, or having to choose between a number of third-party implementations (cough Java).
Swift ranges are actually pretty powerful, so here are five tips that'll let you work with them more effectively.
Range
vs ClosedRange
There are actually two types of complete range in the standard library: Range
, and ClosedRange
.
They're very similar, but the semantics of the ranges that they represent differ; whereas the range represented by a Range
does not include its upperBound
, that represented by a ClosedRange
does.
So when should you use either? One common use of Range
is iterating over a number of integer indexes, which may be 0-indexed. In this case, the index for the count of the elements itself is not valid, so we don't want to include it.
// `..<` gives us a `Range`.
for i in 0..<userCount {
// Fetch the user at `i`.
}
On the contrary, I typically find ClosedRange
useful when working with Date
s (or any form of timestamp). If you want to express an inclusive time range, then ClosedRange
is the way to go.
// `...` gives us a `ClosedRange`.
for day in today...nextSunday {
// Process the day. `nextSunday` will be included!
}
Even if you're just using the lowerBound
and upperBound
properties internally (in which case both ranges perform identically), ensuring that you expose the correct range type as part of your API makes the range semantics of your operation clear.
Did I imply that there are only two types of ranges? I lied - there are actually 5.
What are those funny partial ranges? They're exactly what you'd imagine - open ranges where you specify one bound, but not the other.
PartialRangeFrom
is an open range where you just specify the lower bound. All possible values greater than or equal to that are semantically included in the range. As such, it has no upperBound
property.
// `...` gives us a `PartialRangeFrom`.
let todayOrLater = today...
PartialRangeUpTo
is effectively a Range
where you only specify the upper bound (so there's no lowerBound
). All possible values less than that bound are semantically included in the range.
// `..<` gives us a `PartialRangeUpTo`.
let beforeToday = ..<today
PartialRangeThrough
is its companion (similar to the Range
/ClosedRange
split). Again, you only specify the upper bound, but all possible values less that or equal to that bound are semantically included in that range.
// `...` gives us a `PartialRangeThrough`.
let beforeEndOfWeek = ...sunday
(I promise there are no more range types to cover).
So! In summary,
Range
: lowerBound..<upperBound
(lowerBound <= x < upperBound)ClosedRange
: lowerBound...upperBound
(lowerBound <= x <= upperBound)PartialRangeFrom
: lowerBound...
(lowerBound <= x)PartialRangeUpTo
: ..<upperBound
(x < upperBound)PartialRangeThrough
: ...upperBound
(x <= upperBound)All of the ranges covered so far have a .contains(_:)
method to verify whether an element is included.
let myRange = 1..<5
myRange.contains(2) // true
myRange.contains(5) // false
However, there's also a handy operator that you can use for the same purpose. (Saying that, I always get the order of the range and the value to check wrong).
let myRange = 1..<5
myRange ~= 2 // true
myRange ~= 5 // false
Collection
sBoth Range
and ClosedRange
are fully-fledged Collection
s themselves, whenever the bound is stridable using integer increments, anyway (a fancy way of saying, the jump between elements is well defined). This means you can do any of the normal Collection
-type interactions with them, such as dropping elements, or taking a count.
let range = 1..<5
range.dropLast(2) // 1..<3
range.count // 4
Furthermore, you can easily use ranges to initialize another collection of their included elements. The common case is building an array of incrementing integers.
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
let values = Array(1..<10)
PartialRangeFrom
is an infinite Sequence
, starting at the lower bound. This means you can iterate over one, however clearly you need to bail out at some point.
for x in 3... {
// Need to stop at some point!
break
}
PartialRangeUpTo
and PartialRangeThrough
have no such Sequence
/Collection
conformance. If you think about it, what's the first element?