ABSTRACT
Ranges and Sequences have seen some considerable changes during Swift’s growth from squalling infant, to surly teenager.
Hopefully, we’ve seen the last of these changes. The current implementation is powerful, yet also rather confusing.
RANGES
Ranges (Half-Open Intervals That are Up To, But Not Including, the Last Value)
Ranges are defined by Apple as “A half-open interval over a comparable type, from a lower bound up to, but not including, an upper bound.”
You specify a range as: minVal..<maxVal
, which is referred to as a “half-open interval”.
Closed Ranges (Ranges that Include the Final Value)
Closed Ranges are defined by Apple as “An interval over a comparable type, from a lower bound up to, and including, an upper bound.”
You specify a ClosedRange as: minVal...maxVal
, which is referred to as a “closed interval”.
Ranges (half-open) can be empty. For example, an Int Range of 0..<0
is empty ((0..<0).isEmpty
is true).
ClosedRanges cannot be empty. For example, an Int Range of 0...0
is not empty ((0...0).isEmpty
is false).
Onward and Upward
You specify Ranges and ClosedRanges going up:
let onwardAndUpward1 = -10..<10 // This is a half-open Range, from -10 to <10, in integer steps (19 steps, 20 values). let onwardAndUpward2 = -10...10 // This is a closed Range, from -10 to 10, in integer steps (20 steps, 21 values).
Ranges and ClosedRanges can’t go down. They must either go nowhere, or up.
These will not work:
//: This will cause a nasty error. Uncomment to see the error. //let downwardSpiral = 1...0 //: Same here (which makes sense). //let downwardSpiral = 1..<0 //: You cannot specify Ranges backwards. This will cause an error. //let downwardSpiral = 1>..0
You can also specify Ranges as Double/Float:
let openFloatRange = 0.0..<1.0 let closedFloatRange = 0.0...1.0
And even as Strings:
let openStringRange = "aardvark"..<"zebra" let closedStringRange = "aardvark"..."zebra"
However, you won’t be able to use these in iterators.
Iterators and Countable Ranges
If you look at the above Ranges in the sample playground, you will see that the Int Ranges are flagged as “CountableRange” or “CountableClosedRange,” while the Double and String Ranges are flagged as “Range” and “ClosedRange.”
“Countable” Ranges are stridaeable, conform to the Sequence Protocol, and can be used to generate iterators, while Range and ClosedRange cannot.
Ranges and ClosedRanges are meant to be used for things like switch statements, Array slicing or if statement cases.
In the above Range/ClosedRange examples, only the Int Range can iterate:
//: This will work, as Int has discrete, iterable steps: for integ in openIntRange { print(integ) } //: These will not work, because Doubles and Strings don't have discrete steps: //for fl in openFloatRange { // print(fl) //} //for animal in openStringRange { // print(animal) //}
Int conforms to the Sequence protocol. Doubles and Strings do not.
The lesson here is that whatever type you base your Range on, it needs to conform to Sequence in order to be used as an iterator.
Switch Case Ranges
One of the coolest (and most dangerous) aspects of Swift, is the ability to have Ranges as cases for switch statements.
Take the following code snippet as an example. We’ll walk through it after you give it some thought:
let someNumber = 3 switch someNumber { case openIntRange: print("We has a match (open)! \(someNumber)") case closedIntRange: print("We has a match (closed)! \(someNumber)") default: print("No Match!") } switch someNumber { case closedIntRange: print("We has a match (closed)! \(someNumber)") case openIntRange: print("We has a match (open)! \(someNumber)") default: print("No Match!") } let someOtherNumber = 10 switch someOtherNumber { case openIntRange: print("We has a match (open)! \(someOtherNumber)") case closedIntRange: print("We has a match (closed)! \(someOtherNumber)") default: print("No Match!") }
What do you think the printout in the console will be?
Here’s what it is:
We has a match (open)! 3
We has a match (closed)! 3
We has a match (closed)! 10
You can have overlapping Ranges as cases, but the first one that matches will grab execution, so that explains why closed grabbed the execution in the second one (someNumber). 3 is well within both Ranges.
Note the red value. Was that what you expected?
Remember that half-open Ranges don’t include the last value. 10 is only valid in the ClosedRange.
You can also use tuples to specify “2D” ranges, or use a where clause to specialize the case.
Now, let’s get a bit weird:
var someString = "antelope" switch someString { case openStringRange: print("We have a match! \(someString)") default: print("No Match! \(someString)") } someString = "monster" switch someString { case openStringRange: print("We have a match! \(someString)") default: print("No Match! \(someString)") } someString = "zeb" switch someString { case openStringRange: print("We have a match! \(someString)") default: print("No Match! \(someString)") } someString = "zebr" switch someString { case openStringRange: print("We have a match! \(someString)") default: print("No Match! \(someString)") } someString = "zebra" switch someString { case openStringRange: print("We have a match! \(someString)") default: print("No Match! \(someString)") } someString = "zebpa" switch someString { case openStringRange: print("We have a match! \(someString)") default: print("No Match! \(someString)") }
Now, that was odd. What do you think was going on there?
The answer is that the String Range was evaluated as a series of weighted values, based upon (probably) the Unicode values for the characters.
This is one way that you could filter for names. like so:
func nameSort(_ inName: String) -> String { var ret = "No Match!" switch inName { case "a"..<"ab", "A"..<"AB": ret = "Group 0" case "ab"..<"ac", "AB"..<"AC": ret = "Group 1" case "ac"..<"ad", "AC"..<"AD": ret = "Group 2" case "ad"..<"ae", "AD"..<"AE": ret = "Group 3" case "ae"..<"af", "AE"..<"AF": ret = "Group 4" case "af"..<"ag", "AF"..<"AG": ret = "Group 5" case "ag"..<"ah", "AG"..<"AH": ret = "Group 6" case "ah"..<"ai", "AH"..<"AI": ret = "Group 7" case "ai"..<"ak", "AI"..<"AK": ret = "Group 8" case "ak"..<"al", "AK"..<"AL": ret = "Group 9" case "al"..<"am", "AL"..<"AM": ret = "Group 10" default: break } return ret } print(nameSort("abby")) print(nameSort("aiesha")) print(nameSort("aeisha")) print(nameSort("akbar")) print(nameSort("andy"))
Now, let’s see what gets printed out:
Group 1 Group 8 Group 4 Group 9 No Match!
You can use String ranges to sort through the first letters of the name.
Sort of. Try this:
print(nameSort("Abby"))
No Match!
Damn. No dice. Try this:
print(nameSort("ABBY"))
Group 1
Pretty literal. Looks like you’ll need to add cases for only the first letter capitalized:
func nameSort(_ inName: String) -> String { var ret = "No Match!" switch inName { case "a"..<"ab", "A"..<"AB", "A"..<"Ab": ret = "Group 0" case "ab"..<"ac", "AB"..<"AC", "Ab"..<"Ac": ret = "Group 1" · · ·
YUCK!
Well…you get the picture. I’d probably force lowercase on all the input, and take out the additional Ranges, but that’s not what this page is about. We’re just talking about how you could use non-iterative Ranges to segregate complex data. You could also do something similar with Doubles.
STRIDES
Apple used to have a type called “Interval,” but I guess it didn’t work out when you drilled into the devilish details, so they now have something called “Stride.”
Strides are really generic collections that conform to the Strideable protocol.
The Strideable protocol basically says that you need to get from here to there in discrete steps. They don’t have to be Ints, but they need to be regular and unvarying for the duration of the Stride.
Strides can go backwards, and don’t need to be scalar, and use the stride(from: T, to: T, by: T) function:
let strider = stride(from: 10.0, to: -10.0, by: -3.25) for aragorn in strider { print(aragorn) }
Which prints:
10.0 6.75 3.5 0.25 -3.0 -6.25 -9.5
You can think of Strides as, under the hood, creating an Array of values of type T
, and that Array is Strideable, which means that you can go from one value to the next in discrete steps.
SEQUENCES
Sequences play a huge role in Swift programming. Ranges and strides work with Sequences to allow repetition and traversal of value ranges.
First, your sequence must conform to the Sequence Protocol in order to work properly. Fortunately, that’s fairly simple. There are only 3 required (with non-default implementations) components of the Sequence Protocol:
func makeIterator() associatedtype Element associatedtype Iterator
makeIterator() is required, because it knows the type your collection aggregates (this will usually be for some kind of collection, but really, you could sequence anything -including things like a serial connection to an outside data source -which would be ugly and inadvisable, but hey, sometimes we do stuff simply because we can), and because it also abstracts the specifics of how the data is stored (or accessed).
The other 2 associated types are there because you need to tell the sequence about the data type you are aggregating (or accessing sequentially).
Sequence isn’t just a “dumb” protocol, telling you what you need to implement. The protocol itself implements a significant amount of default behavior, so it behaves more like a base class than a simple interface. Default implementations is actually something that was introduced in Swift 2.0 Protocol Extensions, and it’s starting to make Swift look a bit like a multiple-inheritance language (I know, I know. “It’s different,” but it sure smells all “multiple-inheritancy”).
In the sample playground below, we show how to implement a simple, and a not-quite-as-simple Sequence:
SAMPLE PLAYGROUND
//: Ranges (Open-Ended) can be empty let rangeIsEmpty = (0..<0).isEmpty //: ClosedRanges cannot be empty. let closedRangeIsEmpty = (0...0).isEmpty //: This will cause a nasty error. Uncomment to see the error. //let downwardSpiral = 1...0 //: Same here (which makes sense). //let downwardSpiral = 1..<0 let onwardAndUpward1 = -10..<10 // This is a half-open Range, from -10 to <10, in integer steps (19 steps, 20 values). let onwardAndUpward2 = -10...10 // This is a closed Range, from -10 to 10, in integer steps (20 steps, 21 values). //: You cannot specify Ranges backwards. This will cause an error. //let downwardSpiral = 1>..0 //: You can specify Ranges and ClosedRanges as Int: let openIntRange = 0..<10 let closedIntRange = 0...10 //: You can specify Ranges and ClosedRanges as Float: let openFloatRange = 0.0..<1.0 let closedFloatRange = 0.0...1.0 //: And even as Strings: let openStringRange = "aardvark"..<"zebra" let closedStringRange = "aardvark"..."zebra" //: This will work, as Int has discrete, iterable steps: for integ in openIntRange { print(integ) } //: These will not work, because Doubles and Strings don't have discrete steps: //for fl in openFloatRange { // print(fl) //} //for animal in openStringRange { // print(animal) //} //: Now, let's do a simple Int switch. //: Pay close attention to what happens here: let someNumber = 3 switch someNumber { case openIntRange: //: It will get caught here. 3 is within both Ranges, so the first match gets it. print("We has a match (open)! \(someNumber)") case closedIntRange: print("We has a match (closed)! \(someNumber)") default: print("No Match! \(someNumber)") } switch someNumber { case closedIntRange: //: First match. print("We has a match (closed)! \(someNumber)") case openIntRange: print("We has a match (open)! \(someNumber)") default: print("No Match! \(someNumber)") } let someOtherNumber = 10 switch someOtherNumber { case openIntRange: //: 10 is not actually in this Range. print("We has a match (open)! \(someOtherNumber)") case closedIntRange: //: It is in here. print("We has a match (closed)! \(someOtherNumber)") default: print("No Match! \(someNumber)") } //: OK. Let's get weird. var someString = "antelope" switch someString { case openStringRange: print("We have a match! \(someString)") default: print("No Match! \(someString)") } someString = "monster" switch someString { case openStringRange: print("We have a match! \(someString)") default: print("No Match! \(someString)") } someString = "zeb" switch someString { case openStringRange: print("We have a match! \(someString)") default: print("No Match! \(someString)") } someString = "zebr" switch someString { case openStringRange: print("We have a match! \(someString)") default: print("No Match! \(someString)") } someString = "zebra" switch someString { case openStringRange: print("We have a match! \(someString)") default: print("No Match! \(someString)") } someString = "zebpa" switch someString { case openStringRange: print("We have a match! \(someString)") default: print("No Match! \(someString)") } func nameSort(_ inName: String) -> String { var ret = "No Match!" switch inName { case "a"..<"ab", "A"..<"AB": ret = "Group 0" case "ab"..<"ac", "AB"..<"AC": ret = "Group 1" case "ac"..<"ad", "AC"..<"AD": ret = "Group 2" case "ad"..<"ae", "AD"..<"AE": ret = "Group 3" case "ae"..<"af", "AE"..<"AF": ret = "Group 4" case "af"..<"ag", "AF"..<"AG": ret = "Group 5" case "ag"..<"ah", "AG"..<"AH": ret = "Group 6" case "ah"..<"ai", "AH"..<"AI": ret = "Group 7" case "ai"..<"ak", "AI"..<"AK": ret = "Group 8" case "ak"..<"al", "AK"..<"AL": ret = "Group 9" case "al"..<"am", "AL"..<"AM": ret = "Group 10" default: break } return ret } print(nameSort("abby")) print(nameSort("aiesha")) print(nameSort("aeisha")) print(nameSort("akbar")) print(nameSort("andy")) //: So this should work, right? print(nameSort("Abby")) //: Damn. No dice. How about this? print(nameSort("ABBY")) //: Well, won't bother fixing it. The best way is to force case on the string, and do a simple comparison. This is really just a demo. let strider = stride(from: 10.0, to: -10.0, by: -3.25) for aragorn in strider { print(aragorn) } /*: # SEQUENCES This is about as simple as you'll get. The data is stored internally in a Sequence (an Array of Int), so most of our work is passing the protocol into our storage Array. */ struct SimpleListy: Sequence { typealias Element = Int typealias Iterator = Array<Element>.Iterator private var _myInts:[Element] = [] func makeIterator() -> SimpleListy.Iterator { return self._myInts.makeIterator() } mutating func addInteger(_ inInt: Element) { self._myInts.append(inInt) } } var myIntBucket = SimpleListy() myIntBucket.addInteger(10) myIntBucket.addInteger(3) myIntBucket.addInteger(1000) myIntBucket.addInteger(21) myIntBucket.addInteger(450) myIntBucket.addInteger(105) print("\nPrint the integers:\n") for integ in myIntBucket { print(integ) } /*: The following code implements a simple sequence, where items are stored in a Dictionary, which has an unspecified sort order, but are iterated alphabetically, by first name. An iterator is basically a struct or class with a "next()" method. How it gets there is why you may (or may not) want to do your own custom one. This is a simple demo of why you may want a custom iterator. Say that you want an unorganized list to be iterated in an organized manner? */ //: First, we declare a crony type, with the important information. typealias MyLittleCrony = (name: String, favorsOwed: Int, isRelative: Bool) //: Next, we declare a Sequence struct, which will include a custom ordered iterator. struct CronyList: Sequence { //: This is a typealias for our internal Dictionary typealias CronyListDictionary = [String:Element] //: This is required by the protocol typealias Element = MyLittleCrony //: This is our internal opaque storage. private var _cronyList: CronyListDictionary = [:] //: This is the iterator we'll use. Note that it sorts the response, using the Dictionary keys. struct Iterator : IteratorProtocol { //: We capture a copy of the list here. private let _iteratorList:CronyListDictionary //: This is an array of Dictionary keys that we use to sort. private let _keysArray:[String] //: This is the current item index. private var _index: Int //: Just capture the main object at the time the iterator is made. init(_ myLittleCronies: CronyListDictionary) { self._iteratorList = myLittleCronies self._keysArray = self._iteratorList.keys.sorted() // This sorts the iteration self._index = 0 } //: This is required by the protocol. mutating func next() -> Element? { //: We use the sorted keys array to extract our response. if self._index < self._keysArray.count { let ret = self._iteratorList[self._keysArray[self._index]] self._index += 1 return ret } else { return nil } } } //: This is required by the protocol. func makeIterator() -> CronyList.Iterator { return Iterator(self._cronyList) } //: We are legion var count: Int { get { return self._cronyList.count } } //: This is simply a convenient way to cast our random Dictionary into an ordered Array. var cronies: [Element] { get { var ret: [Element] = [] //: We use the iterator to order the Array. for crony in self { ret.append(crony) } return ret } } //: This allows us to make believe we're an Array. subscript(_ index: Int) -> Element { return self.cronies[index] } //: This is just how we'll load up the list. mutating func append(_ crony: Element) { self._cronyList[crony.name] = crony } } var myCronies = CronyList() myCronies.append((name: "Babs", favorsOwed: 7, isRelative: true)) myCronies.append((name: "Zulinda", favorsOwed: 10, isRelative: true)) myCronies.append((name: "Adriaaaaaan!", favorsOwed: 3, isRelative: false)) myCronies.append((name: "Sue", favorsOwed: 14, isRelative: true)) myCronies.append((name: "Cross-Eyed Mary", favorsOwed: 14, isRelative: false)) myCronies.append((name: "Charmain", favorsOwed: 14, isRelative: false)) myCronies.append((name: "Lucrecia McEvil", favorsOwed: 2, isRelative: false)) print("\nWe have \(myCronies.count) cronies.\n") print("\nFirst, this is the order in which the entries are stored in the Dictionary:\n") print(myCronies) print("\nNext, we iterate the Dictionary, using our custom iterator, which sorts alphabetically:\n") for (name, favorsOwed, isRelative) in myCronies { print("\(name) is owed \(favorsOwed) favors, and is \(isRelative ? "" : "not ")family.") } print("\nNext, we show how our accessor has sorted the contents of the Dictionary for us:\n") print(myCronies.cronies) print("\nFinally, we show how we can subscript our Sequence directly, and get it in order:\n") for index in 0..<myCronies.count { let (name, favorsOwed, isRelative) = myCronies[index] print("myCronies[\(index)]:\(name) is owed \(favorsOwed) favors, and is \(isRelative ? "" : "not ")family.") }