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
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/restrictions 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 compiler 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
Creating an Array:
let array = Array(1...5)
print(array) // [0, 1, 2, 3, 4, 5]
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]) // 💣💣💣 ERROR: Negative Array index is out of range
It’s because the range starts 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 asmap(_:)
,filter(_:)
, orsuffix(_:)
. It is safe to use operations that put an upper limit on the number of elements they access, such asprefix(_:)
ordropFirst(_:)
, and operations that you can guarantee will terminate, such as passing a closure you know will eventually return true tofirst(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)
}
for v in [1,2,3,4,5][newLowerBound..<newUpperBound] {
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.
Character Range
Like I’ve seen folks do
let alphabets = "abcdefghijk"
However you can simply do:
let alphabets = "a"..."k"
String Range
You could create ranges for Strings. Think of them as two words in a dictionary (like a real book dictionary)
let r = "alpha"..."clpha"
["alphaa","d", "cmllllll", "cllzzzz"].forEach {
print(r.contains($0)) // true, false, false, true
}
For String it’s basically a lexicographically i.e. a dictionary sort.
Custom Type Range
struct Person: Comparable {
let name: String
let salary: Int
static func < (lhs: Person, rhs: Person) -> Bool {
return lhs.salary < rhs.salary
}
}
let range = Person(name: "Mohammad", salary: 10000)...Person(name: "Matt", salary: 20000) // Named After Matt Smollinger who's mentored me through out my career + answered countless questions from me. Currently an EM at Ford.
range.contains(Person(name: "Kotaro", salary: 15000)) // true. Named after Kotaro Fujita who's the co-organizer of PhillyCocoa, Side Project Spot Lot Podcast. Principal Engineer at Comcast and more.
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
when its Bound
is Strideable
. See extension ClosedRange : Sequence where Bound : Strideable
The Swift Engineers have decided not to not have Character
conform to Strideable
. To learn why, see my other post on Why Can’t You Loop Over Ranges of Characters in Swift
Any last words?
- Often usage of ranges can be difficult. Because you might need to convert a
Range
intoClosedRange
or vice versa and it’s not very straightforward. - You might have to handle bounds. Example if an array is is empty then the
0...array.count - 1
range will translate to0...-1
which results in a crash/error. Or you might have a left and right range where your 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. To avoid that you should always have the following checkif range.startIndex < range.endIndex
before processing values at - You can create ranges on anything that’s
Comparable
. However only for ranges that areStrideable
you can do things likecount
orfor loop
. - There’s another range type that we didn’t discuss. See unboundedrange