WWDC Quick Look 💓 By SwiftGGTeam
Meet Swift Regex

Meet Swift Regex

Watch original video

Highlight

Swift 5.7 introduces Regex at the language level and supports/.../Three creation methods: literal, runtime construction and Regex Builder, advanced features such as strong type capture, Foundation parser embedding, Unicode correct processing, and Local traceback control.

Core Content

Pain points of string parsing

You are working with a financial investigator to develop a transaction analysis tool. The data they give you is a bunch of plain text strings:

DEBIT     03/05/2022    Doug's Dugout Dogs         $33.27

Fields are separated by “more than two spaces” or “tabs” - no one remembers why. The date format is ambiguous, you can only hope it is month/day/year.

(01:16) UseStringTry the set algorithm?split(whereSeparator: \.isWhitespace)would split “Doug’s Dugout Dogs” into three fields. Drop down to index level and parse manually? The code is verbose and error-prone:

var slice = transaction[...]

func extractField() -> Substring {
    let endIdx = {
        var start = slice.startIndex
        while true {
            guard let spaceIdx = slice[start...].firstIndex(where: \.isWhitespace) else {
                return slice.endIndex
            }
            if slice[spaceIdx] == "\t" {
                return spaceIdx
            }
            let afterSpaceIdx = slice.index(after: spaceIdx)
            if afterSpaceIdx == slice.endIndex || slice[afterSpaceIdx].isWhitespace {
                return spaceIdx
            }
            start = afterSpaceIdx
        }
    }()
    defer { slice = slice[endIdx...].drop(while: \.isWhitespace) }
    return slice[..<endIdx]
}

let kind = extractField()
let date = try Date(String(extractField()), strategy: Date.FormatStyle(date: .numeric))
let account = extractField()
let amount = try Decimal(String(extractField()), format: .currency(code: "USD"))

This code is over 20 lines long and only handles field splitting. You need better tools.

Regex literal: concise syntax

(02:47) Swift 5.7 introduces/.../Regex literal syntax is compatible with mainstream regular dialects such as Perl, Python, Ruby, and Java:

let digits = /\d+/
// digits: Regex<Substring>

Key points:

  • The compiler understands regular syntax and provides syntax highlighting and compile-time error checking
  • The output type is genericRegex<Output>, containing capture type information

First use regular expressions to solve the field splitting problem:

let transaction = "DEBIT     03/05/2022    Doug's Dugout Dogs         $33.27"

let fragments = transaction.split(separator: /\s{2,}|\t/)
// ["DEBIT", "03/05/2022", "Doug's Dugout Dogs", "$33.27"]

03:56\s{2,}Matches two or more whitespace characters,|means or,\tMatches tab characters. One line of code replaces more than 20 lines of indexing.

Still availablereplacingUnify the delimiters:

let normalized = transaction.replacing(/\s{2,}|\t/, with: "\t")
// DEBIT\t03/05/2022\tDoug's Dugout Dogs\t$33.27

Regex Builder: Declarative string processing

(03:44) The literal is concise, but complex regular expressions are difficult to read. Regex Builder provides a declarative syntax:

import RegexBuilder

let fieldSeparator = /\s{2,}|\t/

let transactionMatcher = Regex {
    /CREDIT|DEBIT/
    fieldSeparator
    One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt))
    fieldSeparator
    OneOrMore {
        NegativeLookahead { fieldSeparator }
        CharacterClass.any
    }
    fieldSeparator
    One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US")))
}

Key points:

  • Regex { ... }Is a declarative builder, similar to SwiftUI’s ViewBuilder
  • Can mix literals (/CREDIT|DEBIT/) and the builder component -One(.date(...))Embed directly into Foundation’s date parser -One(.localizedCurrency(...))Embed currency parser

06:55NegativeLookaheadis a key component. It “peeps” at the following input but does not consume it, ensuring that field contents stop matching when a delimiter is encountered:

OneOrMore {
    NegativeLookahead { fieldSeparator }  // Peek: continue only if the next item is not a separator
    CharacterClass.any                    // Match any character
}

NoNegativeLookaheadCharacterClass.anyIt will greedily match to the end of the line and then backtrack all the way, which is inefficient.

Strong type capture

(09:04) UseCaptureExtract the fields of interest, the types are directly reflected inRegexIn the generic parameters:

let transactionMatcher = Regex {
    Capture { /CREDIT|DEBIT/ }
    fieldSeparator

    Capture { One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt)) }
    fieldSeparator

    Capture {
        OneOrMore {
            NegativeLookahead { fieldSeparator }
            CharacterClass.any
        }
    }
    fieldSeparator

    Capture { One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US"))) }
}
// transactionMatcher: Regex<(Substring, Substring, Date, Substring, Decimal)>

Key points:

  • The 0th capture is the entire matched text (Substring)
  • The 1st capture is the transaction type (Substring)
  • The 2nd capture is the date (Date, directly output by the Foundation parser)
  • The 3rd capture is the institution name (Substring)
  • The 4th capture is the amount (Decimal, directly output by the Foundation parser)

Directly access strongly typed properties when using:

func parseLine(_ line: Substring) throws -> MailmapEntry {
    let regex = /\h*([^<#]+?)??\h*<([^>#]+)>\h*(?:#|\Z)/

    guard let match = line.prefixMatch(of: regex) else {
        throw MailmapError.badLine
    }

    return MailmapEntry(name: match.1, email: match.2)
}

Runtime construction and expansion delimiters

(03:20) Some regular expressions need to be constructed from user input at runtime:

let runtimeString = #"\d+"#
let digits = try Regex(runtimeString)
// digits: Regex<AnyRegexOutput>

Key points:

  • Runtime constructs may throw syntax errors
  • The output type isAnyRegexOutput, because the capture information is not known at compile time

(10:53) For regular expressions containing slashes, use extended delimiters#/.../#

let regex = #/
  (?<date>     \d{2} / \d{2} / \d{4})
  (?<middle>   \P{currencySymbol}+)
  (?<currency> \p{currencySymbol})
/#
// Regex<(Substring, date: Substring, middle: Substring, currency: Substring)>

Key points:

  • #/.../#Allow unescaped slashes inside regular expressions
  • Whitespace is ignored in extended syntax mode, and indentation can be used to improve readability. -(?<name>...)It is a named capture, and the output type is labeled

Unicode is handled correctly

(12:45) Swift’s String handles Unicode extended grapheme clusters (character level) by default. Regex also follows this model:

let aZombieLoveStory = "🧟‍♀️💖🧠"
// Characters: 🧟‍♀️, 💖, 🧠

aZombieLoveStory.unicodeScalars
// U+1F9DF, U+200D, U+2640, U+FE0F, U+1F496, U+1F9E0

(14:12) Swift String follows Unicode Canonical Equivalence."café"and"cafe\u{301}"The comparison results in equality even though the underlying Unicode scalars are different.

Regex also matches at the character level by default:

input.firstMatch(of: /.\N{SPARKLING HEART}./)
// 🧟‍♀️💖🧠  - matches three characters

input.firstMatch(of: /.\N{SPARKLING HEART}./.matchingSemantics(.unicodeScalar))
// ️💖🧠  - matches three Unicode scalars; note the leading variation selector

Key points:

  • Default.matchingSemantics(.graphemeCluster)Match Swift Character -.matchingSemantics(.unicodeScalar)Match Unicode scalar level
  • Use the default behavior in most scenarios, switch only when precise control is required

Local traceback control

(21:45) The regular backtracking mechanism is a double-edged sword. Global backtracking is suitable for searches and fuzzy matching, but it can cause performance issues for exact matching:

let fieldSeparator = /\s{2,}|\t/
// With 8 spaces, match all 8; if a later match fails, backtrack to 7, 6, ...

LocalThe builder creates a local lookback scope, discarding untried alternatives after a successful match:

let fieldSeparator = Local { /\s{2,}|\t/ }

let transactionMatcher = Regex {
    Capture { /CREDIT|DEBIT/ }
    fieldSeparator          // Match all whitespace, without backtracking on later failures

    TryCapture(field) { timestamp ~= $0 ? $0 : nil }
    fieldSeparator

    TryCapture(field) { details ~= $0 ? $0 : nil }
    // ...
}

Key points:

  • Local { ... }Equivalent to atomic non-capturing group
  • Suitable for precisely specified tokens, such as field separators
  • Limit the search space and improve matching efficiency

Detailed Content

TryCapture: Execute custom logic in matching

18:55TryCaptureAllows you to use closures to participate in the matching process and implement runtime verification:

let transactionMatcher = Regex {
    Capture { /CREDIT|DEBIT/ }
    fieldSeparator

    TryCapture(field) { timestamp ~= $0 ? $0 : nil }  // Runtime verification with the timestamp regex
    fieldSeparator

    TryCapture(field) { details ~= $0 ? $0 : nil }    // Runtime verification with the details regex
    fieldSeparator

    // ...
}

Key points:

  • TryCaptureThe closure returnsnilIndicates that the match failed
  • Closures actively participate in matching and can perform any verification during the matching process
  • Suitable for integrating existing runtime rules or custom parsing logic

Reusable Regex components

(27:05) Encapsulate common patterns intoRegexComponent, reused in different places:

struct MailmapLine: RegexComponent {
    @RegexComponentBuilder
    var regex: Regex<(Substring, Substring?, Substring)> {
        ZeroOrMore(.horizontalWhitespace)

        Optionally {
            Capture(OneOrMore(.noneOf("<#")))
        }
        .repetitionBehavior(.reluctant)

        ZeroOrMore(.horizontalWhitespace)

        "<"
        Capture(OneOrMore(.noneOf(">#")))
        ">"

        ZeroOrMore(.horizontalWhitespace)
        /#|\Z/
    }
}

// Usage
let match = line.prefixMatch(of: MailmapLine())

Key points:

  • RegexComponentProtocols allow custom types to be used as regular components -@RegexComponentBuilderProperty wrappers enable builder syntax
  • Can mix literals and builders inside components

Embed Foundation parser

struct DatedMailmapLine: RegexComponent {
    @RegexComponentBuilder
    var regex: Regex<(Substring, Substring?, Substring, Date)> {
        // ... previous mailmap pattern ...

        Capture(.iso8601.year().month().day())  // Produces Date directly

        ZeroOrMore(.horizontalWhitespace)
        /#|\Z/
    }
}

Key points:

  • .iso8601.year().month().day()Is Foundation’s date parsing strategy
  • The capture type is directlyDate, no subsequent manual conversion is required
  • Any type that conforms to the parsing strategy can be embedded in the Regex Builder

Core Takeaways

1. Replace all string indexing operations with Regex

Check all items usedString.indexSubstringrange operations,.components(separatedBy:)Where strings are parsed. Especially log parsing, CSV/TSV processing, configuration file reading, and user input validation. Swift Regex’s strong type capture makes parsing results directly available, reducing intermediate conversions.

Entrance:split(separator:)prefixMatch(of:)firstMatch(of:)replacing(_:with:)

2. Use Regex Builder instead of NSRegularExpression

Still used in projectsNSRegularExpressionIn place, migrate to Regex Builder. Declarative syntax is more readable and avoids strong type captureNSTextCheckingResultIndexing operations are handled correctly by Unicode by default. You can start with simple mode and migrate gradually.

Entrance:import RegexBuilderRegex { ... }

3. Encapsulation business-specific RegexComponent

Encapsulate regular patterns reused in the project intoRegexComponent. For example, email verification, mobile phone number format, order number rules, log timestamps, etc. Reusing components ensures consistency, and you only need to change one place when modifying.

Entrance:struct EmailPattern: RegexComponent { ... }

4. Use Local to control performance-sensitive regular expressions

For scenarios where large amounts of text are processed (log analysis, data cleaning, real-time filtering), check whether there are unnecessary global backtrackings in the regex. Field delimiters, fixed format tokens and other precise patterns are usedLocal { ... }Packages,limit the search space.

Entrance:Local { /pattern/ }

Comments

GitHub Issues · utterances