Regular Expression Decoder

A decoder that constructs objects from regular expression matches.
For more information about creating your own custom decoders,
consult Chapter 7 of the
Flight School Guide to Swift Codable.
For more information about using regular expressions in Swift,
check out Chapter 6 of the
Flight School Guide to Swift Strings.
Requirements
- Swift 5+
- iOS 11+ or macOS 10.13+
Usage
import RegularExpressionDecoder
let ticker = """
AAPL 170.69▲0.51
GOOG 1122.57▲2.41
AMZN 1621.48▼18.52
MSFT 106.57=0.00
SWIFT 5.0.0▲1.0.0
"""
let pattern: RegularExpressionPattern<Stock, Stock.CodingKeys> = #"""
\b
(?<\#(.symbol)>[A-Z]{1,4}) \s+
(?<\#(.price)>\d{1,}\.\d{2}) \s*
(?<\#(.sign)>([▲▼=])
(?<\#(.change)>\d{1,}\.\d{2})
\b
"""#
let decoder = try RegularExpressionDecoder<Stock>(
pattern: pattern,
options: .allowCommentsAndWhitespace
)
try decoder.decode([Stock].self, from: ticker)
// Decodes [AAPL, GOOG, AMZN, MSFT] (but not SWIFT, which is invalid)
Explanation
Let’s say that you’re building an app that parses stock quotes
from a text-based stream of price changes.
let ticker = """
AAPL 170.69▲0.51
GOOG 1122.57▲2.41
AMZN 1621.48▼18.52
MSFT 106.57=0.00
"""
Each stock is represented by the following structure:
- The symbol, consisting of 1 to 4 uppercase letters, followed by a space
- The price, formatted as a number with 2 decimal places
- A sign, indicating a price gain (
▲
), loss (▼
), or no change (=
)
- The magnitude of the gain or loss, formatted the same as the price
These format constraints lend themselves naturally
to representation by a regular expression,
such as:
/\b[A-Z]{1,4} \d{1,}\.\d{2}[▲▼=]\d{1,}\.\d{2}\b/
Note:
The \b
metacharacter anchors matches to word boundaries.
This regular expression can distinguish between
valid and invalid stock quotes.
"AAPL 170.69▲0.51" // valid
"SWIFT 5.0.0▲1.0.0" // invalid
However, to extract individual components from a quote
(symbol, price, etc.)
the regular expression must contain capture groups,
of which there are two varieties:
positional capture groups and
named capture groups.
Positional capture groups are demarcated in the pattern
by enclosing parentheses ((___)
).
With some slight modifications,
we can make original regular expression capture each part of the stock quote:
/\b([A-Z]{1,4}) (\d{1,}\.\d{2})([▲▼=])(\d{1,}\.\d{2})\b/
When matched,
the symbol can be accessed by the first capture group,
the price by the second,
and so on.
For large numbers of capture groups —
especially in patterns with nested groups —
one can easily lose track of which parts correspond to which positions.
So another approach is to assign names to capture groups,
which are denoted by the syntax (?<NAME>___)
.
/\b
(?<symbol>[A-Z]{1,4}) \s+
(?<price>\d{1,}\.\d{2}) \s*
(?<sign>([▲▼=])
(?<change>\d{1,}\.\d{2})
\b/
Note:
Most regular expression engines —
including the one used by NSRegularExpression
—
provide a mode to ignore whitespace;
this lets you segment long patterns over multiple lines,
making them easier to read and understand.
Theoretically, this approach allows you to access each group by name
for each match of the regular expression.
In practice, doing this in Swift can be inconvenient,
as it requires you to interact with cumbersome NSRegularExpression
APIs
and somehow incorporate it into your model layer.
RegularExpressionDecoder
provides a convenient solution
to constructing Decodable
objects from regular expression matches
by automatically matching coding keys to capture group names.
And it can do so safely,
thanks to the new ExpressibleByStringInterpolation
protocol in Swift 5.
To understand how,
let’s start by considering the following Stock
model,
which adopts the Decodable
protocol:
struct Stock: Decodable {
let symbol: String
var price: Double
enum Sign: String, Decodable {
case gain = "▲"
case unchanged = "="
case loss = "▼"
}
private var sign: Sign
private var change: Double = 0.0
var movement: Double {
switch sign {
case .gain: return +change
case .unchanged: return 0.0
case .loss: return -change
}
}
}
So far, so good.
Now, normally, the Swift compiler
automatically synthesizes conformance to Decodable
,
including a nested CodingKeys
type.
But in order to make this next part work correctly,
we’ll have to do this ourselves:
extension Stock {
enum CodingKeys: String, CodingKey {
case symbol
case price
case sign
case change
}
}
Here’s where things get really interesting:
remember our regular expression with named capture patterns from before?
We can replace the hard-coded names
with interpolations of the Stock
type’s coding keys.
import RegularExpressionDecoder
let pattern: RegularExpressionPattern<Stock, Stock.CodingKeys> = #"""
\b
(?<\#(.symbol)>[A-Z]{1,4}) \s+
(?<\#(.price)>\d{1,}\.\d{2}) \s*
(?<\#(.sign)>[▲▼=])
(?<\#(.change)>\d{1,}\.\d{2})
\b
"""#
Note:
This example benefits greatly from another new feature in Swift 5:
raw string literals.
Those octothorps (#
) at the start and end
tell the compiler to ignore escape characters (\
)
unless they also include an octothorp (\#( )
).
Using raw string literals,
we can write regular expression metacharacters like \b
, \d
, and \s
without double escaping them (i.e. \\b
).
Thanks to ExpressibleByStringInterpolation
,
we can restrict interpolation segments to only accept those coding keys,
thereby ensuring a direct 1:1 match between capture groups
and their decoded properties.
And not only that —
this approach lets us to verify that keys have valid regex-friendly names
and aren’t captured more than once.
It’s enormously powerful,
allowing code to be incredibly expressive
without compromising safety or performance.
When all is said and done,
RegularExpressionDecoder
lets you decode types
from a string according to a regular expression pattern
much the same as you might from JSON or a property list
using a decoder:
let decoder = try RegularExpressionDecoder<Stock>(
pattern: pattern,
options: .allowCommentsAndWhitespace
)
try decoder.decode([Stock].self, from: ticker)
// Decodes [AAPL, GOOG, AMZN, MSFT]
License
MIT
Mattt (@mattt)
Regular Expression Decoder
A decoder that constructs objects from regular expression matches.
For more information about creating your own custom decoders, consult Chapter 7 of the Flight School Guide to Swift Codable. For more information about using regular expressions in Swift, check out Chapter 6 of the Flight School Guide to Swift Strings.
Requirements
Usage
Explanation
Let’s say that you’re building an app that parses stock quotes from a text-based stream of price changes.
Each stock is represented by the following structure:
▲
), loss (▼
), or no change (=
)These format constraints lend themselves naturally to representation by a regular expression, such as:
This regular expression can distinguish between valid and invalid stock quotes.
However, to extract individual components from a quote (symbol, price, etc.) the regular expression must contain capture groups, of which there are two varieties: positional capture groups and named capture groups.
Positional capture groups are demarcated in the pattern by enclosing parentheses (
(___)
). With some slight modifications, we can make original regular expression capture each part of the stock quote:When matched, the symbol can be accessed by the first capture group, the price by the second, and so on.
For large numbers of capture groups — especially in patterns with nested groups — one can easily lose track of which parts correspond to which positions. So another approach is to assign names to capture groups, which are denoted by the syntax
(?<NAME>___)
.Theoretically, this approach allows you to access each group by name for each match of the regular expression. In practice, doing this in Swift can be inconvenient, as it requires you to interact with cumbersome
NSRegularExpression
APIs and somehow incorporate it into your model layer.RegularExpressionDecoder
provides a convenient solution to constructingDecodable
objects from regular expression matches by automatically matching coding keys to capture group names. And it can do so safely, thanks to the newExpressibleByStringInterpolation
protocol in Swift 5.To understand how, let’s start by considering the following
Stock
model, which adopts theDecodable
protocol:So far, so good.
Now, normally, the Swift compiler automatically synthesizes conformance to
Decodable
, including a nestedCodingKeys
type. But in order to make this next part work correctly, we’ll have to do this ourselves:Here’s where things get really interesting: remember our regular expression with named capture patterns from before? We can replace the hard-coded names with interpolations of the
Stock
type’s coding keys.Thanks to
ExpressibleByStringInterpolation
, we can restrict interpolation segments to only accept those coding keys, thereby ensuring a direct 1:1 match between capture groups and their decoded properties. And not only that — this approach lets us to verify that keys have valid regex-friendly names and aren’t captured more than once. It’s enormously powerful, allowing code to be incredibly expressive without compromising safety or performance.When all is said and done,
RegularExpressionDecoder
lets you decode types from a string according to a regular expression pattern much the same as you might from JSON or a property list using a decoder:License
MIT
Contact
Mattt (@mattt)