Converting A Swift String To A Bool

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
    }
  }
}