正则表达式解码器

Build Status License Swift Version

一个从正则表达式匹配构建对象的解码器。


有关创建自定义解码器的更多信息,请查阅《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)