Ranges in swift are super simple to create. Yet they come in various forms.

let r1 = 1...3
let r2 = 1..<3
let r3 = ...3
let r4 = 3...
let r5 = ..<3

Now they all have range like characteristics, but have slightly different traits. It’s because they’re actually different types.

Different Range Types

let r1 = 1...3 // ClosedRange<Int>
let r2 = 1..<3 // Range<Int>
let r3 = ...3  // PartialRangeThrough<Int>
let r4 = 3...  // PartialRangeFrom<Int>
let r5 = ..<3  // PartialRangeUpTo<Int>

By having different types, the compiler can enforce certain behavior within each type. Looking at the names without seeing the syntax that generates them can be confusing. Hopefully the code above helps with that.

  • ClosedRange: contains both lower bound and upper bound.
  • Range: Contains the lower bound, but not the upper bound.
  • PartialRangeThrough: A partial interval – up to, and including_, an upper bound.
  • PartialRangeFrom: A partial interval extending upward from a lower bound.
  • PartialRangeUpTo: A partial half-open interval up to, but not including, an upper bound.

Each type exposes a slightly different set of properties.

let r3 = ...3 // PartialRangeThrough<Int>
r3.lowerbound // doesn't compile
r3.upperbound // compiles
let r4 = 3... // PartialRangeFrom<Int>
r4.lowerbound // compiles
r4.upperbound // doesn't compile
let r5 = ..<3 // PartialRangeUpTo<Int>
r5.lowerbound // doesn't compile
r5.upperbound // compiles

tldr if a range doesn’t really have a lowerbound / upperbound, then the syntax doesn’t allow accessing it either.

Range Usage

Iterating over a specific range within a sequence:

let range = 1...3
let nums = [8,9,10,11,12]
for i in nums[range] {
    print(i)
}
// Output: 9,10,11

Iterating over a range itself:

for i in r1 {
    print(i)
}
// Output: 1,2,3

Syntax

Ranges are created using either the ... operator or ..<. Both have a condition that minimum <= maximum

Some of their capabilities are:

  • Intersection with another range
  • Overlaps with another range
  • Bounds (You may not have both upper and lower bounds. It just depends)
  • Contains

None existing operators:

// let c = 1<.. // ERROR
// let y = 1<..11 // ERROR

Note the importance of parenthesis:

I got bit by this a number of times, so I thought I mention this:

let points = [3,5,2]
[..<points.count - 1] // incorrect. [range - 1] has no meaning
[..<(points.count - 1)] // correct
let (x,y) = (2,10)
x...y.forEach // incorrect. 
(x...y).forEach // correct

Are the following the same?

let ClosedRange = Int.min...3
let partialRange = ...3

Best way is to try them out

let numbers = [10, 20, 30, 40, 50, 60, 70]

print(numbers[closedRange]) // 💣💣💣 from index: -9223372036854775808 (Int.min) all the way to index: 3
let numbers = [10, 20, 30, 40, 50, 60, 70]

print(numbers[partialRange]) // from the first index in the sequence, all the way to index: 3 
// Prints "[40, 50, 60, 70]"

print(numbers[3...]) doesn’t translate to from index:3 to index: Int.max. It translates to “I want everything after this point. In this everything after (and including) 3”

Basically the start and end are implicit here. It’s based on the collection they are applied on. Not the range of the index type.

What do docs say in regards to ranges that don’t have limits?

It is safe to use operations that put an upper limit on the number of elements they access.

To be more accurate, the docs say (in full):

Because a PartialRangeFrom sequence counts upward indefinitely, do not use one with methods that read the entire sequence before returning, such as map(_:), filter(_:), or suffix(_:). It is safe to use operations that put an upper limit on the number of elements they access, such as prefix(_:) or dropFirst(_:), and operations that you can guarantee will terminate, such as passing a closure you know will eventually return true to first(where:).

Meaning the following have to process the entire range, before the application of map or filter finishes.

(1...).map { $0 * 2 } // 💣💥
(1...).filter { $0 % 2 == 0 } // 💣💥

However the following is ok, because it’s immediately bounded by the array’s bounds.

let arr = [1,2,3][1...].map { $0 * 2 }
print(arr)

let arr2 = [1,2,3][1...].filter { $0 % 2 == 0 } 
print(arr2)

Conversely in the case of dropFirst it only has to adjust a finite number of items in the range.

(1...).dropFirst(5) // good

From docs again

The behavior of incrementing indefinitely is determined by the type of Bound. For example, iterating over an instance of PartialRangeFrom traps when the sequence’s next value would be above Int.max.

Where does the knowledge of ranges become useful?

I found it to be extremely useful during Advent of Code code challenges. I think it’s also very useful for Leetcoding and Interviewing. Like instead of trying to manually skip certain bounds within an array, I just modify the range I need to access within an array.

Ranges help you filter the elements before you get into the for loop - Myself

for (i,v) in [1,2,3,4,5] {
    if i => 3 {
        return 
    }
}

vs

for v in [1,2,3,4,5][..<4] {
    print(v)
}

Can I create ranges for stuff other than numbers?

Yes!

Use the closed range operator (…) to create a closed range of any type that conforms to the Comparable protocol. This example creates a ClosedRange from “a” up to, and including, “z”.

Like I’ve seen folks do

let alphabets = "abcdefghijklmnopqrstuvwxyz"

However you can simply do:

let alphabets = "a"..."z"

Surprisingly I wasn’t able to do:

let r2 = "a"..."m"
print(r2.count) // Referencing property 'count' on 'ClosedRange' requires that 'String' conform to 'Strideable'

Shout out to Josh Caswell for helping me figure it out:

The reason for this error is that there’s a conditional conformance for ClosedRange to the Collection, BidirectionalCollection, RandomAccessCollection protocols. See extension ClosedRange : Sequence where Bound : Strideable

With Characters in Swift you don’t get this conformance out of the box because Swift Characters are a cluster. To learn more about why Swift Characters are a cluster, see my other post on Swift Strings for iOS interviewing

Any last words?

  • Often usage of ranges can be difficult. Because you might need to convert a Range into ClosedRange or vice versa and it’s not very straightforward.
  • You might have to handle bounds. Example 0...array.count - 1 when the array is empty will result in a range that creates 0...-1 which results in a crash/error. Or you might have a left and right range where you range shrinks every time, this could lead to a range of 0, 1, or often negative. So you have to be considerate of all those. As a result you must always have safety checks in your ranges, otherwise your app will crash.
  • There’s another range type that we didn’t discuss. See unboundedrange