Jack Morris
Effective Ranges in Swift
Mar 8, 2020
Swift

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 Dates (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.

Partial Ranges

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,

Checking for Containment

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

Ranges as Collections

Both Range and ClosedRange are fully-fledged Collections 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?

~

Thanks for reading! I'd love to hear your feedback; feel free to contact me directly.