What’s the best way to convert a Swift String to a Bool?
What’s A Valid Boolean?
The answer depends a lot on what you consider to be a valid text representation of a boolean value. While parsing some XML I needed to convert some text to a boolean value. My needs were fairly specific:
- Valid values for
true
are “true” or “yes”. - Valid values for
false
are “false” or “no”. - The conversion should be case-insensitive.
- Ignore any leading or trailing whitespace.
- If the input text is not a valid boolean value return
nil
.
Some examples:
"true", "True ", " YES " // true
"FALSE", "no ", " fAlSe" // false
"not true", "true or false" // nil
Common Approaches
A quick search on stack overflow and the Swift forums gives some common approaches:
Swift Standard Library
The Swift Standard Library implementation of Bool
has an initializer that takes a String. The implementation of Bool.swift is in the Swift Standard Library repository on GitHub:
public init?(_ description: String) {
if description == "true" {
self = true
} else if description == "false" {
self = false
} else {
return nil
}
}
This has the benefit of being fast but it only works for exact matches of “true” and “false”:
Bool("true") // true
Bool("false") // false
Bool("TRUE") // nil
Bool("YES") // nil
I can build on this to add case-insensitive matching and stripping of whitespace:
let input = " True "
Bool(input
.lowercased()
.trimmingCharacters(in: .whitespaces)) // true
But I’m going to need a different approach to match “yes” or “no”.
Core Foundation - NSString
Another common approach is to convert the Swift String
to a Foundation NSString
which has a boolValue method. The implementation of NSString.swift is in the Foundation repository on GitHub:
extension NSString {
public var boolValue: Bool {
let scanner = Scanner(string: _swiftObject)
// skip initial whitespace if present
let _ = scanner.scanCharacters(from: .whitespaces)
if scanner.scanCharacters(from: CharacterSet(charactersIn: "tTyY")) != nil {
return true
}
// scan a single optional '+' or '-' character, followed by zeroes
if scanner.scanString("+") == nil {
let _ = scanner.scanString("-")
}
// scan any following zeroes
let _ = scanner.scanCharacters(from: CharacterSet(charactersIn: "0"))
return scanner.scanCharacters(from: CharacterSet(charactersIn: "123456789")) != nil
}
}
A small extension handles the conversion:
extension String {
var boolValue1: Bool {
(self as NSString).boolValue
}
}
This method has perhaps too much flexibility in what it will accept. Anything starting with a t
, T
, y
or Y
is true
. That can lead to some surprising results:
"true".boolValue // true
"T".boolValue // true
"Yes".boolValue // true
"y".boolValue // true
"That's false".boolValue // true
"You're joking".boolValue // true
Likewise anything starting with f
, F
, n
or N
is false
:
"false".boolValue // false
"F".boolValue // false
"No".boolValue // false
"n".boolValue // false
"Not false".boolValue // false
It also accepts numeric values and ignores whitespace:
"1".boolValue // true
"0".boolValue // false
"001".boolValue // true
"+1".boolValue // true
" TRUE ".boolValue // true
" No ".boolValue // false
The biggest problem for me is that it treats invalid input as false
:
"a".boolValue // false
" ".boolValue // false
Stricter Checking
I need more flexibility than the standard library but stricter checking than NSString
, failing when the input is invalid. Here’s my attempt:
extension String {
var boolValue: Bool? {
guard let startIndex = self.firstIndex(where: { !$0.isWhitespace }) else {
return nil
}
let endIndex = self.lastIndex(where: { !$0.isWhitespace }) ?? self.endIndex
let trimmed = self[startIndex...endIndex]
switch trimmed.lowercased() {
case "true","yes": return true
case "false","no": return false
default: return nil
}
}
}
This meets my needs, is case-insensitive, ignores whitespace, and returns nil for invalid input:
"true".boolValue // true
"yes".boolValue // true
" TRUE ".boolValue // true
"false".boolValue // false
"No".boolValue // false
" FALSE ".boolValue // false
"Not False".boolValue // nil
"a".boolValue // nil
Performance
Trimming the leading and trailing whitespace comes with a performance cost. My version takes twice the time of the NSString
method which only strips leading whitespace. I did find that searching for the first and last index of any white space was slightly faster than using the Swift standard library method:
let trimmed = self.trimmingCharacters(in: .whitespaces)
If you don’t expect whitespace in the input you can speed things up considerably by removing the searches:
extension String {
public var boolValue: Bool? {
switch self.lowercased() {
case "true","yes": return true
case "false","no": return false
default: return nil
}
}
}