WWDC Quick Look 💓 By SwiftGGTeam
Swift Regex: Beyond the basics

Swift Regex: Beyond the basics

Watch original video

Highlight

Swift 5.7 introduces string handlingRegextypes, regular literal syntax, andRegexBuilderDSL that allows developers to match, extract, and transform string content in a type-safe manner, and embed Foundation’s date/number parsers directly into regular expressions.

Core Content

Manipulating strings is one of the most common tasks in programming. From log parsing to data verification, developers write various string matching codes every day.

Before Swift 5.7, processing regular expressions mainly relied onNSRegularExpression. This API comes from the Objective-C era and is cumbersome to use: the matching result isNSTextCheckingResult, the capture group must be accessed through numeric index, and the types are allStringorRange, it is easy to make mistakes.

Swift 5.7 brings a new regular solution. The core is three things:

  1. RegexType: a new regular type in the standard library, strongly typed and composable
  2. Regular literal: The compiler checks syntax, and Xcode supports syntax highlighting
  3. RegexBuilderDSL: Use Swift code to describe regular patterns, with readability close to natural language

The combination of these three turns string processing from “black magic” into ordinary Swift code.

From string to type-safe matching

Let’s say you want to extract user IDs from a piece of text:

name:  John Appleseed,  user_id:  100

Used beforeNSRegularExpressionA bunch of boilerplate code needs to be written. Now using Swift Regex, there are three writing methods to choose from.

Create from string (runtime parsing):

let regex = try Regex(#"user_id:\s*(\d+)"#)

Regular literal (compile time check):

let regex = /user_id:\s*(\d+)/

RegexBuilder DSL (best readability):

import RegexBuilder

let regex = Regex {
    "user_id:"
    OneOrMore(.whitespace)
    Capture(.localizedInteger)
}

Created by three writing methodsregexAllRegexType, which can be passed to the unified matching algorithm.

List of matching algorithms

The Swift standard library isStringandSubstringExtended a batch of regularity-driven algorithms (03:49):

let input = "name:  John Appleseed,  user_id:  100"
let regex = /user_id:\s*(\d+)/

input.firstMatch(of: regex)           // Find the first match
input.wholeMatch(of: regex)           // The entire string must match
input.prefixMatch(of: regex)          // Prefix match
input.starts(with: regex)             // Whether it starts with the pattern
input.replacing(regex, with: "456")   // Replace the matched content
input.trimmingPrefix(regex)           // Remove the prefix-matched portion
input.split(separator: /\s*,\s*/)     // Split by regex

Regularity can also be used inswitchIn the statement:

switch "abc" {
case /\w+/:
    print("It's a word!")
}

Parse bank statement

Foundation’s parser can be directly embeddedRegexBuilder05:14):

let statement = """
    DSLIP    04/06/20 Paypal  $3,020.85
    CREDIT   04/03/20 Payroll $69.73
    """

let regex = Regex {
    Capture(.date(format: "\(month: .twoDigits)/\(day: .twoDigits)/\(year: .twoDigits)"))
    OneOrMore(.whitespace)
    OneOrMore(.word)
    OneOrMore(.whitespace)
    Capture(.currency(code: "USD").sign(strategy: .accounting))
}

here.date()and.currency()It is a parser provided by Foundation that supports localization and handles various edge cases. You don’t need to write date parsing logic yourself.

Detailed Content

Use Capture to extract data

Matching is only the first step, extracting data is the key.CaptureYou can save the matched substring and append it to the output tuple (10:28):

let regex = Regex {
   "a"
   Capture("b")
   "c"
   /d(e)f/
}

if let match = "abcdef".wholeMatch(of: regex) {
    let (wholeMatch, b, e) = match.output
    // wholeMatch = "abcdef"
    // b = "b"
    // e = "e"
}

Key points:

  • match.outputis a tuple, the first element is the entire match, followed by eachCapture- Parentheses in regular literals()Also a capture group
  • The type of the output tuple is determined at compile time

Practical combat: parsing XCTest logs

Let’s say you want to parse test suite logs:

Test Suite 'RegexDSLTests' started at 2022-06-06 09:41:00.001
Test Suite 'RegexDSLTests' failed at 2022-06-06 09:41:00.001.
Test Suite 'RegexDSLTests' passed at 2022-06-06 09:41:00.001.

The first step is to write the basic regular rules (06:24):

import RegexBuilder

let regex = Regex {
    "Test Suite '"
    /[a-zA-Z][a-zA-Z0-9]*/
    "' "
    ChoiceOf {
        "started"
        "passed"
        "failed"
    }
    " at "
    OneOrMore(.any)
    Optionally(".")
}

Key points:

  • ChoiceOfMatch one of multiple options -OptionallyMatch zero or one time
  • Regular literals/[a-zA-Z][a-zA-Z0-9]*/Can be embedded in DSL

plusCaptureExtract data (11:10):

let regex = Regex {
    "Test Suite '"
    Capture(/[a-zA-Z][a-zA-Z0-9]*/)
    "' "
    Capture {
        ChoiceOf {
            "started"
            "passed"
            "failed"
        }
    }
    " at "
    Capture(OneOrMore(.any))
    Optionally(".")
}

for line in testInputs {
    if let (whole, name, status, dateTime) = line.wholeMatch(of: regex)?.output {
        print("\(name), \(status), \(dateTime)")
    }
}

Dealing with greedy matching problems

Run the above code and you will finddateTimeIncludes the final period. The reason isOneOrMore(.any)The default is eager and will match as many characters as possible (11:51).

The fix is ​​to change to reluctant mode:

Capture(OneOrMore(.any, .reluctant))

Key points:

  • .reluctantLet duplicates match as few characters as possible
  • The engine will first try to match the following pattern, and then backtrack to consume more characters if it fails.
  • can also be used.repetitionBehavior(.reluctant)Modify the default behavior of the entire regex

Transforming Capture: from substring to specific type

The captured content defaults toSubstring, usually needs to be converted to a more meaningful type.CapturesupporttransformClosure (15:20):

Regex {
    Capture {
        OneOrMore(.digit)
    } transform: {
        Int($0)  // The output type becomes Int?
    }
}

If the conversion may fail, useTryCapture, which will “flatten” the optional type (15:55):

Regex {
    TryCapture {
        OneOrMore(.digit)
    } transform: {
        Int($0)  // The output type is Int, not Int?
    }
}

Key points:

  • TryCaptureThe transform returnsOptional, but the output type is non-optional
  • When the conversion fails, the engine will automatically backtrack and try other matching paths.
  • Suitable for use with failable initializers

Processing dates with the Foundation parser

Convert the status to an enumeration and the timestamp toDate16:21):

enum TestStatus: String {
    case started, passed, failed
}

let regex = Regex {
    "Test Suite '"
    Capture(/[a-zA-Z][a-zA-Z0-9]*/)
    "' "
    TryCapture {
        ChoiceOf { "started"; "passed"; "failed" }
    } transform: {
        TestStatus(rawValue: String($0))
    }
    " at "
    Capture(.iso8601(
        timeZone: .current,
        includingFractionalSeconds: true,
        dateTimeSeparator: .space))
    Optionally(".")
}
// Output type: Regex<(Substring, Substring, TestStatus, Date)>

Key points:

  • .iso8601()Is a date parser provided by Foundation
  • The last element of the output tuple is directlyDateType
  • No need to do secondary analysis manually

Custom parser: access to C library

If you want to plug into an existing C parser, you can doCustomConsumingRegexComponentProtocol (19:16):

import Darwin

struct CDoubleParser: CustomConsumingRegexComponent {
    typealias RegexOutput = Double

    func consuming(
        _ input: String,
        startingAt index: String.Index,
        in bounds: Range<String.Index>
    ) throws -> (upperBound: String.Index, output: Double)? {
        input[index...].withCString { startAddress in
            var endAddress: UnsafeMutablePointer<CChar>!
            let output = strtod(startAddress, &endAddress)
            guard endAddress > startAddress else { return nil }
            let parsedLength = startAddress.distance(to: endAddress)
            let upperBound = input.utf8.index(index, offsetBy: parsedLength)
            return (upperBound, output)
        }
    }
}

Using a custom parser (20:13):

let regex = Regex {
    "Test Case "
    OneOrMore(.any, .reluctant)
    "("
    Capture {
        CDoubleParser()
    }
    " seconds)."
}

if let match = input.wholeMatch(of: regex) {
    print("Time: \(match.1)")  // Double type
}

Key points:

  • consumingThe method returns the upper bound index of the match and the parsed result
  • returnnilIndicates that the match fails and the engine will backtrack
  • This is a bridge to existing parsing libraries (such as JSON, YAML parsers)

Core Takeaways

1. Log parser

What to do: Write an Xcode build log parser for your project that extracts compilation errors and warnings.

Why it’s worth doing: UseRegexBuilderTo describe the log format, useTryCaptureConvert error information into structured data, which is easier to maintain than regular strings.

How to start: Define aBuildLogEntrystructure, useRegexBuildermatcherror: / warning:prefix, useCaptureExtract file name, line number and error description.

2. Smart Clipboard Tool

What to do: Make a macOS tool that automatically recognizes clipboard content and extracts key information (such as delivery number, mobile phone number, URL).

Why it’s worth doing: Swift RegexfirstMatchandwholeMatchCan be used directly on clipboard strings, withTryCaptureConvert the matching result to a specific type.

How to start: UseNSPasteboardMonitor clipboard changes, try multiple regular patterns on text content, and display the extraction results after successful matching.

3. Configuration file validator

What to do: Write a validation tool for the team’s internal configuration file format, check for syntax errors and give friendly prompts.

Why it’s worth doing:RegexBuilderThe readability makes the configuration rules clear at a glance, and new members can quickly understand the verification logic.

How to start: Define each line of the configuration file as aRegex,usewholeMatchVerification, returning the specific error location and expected format when it fails.

4. Data migration script

What to do: Write a script to migrate the text logs of the old system to the new database, use regular expressions to extract fields and convert types.

Why it’s worth doing:TryCaptureCooperateDate / Int / DoubleInitializer, one line of regular expression can complete the extraction + conversion, no secondary parsing is required.

How to get started: Use Foundation.date()and.currency()Parser to handle date and amount fields, customCustomConsumingRegexComponentHandle special formats.

  • Meet Swift Regex — Getting started with Swift Regex, introducing basic syntax and core concepts
  • What’s new in Foundation — How Foundation’s formatters and parsers work with RegexBuilder
  • Swift Concurrency — Understand the Swift concurrency model and write high-performance parsing tools with string processing

Comments

GitHub Issues · utterances