DEV Community

loading...

Swift Ranges

fosteman profile image Timothy Fosteman ・3 min read

Swift Ranges

A range is an interval of values and is defined by its lower and upper bounds.
Two range creators ..< for half-open ranges that don;t include their upper bound, and ... for closed ranges that include both bounds

let singleDigitNumbers = 0..<10
Array(singleDigitNumbers)
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let lowercaseLetters = Character("a")...Character("z")
lowercaseLetters
// {lowerBound "a", upperBound "z"}

There's also prefix and postfix variants of these operators

let fromZero = 0... // PartialRangeFrom<Int>
let upToZ = ..<Character("z") // PartialRangeUpTo<Character>

There are five concrete implementations of ranges each captures constraints on the value. Most common two are Range (a half-open range, ..<) and ClosedRange (created using ...).
Both have a generic Bound parameter: the only requirement is that Bound must conform to Comparable.
Meaning the above lowercaseLetters expression is of type ClosedRange<Character>.

  • Only a half-open range Range can represent an empty interval (when the lower and uppoer bounds are equal)
  • Only a closed range ClosedRange can contain the maximum value of its element type (0...Int.max).

Countable ranges

Ranges can be looped over and treated as a collection, however, Character does not conform to protocol Strideable because of an issue with Unicode, thus character ranges are non-iterable.
In other words, the range must be Countable in order for it to be iterated over. To iterate over the elements use stride(from:to:by) and stride(from:through:by) functions.

Partial ranges

Partial ranges are created with ... or ..< as a prefix or a postfix operator. They are called partial because there is only the starting bound. Thus there are three kinds

let fromA: PartialRangeFrom<Character> = Character("A")...
let throughZ: PartialRangeThrough<Character> = ...Character("Z")
let upTo100: PartialRangeUpTo<Int> = ..<100

When you iterate over a countable PartialRangeFrom, iteration starts with the lower bound and repeatedly calls advanced(by: 1). If you use such a range in a for loop, you must take care to add a break condition. PartialRangeThrough and PartialRangeUpTo aren't countable.

Expressions

All five ranges conform to RangeExpression protocol, which computes a fully-specified range with given constraint. Here's it's simple implementation

public protocol RangeExpression {
    associatedtype Bound: Comparable
    func contains(_ element: Bound) -> Bool
    func relative<C>(to collection: C) -> Range<Bound> where C: Collection, Self.Bound == C.Index
}

For partial expressions with a missing lower bound, the relative(to:) method adds the collection's startIndex as the lower bound. For partial ranges with a missing upper bound, the method will use the collection's endIndex. Partial ranges enable a very compact xyntax for sclicing collections

let array = [1,2,3,4] // [1, 2, 3, 4]
array[2...] // [3, 4]
array[..<1] // [1]
array[1...2] // [2, 3]
array[...] // [1, 2, 3, 4]
type(of: array[...]) // ArraySlice<Int>.Type

Think of ArraySlice as of a SQL table view. This way Swift reduces computational costs - array elements do not get copied.

Notably, ArraySlice conforms to Collection protocol, as Array does, so both share same methods. If you'd need to convert the view into real array

Array(array[...]) // [1, 2, 3, 4]

This works because the corresponding subscript declaration in the Collection protocol takes a RangeExpression rather than one of the concrete range types.
This is implemented as a special case in the standard library as unbounded range.

In case of making your own functions take in range, it's really advisable to copy paste from the standard library. This way clients of my APIs are always happy!

Discussion

pic
Editor guide