RegexBuilder in Swift

Hey Swifters 🙂 You might have used regular expressions in the past and would already know how messy and hard to read they can get, well in this guide we will explore the RegexBuilder in Swift that makes our lives easier. Lets start with:

What Is RegexBuilder?

RegexBuilder in Swift is a type-safe, compile checked way to construct regular expressions with Swift syntax instead of cryptic literals. You get autocompletion, strong typing, and instant compiler feedback on mistakes out of the box. Here is a quick taste of this new API.

Swift
import RegexBuilder

let digits = Regex {
    OneOrMore(.digit)
}

let test = "099079"
print(test.firstMatch(of: digits) != nil)  
// true

Not cryptic at all, right? Lets dig deep into every component, quantifier and capture.

Components

CharacterClass

Character classes let you define sets of characters to match. You can use predefined categories (like digits), custom ranges (e.g. a…z), or specific listings (e.g. [aeiou]). This is the building block for matching letters, numbers, or any custom group.

Swift
import RegexBuilder

// Lowercase letters only
// We want to match *only* strings made entirely of lowercase letters.
let lowercaseOnly = Regex {
  Anchor.startOfSubject
  OneOrMore("a"..."z")
  Anchor.endOfSubject
}

print("hello matches?:",  "hello".firstMatch(of: lowercaseOnly) != nil)
// true

print("Hello matches?:",  "Hello".firstMatch(of: lowercaseOnly) != nil)
// false

print("hell0 matches?:",  "hell0".firstMatch(of: lowercaseOnly) != nil)
// false

// Vowels only
let vowelsOnly = Regex {
  CharacterClass.anyOf("aeiou")
}
print("First vowel:", "swift".firstMatch(of: vowelsOnly)?.0 ?? "none") 
// "i"

// Not digits
let notDigits = Regex {
  CharacterClass(.digit).inverted
}
print("First non-digit:", "123abc".firstMatch(of: notDigits)?.0 ?? "none")
// "a"

Anchor

A regex component that matches a specific condition at a particular position in an input string.

Swift
import RegexBuilder

do {
  let startsWithHello = Regex {
    Anchor.startOfSubject
    "Hello"
  }
  print(try startsWithHello.firstMatch(in: "Hello, world!") != nil)
  // true
  print(try startsWithHello.firstMatch(in: "Well, Hello!") != nil)
  // false

  let endsWithPeriod = Regex {
    "."
    Anchor.endOfSubject
  }
  print(try endsWithPeriod.firstMatch(in: "Hi.") != nil)
  // true
  print(try endsWithPeriod.firstMatch(in: "Hi!") != nil)
  // false
} catch {
  print(error)
}

Lookahead & NegativeLookahead

Lookahead is a regex component that allows a match to continue only if its contents match at the given location. NegativeLookahead is a regex component that allows a match to continue only if its contents do not match at the given location.

Swift
import RegexBuilder

do {
  // Match “dog” only if immediately followed by “house”
  let dogHouse = Regex {
    "dog"
    Lookahead { "house" }
  }
  print(try dogHouse.firstMatch(in: "doghouse") != nil)
  // true
  print(try dogHouse.firstMatch(in: "doggie") != nil)
  // false

  // Match “car” only if NOT followed by “pet”
  let carNotPet = Regex {
    "car"
    NegativeLookahead { "pet" }
  }
  print(try carNotPet.firstMatch(in: "carpool") != nil)
  // true
  print(try carNotPet.firstMatch(in: "carpet") != nil)
  // false
} catch {
  print(error)
}

ChoiceOf

ChoiceOf is like the “or” operator (|) in traditional regex, it tries each alternative and picks the first one that matches.

Swift
import RegexBuilder

do {
  let colorPicker = Regex {
    ChoiceOf {
      "red"
      "green"
      "blue"
    }
  }
                    
  try ["red", "yellow", "blue", "Green"].forEach {
    print("\($0):", try colorPicker.firstMatch(in: $0) != nil)
  }
  // red:true, yellow:false, blue:true, Green:false
} catch {
  print(error)
}

Quantifiers

One

Match exactly one occurrence, use One when you explicitly want exactly one occurrence of a component. It’s the most literal way to say “exactly one.”

Swift
import RegexBuilder

do {
  let singleHash = Regex { One("#") }
  print(try singleHash.firstMatch(in: "er#fs#") != nil)
  // true - Looks for at least one # in the string
  print(try singleHash.wholeMatch(in: "##") != nil)
  // false - Looks for exactly one # and nothing else in the string
} catch {
  print(error)
}

Optionally

This makes the preceding element optional, zero or one occurrence.

Swift
import RegexBuilder

do {
  let signedNumber = Regex {
    Optionally { ChoiceOf { "+"; "-" } }
    OneOrMore(.digit)
  }
  try ["+42", "42", "-"].forEach {
    print("\($0):", try signedNumber.firstMatch(in: $0) != nil)
  }
  // "+42" : true, "42" : true, "-" : false
} catch {
  print(error)
}

"+42" is true beacuse it has the optional + and has two digits so true. “42” has no sign which if fine and has two digits so true again. Now “-” is the sign but it does not have any digits thats why false.

ZeroOrMore

This matches as many as possible, even zero of the given component.

Swift
import RegexBuilder

do {
  let manyXsThenY = Regex {
    ZeroOrMore("x")
    "y"
  }
  try ["p", "iy", "xxxxxy"].forEach {
    print("\($0):", try manyXsThenY.firstMatch(in: $0) != nil)
  }
  // "p" : false, "iy" : true, "xxxxxy" : true
} catch {
  print(error)
}

OneOrMore

This requires at least one but potentially many of the component.

Swift
import RegexBuilder

do {
  let repeatedAThenB = Regex {
    OneOrMore("a")
    "b"
  }
  try ["ab", "aaab", "b"].forEach {
    print("\($0):", try repeatedAThenB.firstMatch(in: $0) != nil)
  }
  // "ab" : true, "aaab" : true, "b" : false
} catch {
  print(error)
}

Repeat

A regex component that matches a selectable number of occurrences of its underlying component.

Swift
import RegexBuilder

do {
  let exactlyThreeAs = Regex {
    Repeat(count: 3) { "a" }
    "b"
  }
  print(try exactlyThreeAs.firstMatch(in: "aaab") != nil)
  // true
  print(try exactlyThreeAs.firstMatch(in: "aab" ) != nil)
  // false
  
  let twoToFourBsThenC = Regex {
    Repeat(2...4) { "b" }
    "c"
  }
  try ["bbc", "bbbbc", "bc"].forEach {
    print("\($0):", try twoToFourBsThenC.firstMatch(in: $0) != nil)
  }
  // "bbc" : true, "bbbbc" : true, "bc" : false
} catch {
  print(error)
}

Local

This creates an atomic group, preventing the engine from backtracking inside it. Use this when you need to lock in a match early.

Swift
import RegexBuilder

do {
  let atomicExample = Regex {
    Local {
      OneOrMore("a")
      "b"
    }
    "b"
  }
  // Try "aab": the inner group must match "aab" then literal "b" fails, and no backtracking occurs.
  print(try atomicExample.firstMatch(in: "aabb") != nil)
  // true
  print(try atomicExample.firstMatch(in: "aab")   != nil)
  // false
} catch {
  print(error)
}

Captures

Capture

It saves whatever is matched inside it so that you can retrieve it later from the match object.

Swift
import RegexBuilder

do {
  let dateCapture = Regex {
    Capture { Repeat(count: 2) { .digit } }  // Month
    "/"
    Capture { Repeat(count: 2) { .digit } }  // Day
    "/"
    Capture { Repeat(count: 4) { .digit } }  // Year
  }
  
  if let match = try dateCapture.firstMatch(in: "04/15/2025") {
    let (whole, month, day, year) = match.output
    print("Full date:", whole)   // "04/15/2025"
    print("Month:", month)       // "04"
    print("Day:", day)           // "15"
    print("Year:", year)         // "2025"
  }
} catch {
  print(error)
}

TryCapture

This not only saves the matched substring but also attempts to transform it. If the transform fails, it backtracks as if the capture didn’t match.

Swift
import RegexBuilder

do {
  let dateCapture = Regex {
    // Month: Convert to Int + validate (1-12)
    TryCapture {
      Repeat(count: 2) { .digit }
    } transform: { (str: Substring) -> Int? in
      guard let num = Int(str), (1...12).contains(num) 
      else { return nil }
      return num
    }
    
     "/"
    
    // Day: Convert to Int + validate (1-31)
    TryCapture {
      Repeat(count: 2) { .digit }
    } transform: { (str: Substring) -> Int? in
      guard let num = Int(str), (1...31).contains(num) 
      else { return nil }
      return num
    }
    
    "/"
    
    // Year: Convert to Int + validate (1900-2100)
    TryCapture {
      Repeat(count: 4) { .digit }
    } transform: { (str: Substring) -> Int? in
      guard let num = Int(str), (1900...2100).contains(num) 
      else { return nil }
      return num
    }
  }
  
  if let match = try dateCapture.firstMatch(in: "04/15/2025") {
    let (wholeMatch, month, day, year) = match.output
    print("Full date:", wholeMatch)   // "04/15/2025"
    print("Month:", month)            // 4 (Int)
    print("Day:", day)                // 15 (Int)
    print("Year:", year)              // 2025 (Int)
  }
} catch {
  print(error)
}

Reference

A reference to a captured portion of a regular expression. You can use a Reference to access a regular expression, both during the matching process (backreferencing) and after a capture has been successful.

Swift
import RegexBuilder

do {
  let monthRef = Reference<Int>()
  let dayRef = Reference<Int>()
  let yearRef = Reference<Int>()
  
  let dateRegex = Regex {
    TryCapture(as: monthRef) {
      Repeat(count: 2) { .digit }
    } transform: { str in
      Int(str).flatMap { (1...12).contains($0) ? $0 : nil }
    }
    
    "/"
    
    TryCapture(as: dayRef) {
      Repeat(count: 2) { .digit }
    } transform: { str in
      Int(str).flatMap { (1...31).contains($0) ? $0 : nil }
    }
    
    "/"
    
    TryCapture(as: yearRef) {
      Repeat(count: 4) { .digit }
    } transform: { str in
      Int(str).flatMap { (1900...2100).contains($0) ? $0 : nil }
    }
  }
  
  if let match = try dateRegex.firstMatch(in: "04/15/2025") {
   // Access captured values using references
    print("Month:", match[monthRef])  // 4
    print("Day:", match[dayRef])      // 15
    print("Year:", match[yearRef])    // 2025
  }
} catch {
  print(error)
}


Posted

in