Reputation: 1160
I'm just wondering if there's a more readable/less verbose way to write the following:
let rankMap: Dictionary<Int,String> = [1:"Ace", 11:"Jack", 12:"Queen", 13:"King"]
func getCardImage(suit: String, rank: Int) -> NSImage {
var imgRank: String
if rankMap[rank] == nil {
imgRank = rank.description
} else {
imgRank = (rankMap[rank]! as NSString).substringToIndex(1)
}
let imgSuit = (suit as NSString).substringToIndex(1)
//let imgRank: String = rankMap[rank] ?? rank.description
return NSImage(named: imgRank + imgSuit)
}
In particular, I'm interested in the part where rankMap is checked vs nil. Whatever is returned from the array needs to have every character except the first chopped off. I'm unable to write something like this:
let imgRank: String = (rankMap[rank] as NSString).substringToIndex(1) ?? rank.description
..due to the semantics around nil coalescing. Is there anything else I can do aside from the "hard way" of just doing an "if nil" check?
Upvotes: 0
Views: 432
Reputation: 126137
You don't need substringToIndex
to get the first character of a Swift string. String
is a CollectionType
, to which this generic function from the standard library applies:
func first<C : CollectionType>(x: C) -> C.Generator.Element?
So you might start by trying something like this:
let imgRank = first(rankMap[rank]) ?? rank.description // error
That doesn't work, for two reasons:
String.Generator.Element
is a Character
, not a String
, so if you're going to put it in a ??
expression, the right operand of ??
needs to also be a Character
. You can construct a Character
from a String
(with Character(rank.description)
), but that'll crash if that string has more than one character in it.rankMap[rank]
is an optional, so you can't pass it directly to first
. But if you try to force-unwrap it immediately (with first(rankMap[rank]!)
, you'll crash upon looking up something not in the dictionary.You could try solving these problems together by putting the ??
inside the first
call, but you probably want a two-character string for rank 10.
It'd be nice if we could treat String
like an Array
, which provides var first
for getting its first element. (And have a version of first
for strings that returns a String
.) Then we could take advantage of optional chaining to put a whole expression to the left of the ??
, and just write:
let imgRank = rankMap[rank]?.first ?? rank.description
Well, let's write it in an extension!
extension String {
var first : String? {
// we're overloading the name of the global first() function,
// so access it with its module name
switch Swift.first(self) {
case let .Some(char):
return String(char)
case .None:
return nil
}
}
}
Now you can be nice and concise about it:
let imgRank = rankMap[rank]?.first ?? String(rank)
(Notice I'm also changing rank.description
to String(rank)
. It's good to be clear -- description
gives you a string representation of a value that's appropriate for printing in the debugger; String()
converts a value to a string. The former is not guaranteed to always match the latter, so use the latter if that's what you really want.)
Anyhow, @RobNapier's advice stands: you're probably better off using enums for these. That gets you a lot less uncertainty about optionals and invalid lookups. But seeing what you can do with the standard library is always a useful exercise.
Upvotes: 0
Reputation: 299355
I removed the NSImage
part, just so it's easier to test as a filename, but this should behave similarly.
func cardImage(suit: String, rank: Int) -> String {
let imgRankFull = rankMap[rank] ?? rank.description
let imgRank = first(imgRankFull) ?? " "
let imgSuit = first(suit) ?? " "
return imgRank + imgSuit
}
This shows how to use ??
, but it still has lots of little problems IMO. I would recommend replacing suit
and rank
with enums. The current scheme has lots of ways it can lead to an error, and you don't do any good error checking. I've dodged by returning " ", but really these should be either fatalError
or an optional, or a Result
(an object that carries a value or an error).
Upvotes: 2