一个从正则表达式匹配构建对象的解码器。
有关创建自定义解码器的更多信息,请查阅《Flight School Guide to Swift Codable》的第 7 章。有关在 Swift 中使用正则表达式的更多信息,请查看《Flight School Guide to Swift Strings》的第 6 章。
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)
假设您正在构建一个应用程序,该应用程序从基于文本的价格变动流中解析股票报价。
let ticker = """
AAPL 170.69▲0.51
GOOG 1122.57▲2.41
AMZN 1621.48▼18.52
MSFT 106.57=0.00
"""
每只股票都由以下结构表示
▲
)、下跌 (▼
) 或无变化 (=
)这些格式约束自然适合用正则表达式表示,例如
/\b[A-Z]{1,4} \d{1,}\.\d{2}[▲▼=]\d{1,}\.\d{2}\b/
注意:
\b
元字符将匹配锚定到单词边界。
此正则表达式可以区分有效和无效的股票报价。
"AAPL 170.69▲0.51" // valid
"SWIFT 5.0.0▲1.0.0" // invalid
但是,要从报价中提取单个组件(代码、价格等),正则表达式必须包含捕获组,捕获组有两种类型:位置捕获组和命名捕获组。
位置捕获组在模式中由括起来的括号 ((___)
) 标明。经过一些小的修改,我们可以使原始正则表达式捕获股票报价的每个部分
/\b([A-Z]{1,4}) (\d{1,}\.\d{2})([▲▼=])(\d{1,}\.\d{2})\b/
匹配时,可以通过第一个捕获组访问代码,通过第二个捕获组访问价格,依此类推。
对于大量的捕获组——尤其是在具有嵌套组的模式中——人们很容易忘记哪些部分对应于哪些位置。因此,另一种方法是为捕获组分配名称,其语法为 (?<NAME>___)
。
/\b
(?<symbol>[A-Z]{1,4}) \s+
(?<price>\d{1,}\.\d{2}) \s*
(?<sign>([▲▼=])
(?<change>\d{1,}\.\d{2})
\b/
注意:大多数正则表达式引擎——包括
NSRegularExpression
使用的引擎——都提供一种忽略空格的模式;这允许您将长模式分段到多行,使其更易于阅读和理解。
从理论上讲,这种方法允许您为正则表达式的每个匹配项按名称访问每个组。但在实践中,在 Swift 中这样做可能很不方便,因为它需要您与繁琐的 NSRegularExpression
API 进行交互,并以某种方式将其合并到您的模型层中。
RegularExpressionDecoder
提供了一个方便的解决方案,可以通过自动将编码键与捕获组名称匹配,从正则表达式匹配中构造 Decodable
对象。由于 Swift 5 中新的 ExpressibleByStringInterpolation
协议,它可以安全地做到这一点。
要理解这一点,让我们首先考虑以下采用 Decodable
协议的 Stock
模型
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
}
}
}
到目前为止,一切都很好。
现在,通常,Swift 编译器会自动合成对 Decodable
的一致性,包括嵌套的 CodingKeys
类型。但是为了使下一部分能够正确运行,我们必须自己完成
extension Stock {
enum CodingKeys: String, CodingKey {
case symbol
case price
case sign
case change
}
}
这是真正有趣的地方:记住我们之前带有命名捕获模式的正则表达式吗?我们可以用 Stock
类型编码键的插值替换硬编码的名称。
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
"""#
注意:此示例受益于 Swift 5 中的另一个新功能:原始字符串文字。开头和结尾的八位字节 (
#
) 告诉编译器忽略转义字符 (\
),除非它们也包含八位字节 (\#( )
)。使用原始字符串文字,我们可以编写正则表达式元字符,例如\b
、\d
和\s
,而无需双重转义它们(即\\b
)。
由于 ExpressibleByStringInterpolation
,我们可以将插值段限制为仅接受这些编码键,从而确保捕获组与其解码属性之间的直接 1:1 匹配。不仅如此——这种方法还允许我们验证键是否具有有效的 regex 友好名称,并且不会被捕获多次。它非常强大,允许代码具有令人难以置信的表达能力,而不会影响安全性或性能。
总而言之,RegularExpressionDecoder
允许您根据正则表达式模式从字符串中解码类型,就像您可能使用解码器从 JSON 或属性列表解码类型一样
let decoder = try RegularExpressionDecoder<Stock>(
pattern: pattern,
options: .allowCommentsAndWhitespace
)
try decoder.decode([Stock].self, from: ticker)
// Decodes [AAPL, GOOG, AMZN, MSFT]
MIT
Mattt (@mattt)