Regular Expressions in Swift
Guy Lapalme
RALI-DIRO
Université de Montréal
August 2024

The Swift programming language’s regular expression notation is unique enough to warrant an explanation. While it adheres to most of the conventions found in languages such as PERL, Java, JavaScript, PHP and Python, it also has some significant differences that I found challenging to understand and had to figure out through trial and error. This document is the product of my efforts to understand and I am sharing it in the hope that it will help others. I will first review what regular expressions are, and then demonstrate how they can be represented and used in Swift. I will use three examples to highlight some unique aspects of Swift’s regular expression implementation: parsing Roman numerals, a tokenizer, and an Eliza-like chatbot. The appendix includes links to additional resources and a handy cheat sheet.

This document serves as a personal reference for me, as I couldn’t find a comprehensive guide on Swift regular expressions. I have searched the internet for tutorials and videos on Swift regexes, but I have not found a clear definition of what they can be used for and why. Most examples I have seen are repetitive.

[HTML version] [Markdown version] [PDF version]

What is a regex ?

In 1951, S. C. Kleene introduced the concept of a regular expression, also known as regex, as a formal language construct that describes the collection of strings formed through the operations of concatenation, alternation, and repetition, or quantification. This concept was later applied in the 1970s to define patterns for matching lines in a file. For instance, the Unix grep command, whose name derives from “global regular expression print”, searches for and displays lines that match a specified regular expression. This notation has proven to be extremely practical and effective for extracting data from extensive texts, as well as for defining modifications in text editors.

Most modern programming languages offer a regular expression syntax for extracting the string parts that match a regex, or for indicating failure if no match is found.

Swift is a strongly typed programming language, and its creators have taken great care to ensure that regular expressions and their outputs comply with type constraints. Swift’s regular expression syntax was introduced in 2022, along with Swift 5.7. Previous Swift versions relied on the NSRegularExpression Objective-C library, which was less than ideal due to the complexity of bridging Objective-C’s NSString and Swift’s String. As a result, many people, including me, created custom functions for common tasks. However, these new regular expressions only work on iOS 16.0+ and MacOS 13.0+.

Swift’s syntax may be new, but it shares similarities with well-known languages like Perl, Java, JavaScript, PHP, and Python. Despite some difficulties, I have understood certain aspects of Swift. This document summarizes my understanding, which I believe can benefit others.

We first briefly recall what is a regular expression to define the terms used in the rest of the document. In this section, we focus on the features that are common to all regex notations. Swift peculiarities will be detailed later.

Components of Regex Definitions

A regular expression defines a pattern whose occurrences must be matched within a string, the subject. We follow the time-honored terminology of SNOBOL4 which, in the sixties, was the first programming language to allow the definition of patterns as first-class objects for matching and operating on strings.

A pattern (a RegexComponent in the Swift parlance) is a combination of :

Sequence has priority over alternation, but parentheses can be used to change this ordering. For example, the pattern abc|def|ghi matches occurrences of abc, def or ghi , while a(bc|de)f|ghi matches occurrences of abcf, adef or ghi . But we will see later that parentheses are also used for delimiting parts of patterns that matches that are called capture groups.

A pattern can be repeated a certain number of times by adding a quantifier after it:

By default, repetition of an indeterminate number of times is eager to find the longest match which is most often what is needed. In some cases, this strategy can lead to unexpected results. For example, given the subject string "hello" my "friends", the pattern ".*" will match once the whole subject, matching the quotes at the start and at the end of the string because the dot matches any character including a quote.

Repetition can be specified as either:

When a regex must match a special character used for alternation or quantifier, this character must be escaped by a backslash, e.g., so matching a and b separated by + must be defined as a\+b otherwise it would match one or more a followed by a b.

Matching a character looks simple, but there can be variations. Should matching be case insensitive? Should diacritics be taken into account? For a multiline string, should position matches apply to each line ? Each language has its own way (usually flags) of specifying changes in matching behavior.

This very brief refresher on regex syntax is very far from complete, but it is adequate for explaining how regexes work in Swift. For a more complete list of metacharacters and operators.

Result of the matching process

When applying a pattern to a subject, one of two things may happen:

Given that regular expressions in most programming languages are defined using strings, errors or exceptions can occur at run-time if the regex string is not syntactically correct (e.g., unbalanced parentheses). Some compilers check the syntax of string literals in some special cases, but not in all of them. Swift goes to great lengths to limit the risk of such failures by applying static type checking to regexes as well. In other type-checked languages such as Java, regexes are considered as plain strings whose peculiar structure is checked by a compile method, which is called at run-time.

Regex in Swift

The matching process can be relatively slow because the pattern must be interpreted while scanning the subject. Most programming languages allow the compilation of a regex from the string to create an automaton for faster matching. A Swift regex is compiled by default to ensure that it is well formed and type-checked. Swift defines a Regex type used for type checking any expression involving regexes.

Swift Regex Definition

Swift provides different notations for defining a regular expression:

RegexBuilderRegexBuilderExtended literal
Regex {
ChoiceOf {
   Regex {
     "a"
ChoiceOf {
"bc" ; "de"
}
"f"
}
"ghi"
}
Regex {
/"/
ZeroOrMore(.reluctant) {
/./
}
/"/
#/
" # opening quote
.*? # up to next quote
" # ending quote
/#

We will later show how to define run-time regular expressions using strings. In the remainder of this section, we use regex literals as the other notations are equivalent. We want to focus on the matching process, not on the syntax of the regular expressions.

Regex Modifications

Changing some aspect of the matching behavior, obtained using flags in other programming languages, is performed by calling a Regex method that returns a new Regex:

Swift Regex Operations

Regular expressions are used for identifying which parts of a subject correspond to the pattern. In some cases, it is enough to only check whether a pattern occurs in the subject, but more often it is necessary to get more information about the match.

Here are the definitions of pattern and subject used as examples in this subsection.

Checking for an Occurrence of a Pattern

The String methods .contains(..) and .starts(with:..) return a Bool (the value of each expression is shown here after // =>)

Looking in a Subject for Matches of a Pattern

In Swift, the result of applying a pattern to a subject results in match object of optional type Regex<Output>.Match? . The String methods .firstMatch(of: Regex), .prefixMatch(of: Regex) and .wholeMatch(of: Regex) return nil when no instance of the pattern is found, otherwise they return an object whose properties give access to matching information such as

Caution: .output and .count have slightly different meanings for a dynamic regex defined by a string.

Here are examples of calls that use optional chaining operator ?. that returns nil when its left part is nil; the binary nil coalescing operator ?? unwraps its left Optional operand if it is not nil otherwise it returns its right operand. a ?? b can be understood as a != nil ? a! : b.

In this case, the subject is matched for an instance of a pattern

To get an array of all matches, we use the String method .matches(of: Regex) . When no match is found in the subject, the array is empty in the spirit of replacing failure by a list of successes.. This is useful for transforming all matches using a closure. Here is an example that returns the matched substrings in upper case.

Given a Pattern, Finding Matches within a Subject

A pattern can also be searched within a string using Regex methods bearing the same names as in the previous section but with a different keyword for their parameter.But .matches(..) is not defined for patterns. For reasons that will be explained later, the evaluation of a pattern in this context can raise an exception, so the pattern must be prefixed by try .

Replacing the Match in the Subject

Once a match is found, we often want to change it by another string, the replacement. There are two related String methods:

In the next example, the replacement is a string, so it creates a new string in which each identifier is replaced by *id*, unmatched substrings (here 10 and punctuation signs) are not modified.

A closure is needed when the replacement depends on the content of the subject such as in the following example where each identifier is wrapped in square brackets:

This notation can be simplified with a trailing closure accessing to the match with $0 , the implicit first parameter for a closure in Swift. $n happens to be also the notation used in regex replacements in many programming languages.

Here is an example of a replacement within a variable string. Caution .replace(...) returns Void as a reminder of the side effect of this expression.

Determining the Ranges of Matches

To get the list (possibly empty) of the ranges of matches within a string, we can use .range(of:..). A range in Swift can be used for indexing a string to get the corresponding substring. For example, here to get the list of matched substrings.

Splitting a String According to a Pattern

A regex can also be used to split a string to get an array of substrings between separators. For example, a simple minded tokenizer can be implemented by splitting using a non-empty sequence of characters that cannot be part of a word /\W+/:

This removes the separators, but keeping them in the split is a bit more involved. We could instead try matching an empty string (called a lookBefore) before the non-word pattern , here /(?=\W+)/:

Unfortunately, this is not adequate because some tokens have spaces or a parenthesis before them. To avoid this problem, the separator should also use a lookBehind (?<=\W+) match, so the expression should be the following:

While Swift recognizes the lookBehind syntax, it warns that it is not yet implemented. Getting the list of both the substrings and the separators can be achieved using the .ranges(of:) method described in the previous section. Using the list of ranges of separator occurrences, we build the list substrings between each range while adding also the content of each separator. This can be implemented with this String extension.

The following expression separates a string at any non-word ignoring tokens comprising only a single space which are not considered useful.

Removing a Match at the Start of a Subject

To remove a match at the start of a subject, .trimmingPrefix(..) can be used. For example

If the pattern does not match the beginning of the subject, the subject is returned unchanged. Similar to the .replacing(..)/.replace(..) pair, .trimPrefix(..) removes the start of the subject which must have been declared as var.

Capturing Information within a Match

Regular expressions are useful for extracting information from strings. Once a match is found, parts of it can be captured. For illustrating this concept, we define a pattern to extract key-value pairs: the key is an identifier and the value is a series of digits. They are separated by an equal sign with optional spacing between them. The value field with the preceding equal sign can be omitted. The following is an example subject.

The following pattern can be used to extract the substring associated with the key and the value substrings. Capture groups are delimited by parentheses and numbered from the left according to their open parentheses (the comment line below shows the opening parenthesis starting each group), group 0 is the substring corresponding to the whole match.

In this pattern, group 1 is the key, while group 3 is the value: these values can be accessed through indexing (e.g. m.1 or m.3) like any Swift tuple.

This convention is widely used in regular expressions in programming languages. In Swift, however, because of the strong typing discipline, adding capture groups changes the type of the match. This is because it creates a tuple of n+1 substrings, in being the number of capture groups. In our example, the type becomes Regex<(Substring, Substring, Substring?, Substring?)>. We see that groups numbered 2 and 3 are associated with the part that can be omitted, so it is given an Optional type indicated by a trailing ?. So although the match succeeds, some capture groups can still be nil.

The next example returns the list of values as integers defaulting to 1 when no value is given.

In the resulting array of .matches(of:..), it is guaranteed that each match is not nil, but this is not the case for$0.3. Using the nil coalescing operator, the string"1" is returned when it is nil which is passed to the Int constructor to return an integer corresponding to this substring. But the Int constructor itself returns an Optional value; it might return nil if the substring does not correspond to the syntax of an integer. Here given that the substring contains only digits, it can be unwrapped unconditionally to get an integer value. This explains the use of the final ! operator.

Contrarily to other programming languages such as Python or Java, capture groups cannot be indexed by an integer variable. As Swift tuple components may be of different types, they must be accessed by a known subscript to allow to determine the type of the chosen component. In some cases, it is possible to use reflection tricks to transform a tuple into an array whose all elements must be of the same type and thus indexable by a variable.

Naming Capture Groups

Keeping track of group numbers is error-prone, especially when, during the development, adding or removing groups within a pattern. It is thus possible to assign names to groups by starting the group with ?<name> . This allows documenting the kind of values expected in the groups from the subject. We can thus give a more explicit version of the previous pattern as

in which group 1 is given the name key and group 3 the name value. This change is also reflected in the type of the match: Regex<(Substring, key: Substring, Substring?, value: Substring?)> in which some fields have been given the name of the capture group.

As spacing around the equal sign is not relevant, we can ignore a group by starting it with ?: in the output but the parentheses are kept for delimiting the optional grouping. Our previous example becomes

which now has the type Regex<(Substring, key: Substring, value: Substring?)> ignoring the capture group starting with ?:. We can now rewrite our example of extracting the integer values of the subject with the following version easier to understand by using the name of the field (here value).

It is still possible to use indexing (i.e here use $0.2 instead $0.value) but this defeats the purpose of defining names for captured groups.

Matching Capture Groups

A capture group can also be used within the same regular expression to match a repetition of a previous match. Technically, this does not fit the theoretical definition of a regular expression, but this is sometimes useful. The reference is obtained by using the \n pattern where n is the number of the group.

For example, /([a-z]+)\1/ matches a substring of consecutive repeated lowercase letters such as abcabc. For named capture groups, reusing a previous match is achieved with \k<name>. So the previous example, could have been written as /(?<x>[a-z])+\k<x>/.

To illustrate the use of matching captured string, we develop a pattern for removing XML tags from a subject. We first recall the main rules for XML tags, see this document for more details. We do not advocate using regular expressions to parse XML, but this is an interesting pedagogical exercise.

An XML tag is an NCNAME inside angle brackets. NCNAME (name without a colon) is an identifier starting with a letter or an underscore, possibly followed by a list of letters, digits, underscores, hyphens or periods, corresponding to the /[a-zA-Z_][-a-zA-Z0-9_.]*/ expression literal (note that the hyphen at the start and the period in the character class are not considered as special characters).

There are three types of XML tags:

We show a regular expression that matches an XML tags, skipping attributes in start-tag, but capturing the content between corresponding start-tags and end-tags. As this expression is quite involved, we define it using an extended regex literal which allows commenting subtleties.

In this expression, we take for granted that no nested XML tags of the same name exist. This case will be dealt later in the document.

We can use this to remove XML tags from a string. This example shows how we keep only the content group of the first two tags; we remove the third empty tag, but the last tags do not match because their start-tag and end-tag names are different.

In this section, we have shown how to achieve in Swift what regular expressions can do in other programming languages. Now we describe a way of writing regular expressions that sets Swift apart and allows many variations and combinations while keeping the strong typing discipline.

RegexBuilder

In addition to the Regex literal notation shown in the previous section, Swift provides an alternative notation based on the overloaded Regex constructor which accepts different kinds of parameters. Most often it is a closure (written as a trailing closure) that returns a RegexComponent created with the Result Builder notation of Swift. See this document for an introduction to this original notion for Domain Specific Languages (DSL) similar to SwiftUI code. This provides a more readable and type-safe notation for regular expressions and it also allows a systematic composition of regular expressions.

A RegexBuilder expression combines simple strings and other regexes by concatenation combined with components such as CharacterClass, LookAhead or ChoiceOf and quantifiers such as Optionally or ZeroOrMore.

To use this notation, the RegexBuilder module must be imported. The regex of an identifier can now be rewritten as

which is more verbose, but more readable and maintainable. Now identifierRB, whose type is Regex<Substring> can be used as a any regular expression literal.

Xcode provides a refactoring tool to transform a Regex literal into a RegexBuilder expression. Because RegexBuilder allows constructions that cannot be written as a literal string, an automatic tool to transform a RegexBuilder expression to a regex literal is not available.

Capturing Information

We now define an alternative regex for the example used in the named capture group example for parsing key-value pairs, values being optional. It will be built from simpler expressions. Named captures are obtained through Reference in this context and serve for subscripting the resulting match i.e., using square brackets. A reference is not a property name as it is the case for regex literals. A reference is a value created with the Reference constructor called with a type as parameter, hence the .self after the type name. One important feature is that the resulting value can be a transformation of the matched substring, which may be of a different type than a substring.

First the regex for the key with its captured reference whose type is Regex<(Substring, Substring)>:

The regex for the value to be transformed into an integer. In principle, the Int constructor could fail (but not in this case as the substring only contains digits. The type of vPatRB is Regex<(Substring, Int)>.

With these definitions, we can now build a key-value regex, which combines the pattern for capturing the key and optionally parses the equal sign with surrounding spacing and captures the value. Usually elements in a ResultBuilder are put on separate lines like Swift constructs but here we use semicolons to separate some of them on the same line for compactness. The type of kvPatRB is now Regex<(Substring, Substring, Int?)>. The trailing ? indicates that the integer value is optional.

The list of all values in the subject is obtained like the following (to be compared with the literal regex version). A check is made that the value part is present and if so its integer value is obtained by subscripting.

Note the subscripting within the match using the Reference variable value. The result does not have to be unwrapped because the Regex transform has already performed the conversion from the Substring to an Int.

If a capture is not given a name, its substring or value is referenced using indexing like an unnamed capture in a regex literal. Throwing an error from a transform closure aborts matching and propagates the error out to the caller, if this is not what is wanted TryCapture can be used as a transformation that can fail, where a nil result forces backtracking within the regex matching process.

To match a previously captured string within the same regular expression is only a matter of using the name of the captured value as a RegexComponent. Here is a version of our previous example of matching an XML tag using the RegexBuilder notation.

Line 2 defines a regex literal for the NCNAME which is used on line 9 where its value is captured. The captured value is used in the Regex on line 17. As a matched tag does not necessarily have content, the Reference on line 5 is marked as Optional. Because the two alternatives of ChoiceOf must have type String, a transform closure is used on line 16 to create a String from the captured value.

This regex can be used like this

We thus see that the RegexBuilder notation is more versatile, readable and compositional than the literal one.

Foundation Parsers

Swift’s regexes allow combining regular expressions with existing parsers for commonly occurring strings, such as URLs, locale-dependent numbers, dates and currencies. These are called Foundation parsers in the Swift terminology. These industrial strength parsers can be used like any other regular expression component and return properly typed values. Such specialized parsers, that are often error prone to develop, are more efficient than regular expression interpretation. The next section will show that these parsers are merely implementing a protocol that users can follow for implementing their own parsers.

Customizing Foundation Parsers

The API defines 6 methods to match different ways of writing a date and capturing it as a Date object. We choose one in which the format is specified by a string. It is also possible to match an ISO 8601-formatted date string. As each method follows the convention of a given locale, it is a very flexible tool.

The Swift interpolated string for the format: parameter declares the field names of the Date object followed by a writing specification (here .defaultDigits). It drives the matching process to create the date.

Matching a currency is specified by locale properties and the result can be specified either as an Integer, in which case the cents are ignored. To deal with cents, we must use a Decimal number which is a Swift structure representing a base-10 number with its own arithmetic operators.

Matching a url , which is a quite elaborate regex, creates a structure with the usual fields such as scheme, host, part, query...

Using Foundation Parsers

We now show an example of use of these parsers to process strings representing orders to an online store. The following string will be used as an example subject for a regular expression that combines Swift predefined parsers.The order contains a localized date and currency, using the French Canada locale, followed by a URL. These fields are separated by a colon with spacing around it.

The order for a computer screen was placed on February 19th, 2024 and costed $258.92 followed by a URL describing the item. The format of the date and the currency in the subject are written according to the writing convention for French in Canada.

The ordered item is a list of characters, while a separator is a colon with some spacing around it. Using a quantifier such as ZeroOrMore can sometimes lead to some inefficiencies because the regex engine might have to backtrack a few times with different starting points. But in many cases, such as in the separator here, once a separator has been matched, we are sure of the choice and we can avoid any backtracking over this choice by marking it Local.

In other regex formalisms, this is called an atomic or non-backtracking group indicated by ?> which is also allowed in Swift. This idea is similar to the FENCE pattern in SNOBOL4 or the cut ! in Prolog. For example, the regular expression a(bc|b)c (capturing group) matches abcc and abc, but a(?>bc|b)c matches abcc but not abc, because once it has matched bc, the remaining choice for b is lost because of the local marking. Such Local or atomic group does not create a capture and thus does not add a component to the type.

Matching the complete order is now only a matter of composing the previous regexes with embedded separators.

We now create an invoice from the captured values by matching the subject in the orderRB regex.

We define an English locale aware format for a Date object.

We want Decimal numbers displayed with 6 digits for the dollar part and two for the cents, but this format adds leading 0 and spaces. We define a function to format the value and use a regex (of course...) to replace leading 0 and spaces by spaces and add a dollar sign.

We can now apply the orderRB to the subject order and extract captures. The sales tax rate for Québec in computed and added as a Decimal number. A multi-line literal string in which the captured and computed values are then printed.

To produce the following in which the date (month/day/year) is now formatted according to the English locale.

Custom RegexComponent

We now show how to build a specialized matcher and use it like a regex. This approach relies on implementing the CustomConsumingRegexComponent protocol with the consuming function that receives a string, a starting index and bounds to work within. When the function considers it has a match, it returns a pair whose first value is the index following the end of the match, the second value being the matched substring. The function returns nil when no match is found. This is the protocol implemented by Foundation Parsers used in the previous section.

Matching Balanced Parentheses

For illustrating a custom regex component, we define a matcher for a well parenthesized expression, similar to the predefined BAL pattern in SNOBOL4. This is a classical example of a pattern that cannot be written using a formal regular expression. Some programming languages allow the definition of recursive regular expressions, but Swift does not; we cannot refer to a regex within itself.

In this function, when the match begins with an open parenthesis, the level variable is set to 1. The function iterates over the characters of the string decrementing level when a close parenthesis is encountered and incrementing when an open parenthesis is seen. A match is found as soon as level reaches 0 and a pair is returned containing the index of the next character and the substring between the start and current indices. Should this match fails, the global regex engine calls it at another starting position.

Here are some tests returning a list of the balanced parenthesized substrings within a subject.

Matching Nested XML Tags

The next example is a custom RegexComponent for matching nested XML tags, building on our previous example, but defining them as RegexBuilder expressions instead of literals.

With these definitions, we can define a custom RegexComponent that uses a global stack (line 1) for keeping track of open tags with their starting index. When an end tag of the same name as the one on the top of the stack is encountered, it returns a match containing the portion of the subject between the starting position and the end of the current match. An empty tag is considered as balanced. In the case of nested XML tags, both inner and outer tags are matched. Errors are raised for badly nested tags or for a < not followed by a tag.

This function can be called as follows to print all balanced XML elements within a multi-line string containing nested XML tags.

 

Run-time Regular Expressions

In the previous sections, the regular expression was defined in Swift code, which allowed its static typing. However, in some cases, a regular expression must be created from a string provided by the user, read from a file, or generated on the fly. This is how regexes are defined in most other programming languages, including ones advocating a strong typing discipline, such as Java. In these cases, the regular expression syntax can be checked when it is compiled, which occurs at run-time, though. Swift also allows this mode of definition of regexes.

To create a regular expression from a String, we use the Regex constructor with a string as parameter. When it is a string literal, such as "a(bc|de)f|ghi" or "\".*?\"", care must be taken to escape special characters such as in the second expression where the quotes that delimit standard Swift strings must be matched. To reduce the number of characters that need to be escaped, extended string delimiters can be used. The second example can thus be written as #"".*?""#.

But the creation of a regex from an arbitrary string may raise an error in the case of a malformed pattern. It is thus necessary to embed the call to the Regex constructor within a try block. If we are confident the regex is well formed, then using try! creates a regex that can be used directly, such as the following:

These regular expressions can be used with the String methods .contains(..) or .startsWith(..) which return a boolean result (see Line 2 in the following example).

The result of a matching method for a run-time regex is an object of the erased type AnyRegexOutput (see Line 3) in a way similar to the result of a regex match in other programming languages such as Java or Python.

The match result gives access to the range of the whole match, but its output property is an array of Elements corresponding to the capture groups. Subscripting (e.g. [n]) is used to retrieve the value of a capture group. This differs from the index (e.g. .n) used for accessing fields of a tuple in the case of statically typed regexes. We can use output[0] to get information about the whole match or output[n] to get information about the capture group n. The count property indicates the number of capture groups plus 1.

Each element of the output property has the substring property to get the match substring of this group, range for its range and name to get the name of the capture group, nil if it does not have a name. Here are a few examples of access to the result of matching a run-time regex.

Specifying the Expected Type

Whenever possible, the generic parameters for the Regex constructor should be specified as in the following examples so that the compiler can detect some potential errors at compile time. In the following examples, we could have used literal regexes in which case, the compiler would have inferred the appropriate types, but we use literal strings instead of string variables in the constructor calls for simplification.

Line 1 shows a case where the regular expression matches a substring. The type of the expression in line 2 is a pair: the parentheses, used to change the priority of alternation over concatenation, also create a typed capture group that is combined with the substring for the whole match. The second type of the pair is marked as Optional because the group appears on the left of the alternation with ghi. In line 3, the group is marked as not captured by prefixing it with ?:, so its type is not added in the signature; ?: groups should be specified when a given group is not needed as it simplifies the type.

It is also possible to specify the type as a parameter of the Regex constructor, so the following examples are equivalent to the preceding ones. Note the use of .self to refer to the type.

As we have specified that the type of the result of the match is a substring, then using the result of the match is similar to what we have shown in the previous sections for literal regexes and regex created by calls to the RegexBuilder. The result of the call to firstMatch is still an optional because the subject might not have an occurrence that matches the pattern, this explain unwrapping the result in line 1.

This section shows that although it is possible to use strings to define regular expressions, it is simpler and more reliable to use regex literals or Regexbuilder expressions whenever this is possible because type checking occurs when the program is compiled and not at run-time. Moreover, access to the components of the match is simpler when the types are specified.

Using Run-Time Regexes

We now illustrate a use of a run-time regex in a struct for replacing French words with their corresponding English word. This struct is initialized with a dictionary (line 5) and a regex which is an alternation joining keys of the dictionary (line 7) separated by | , As we want to match complete words, the alternation must be enclosed by word boundaries \b (line 8). As the alternation is the only expression in this regex, it is not necessary to capture the result, so (?:…) is used. Note the use of the raw string notation between #" and "# which avoids escaping backslashes. But then to use string interpolation, we need to use \#(…) instead of \(…). As the built regex always has the same form, we can specify its type Regex<Susstring> when it is declared (line 3) and does not need to be repeated when the regex is created (line 8).

The replacing(in:...) method (lines 11-13) replaces matches of created regex on a string by the corresponding value of the original dictionary.

Line 16 builds a struct from a simple dictionary of french and English words. Line 17 makes the replacement of the words of the dictionary within a string. Note that jour is not replaced because is does not occur at the word boundary.

Use Cases

This section presents some compelling uses of Swift regexes. First a simple example of parsing Roman numerals using either a regex literal or a RegexBuilder expression. Then a more elaborated example of a tokenizer that combines literal regexes within a RegexBuilder expression. Finally we show how dynamic regexes can be created from a JSON file for building the core of an ELIZA-like chatbot. The complete source file of these examples are available on the companion web site.

Parse a Roman Numeral

To illustrate the use of Swift regex in a real-world scenario, we will now demonstrate a regular expression that can be used to convert a Roman numeral string into its decimal equivalent. Roman numerals use letters to represent numbers: M:1000, D:500, C:100, L:50, X:10, V:5 and I:1. Up to three letters can appear following another to add their value to the previous one. If a lower-valued unit appears before a higher-valued one, it is deducted.

With a regex literal

Roman numerals in the range [0,4000) can be matched using the following regex muli-line literal which applies four optional regexes on the string. The first regex (line 2) matches one to three M, while regexes for hundred, tens and units follow the pattern of line 5 replacing I, V, X by X, L, C and by C, D and M respectively. This pattern is an alternative between:

The regexes on lines 3 to 5 could have been also written in the form (I|II|III|IV|V|VI|VII|VIII|IX)? but this would entail more backtracking, unless the regex compiler is very clever. We prefer writing the tree-based form which seems very clear anyway.

These spans of letters correspond to values to be added to determine the overall value:

With these definitions, the value of a string corresponding to a roman numeral can be obtained with the following function that matches the whole string (line 2). The values of each captured string that appears to the result (lines 5-8) are then added. Note the use of the optional captured groups that must be unwrapped; the value used as a key for the romanVals dictionary must also be unwrapped. When the whole string cannot be matched, the string does not correspond to a valid roman numeral (lines 11,12).

Note that it is not possible to use an integer index for the captures because the result of the match is a tuple. So we deal with them separately.

With a RegexBuilder Expression

We now illustrate how this approach to Roman numeral parsing can be implemented using a RegexBuilder. As the regex for units, tens and hundreds follow the same pattern, we use a function to define a pattern parametrized by strings for the unit, the five and the ten. It returns (line 7) the integer (already unwrapped in the transformation) corresponding to the parsed string.

makeRB is used to define the regex for all components.

This function can be used to parse a string and return the corresponding value by adding the returned value by each optional regex when it exists, 0 otherwise (line 4). Compare this with parseRomanRE above.

Developing a Tokenizer

We will now demonstrate how to create a tokenizer that classifies substrings using regular expressions. This is typically the first stage of compiling, but it can also be used in other text-processing applications. This tokenizer is a Swift implementation of an example from the Python documentation, but in this case, we combine literal regexes with a RegexBuilder expression to get the best of both worlds.

Defining a Token

A Token is a structure (lines 13-19) with many alternatives (kinds) defined by an enum with associated values (lines 1-11) to classify each span of text. In some cases (ID, KEYWORD, OP), the text span is kept with the token; if it is a NUMBER, it is converted to a numeric value. The line and column positions of the start of the token (line 15) are also saved, which is useful for error messages or for languages that take indentation into account. Line 16 specifies the format for displaying a Token: the kind followed by line and column numbers within square brackets.

Creating a Pattern

The association of a portion between a matched substring by a regex and a Token can be defined using the following Swift construct.

To simplify the notation, we define pat, a function to create such associations. It has two parameters: a regex and a closure to define the kind of token to create. lineNumber is a global variable (line 1) maintained by the tokenizing process and colPos is function (lines 5-8) giving the starting position of the matched substring in the subject. pat (lines 10-15) returns a Capture with the matched substring and the transformed Token. Since the closure will be executed after the pat function returns, it must be annotated with @escaping (more information about this annotation).

Using the Patterns

We can now construct a RegexBuilder expression for each token type, with each line being a call to pat with a literal regex as first parameter and a trailing closure returning the appropriate kind of the token depending on the matched substring s. Line 2 demonstrates the conversion of the string into a numeric value. The substring matched by line 5 can either be an ID or a KEYWORD, depending on whether it appears in the set of predefined keywords (line 14). An underscore is given as a parameter to the closure when the value of the matched string is not needed. If none of the first seven patterns match, it returns a MISMATCH.

These pairs of regex and token kinds could be extended to include other tokenization features, such as end-of-line comments, literal strings, parentheses, brackets, braces, etc.

The inferred type of tokenSpecifications is

a tuple containing the matched substring, followed by eight optional TokenS, only one of which is non-nil because the ChoiceOf stops as soon as it finds a match, so properly ordering the pat calls is important. The access to the components of the resulting tuple is done with a number between 1 and 8, as indicated in the comments following each pat call. As contrarily to Python, Swift does not give access to the last match number, all possibilities will have to be checked.

Defining the Tokenizer

A tokenizer is typically invoked by a parsing routine that iteratively handles each token using a method that generates a new token on every invocation. This pattern is comparable to the IteratorProtocol which requires the definition of a next() method.

Here is a Swift Tokenizer structure taking a program string (line 5) to build a list of numbered lines kept as a list of pairs. It stores the current line in a property and updates it using the static nextLine method (lines 37-42). This method creates a new string from the first element of lines and removes it from the list. This ensures that the column numbers are relative to the beginning of the current line rather than of the entire program. The line number is included in the returned value.

The next() function (lines 12-35) first checks whether the current line is empty. If it is not, it retrieves the subsequent line. Otherwise, it returns nil. At line 20, the beginning of the line is identified, the outcome of the match is stored, and the matched substring is erased from the start of the line. If the result is MISMATCH (line 23), an error message is displayed, indicating the incorrect substring (in this case, a single character) along with the line and column numbers. The function next() is then called recursively (line 21) to search for a real token. If the result is either NEWLINE or SKIP (line 27), the function ignores it by calling next(). In all other instances (line 30), the transformed token is returned (line 23).

Running the Tokenizer

The code below demonstrates how to use this tokenizer. First an instance of a Tokenizer is created (line 8) with the program statements. The tokenizer is used on lines 9-11 to print each returned tokens, but it could be seamlessly integrated into a more complex program. Lines 14-23 show an excerpt of the output.

Eliza-like Chatbot

Eliza in 1966 was one of the first program that allowed a conversation between a human and a computer. Once a pattern with placeholders is detected in the user input, an output is chosen within a predefined list of sentences in which some parts are filled with information extracted from the placeholders. Although the original implementation did not use regular expressions , Eliza-like programs have since been developed which make extensive use of regexes. The source code gives a Swift implementation of a JavaScript version that creates run-time regexes from patterns given in JSON.

This section explains the heart of the matching process used by this program. Here, you will find two rules (lines 1-10) within a series of matched pairs, presented in a JSON-like format:

Lines 10–20 of the code below define a rule that creates a closure matching the user input and returning the responses with the appropriate captured groups substituted.

The trigger’s transformation is only performed when this function is applied, not every time the created closure is used. If the trigger contains an @ (lines 5,6), a list of synonyms is extracted by looking at the synons dictionary with the key taken from the word following the ampersand. The word and its synonyms are separated by vertical bars for alternation. This list is enclosed in parentheses, forming a capture group at a word boundary. The stars in the trigger are then replaced (line 8) by a capture group matching reluctantly many characters. A case-insensitive trigger regular expression is created from this transformed string (line 9).

The function returned receives a user input that is compared to the trigger regular expression. When a match is found (line 12), a response is selected where the captured strings from the user input (lines 14-16) replace the numbers in parentheses. It is important to note that the match output component selection (line 15) is done using an index because the regex on line 14 is of type Regex<(Substring,SubString)> ; in contrast, on line 16, the component is obtained through subscripting, because triggerRE (line 11) is of type Regex<AnyRegexOutput>. In the end (line 17), sequences of one or more spaces are replaced by a single space. The function returns nil when the input does not match the trigger regex (line 19).

The following first three lines create a list of replies from the rules given above. The types of trigger and responses must be explicitly stated with as! because the type of rules is [[Any]]. In Swift, all elements of an array must be of the same type, but here the first element is a string and the second, an array of strings. We could have created a tuple, but in the application the rules are extracted from JSON, which does not allow tuples.

The chat function, which spans lines 5 to 12, scans the user input and returns the first non-empty response. It asks to continue if no rule has returned a result (line 11). Line 14 launches chat on three strings. Lines 15-17 demonstrate one potential outcome of this call.

 

Conclusion

This document has described some original aspects of Swift regular expressions and provided illustrative instances of their use. Although it does not aim to be comprehensive, it should offer enough understanding for users to further investigate the Swift API. It also showcased three full-fledged examples: translating Roman numerals, breaking down a string into tokens and a pattern-matching based chat box. These examples demonstrate the cutting-edge features of Swift’s regex abilities.

I hope that this text will be just as helpful to the reader as it was for me while writing it.

Appendix

Further reading

Swift API

Source Files

Source files for the examples in this document

Showing Matches

In the main.swift source file, we have defined the function showMatches(of patternS:String, in subject:String) which highlights with up arrows the matched characters within a string by a regular expression given as a String. It takes for granted that the subject is a single line. Here are a few examples of calls.

It is also possible to call this overloaded function by giving it a RegexComponent, but in this case the string corresponding to the regular expression cannot be printed. This is the core function called by the preceding one.

We found these functions useful for learning and debugging purposes.

Showing Capture Groups

It can be useful for debugging to get the list of substrings matched by capture groups of a matching result, but the standard way that Swift prints substrings is difficult to interpret. The following function can be used to get a list of strings for the capture groups in the result match of a run-time regex.

getGroups can also be used for a typed regex match result by converting it by calling the getGroups(Regex.Match(...)) . The Regex.match constructor creates type erased Match object.

Swift Regex Cheat Sheets

Methods

OperationMethod
Check for a occurrenceString.contains(Regex)->Bool
String.starts(with:Regex)->Bool
Find a matchString.firstMatch(of:Regex)->Regex.Match?
String.wholeMatch(of:Regex)->Regex.Match?
String.prefixMatch(of:Regex)->Regex.Match?
Regex.firstMatch(in:String)->Regex.Match?
Regex.wholeMatch(in:String)->Regex.Match?
Regex.prefixMatch(in:String)->Regex.Match?
Find all matchesString.matches(of:Regex)->[Regex.Match]
Replace a matchString.replacing(Regex, with:String)
String.replacing(Regex){Closure}
Change a string with a matchString.replace(Regex, with:String)
String.replace(Regex){Closure}
Find ranges of a matchString.firstRange(of:Regex)->Range?
String.ranges(of:Regex)->[Range]
Split a string with a regexString.split(separator:Regex)->[String]
Get a string after removing a prefixString.trimmingPrefix(Regex)->[String]
Remove prefix of stringString.trimPrefix(Regex)->Void

Frequently Used Metacharacters

Uppercase letters are the set inverse of the corresponding lowercase.

Escape sequenceMeaningCharacterClass
.any characterany
\b \Bword boundary 
\d \Ddigitdigit
\k<name>back reference a named capture 
\p{..} \P{}any character with a unicode property 
\s \Sany white space characterwhiteSpace
\w \Wany word characterword
[..]any character in the setanyOf(...)
^beginning of line 
$end of line 
\Nback reference a capture group by number N 
\quote one of the following characters
\ * ? + [ ( ) { } ^ $ *
 

Operators in Literal and Keyword in RegexBuilder

OperatorDescriptionRegexBuilder
|alternationChoiceOf
* *? *+match 0 or more times (eager, reluctant, possessive)ZeroOrMore
+ +? ++match 1 or more times (eager, reluctant, possessive)OneOrMore
?match 0 or 1Optionally
( )capture groupCapture
(?: )non-capture groupOne often omitted
(?<name>)named capture groupCapture(as:...)
(?= ...)match position before patternLookahead
(?!...)match position not before patternNegativeLookahead
(?>...)create a Local (atomic) group without captureLocal
{m,n}repeat previous match between m and n times, m is 0 and n is +∞ by defaultRepeat

Access to the properties of the Match Object (m)

Literal regex, RegexBuilder expression or typed run-time regex: a Tuple

OperatorDescriptionType
m.outputSubstring matchedSubstring
m.rangeInterval of string indices spanning the matchRange<String.Index>
m.countLength of matchInt
m.NNth capture groupSubstring
m.nameregex with named capture groupsSubstring

Untyped run-time regex (Regex<AnyRegexOutput>.Match) : an Array

OperatorDescriptionType
m.output[N].substringNth groupSubstring
m.output[name]?.substringregex with named capture groupSubstring
m.output.countnumber of capture groupsInt