本文正在参加「金石计划」

前言

在咱们日常的项目开发中,经常会碰到和正则表达式打交道的时分。比方用户暗码,通常会要求一起包含小写字母、大写字母、数字,并且长度不少于 8 位,以此来进步暗码的安全性。

在 Swift 中,咱们能够用正则表达式的字面量方式来进行完结。

Regex 字面量

Regex 字面量完结代码:

let regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/
let text = "Aa11111111"
print(text.matches(of: regex).first?.output) // Optional("Aa11111111")

经过上述代码能够看到,//经过两个斜线就能够来生成正则的字面量。用字面量的方式的确能够使代码很简洁,但简洁的代价便是很难看懂,对后边的代码维护也造成了很大的困难。

就像网上盛传的一句梗一样:“我有一个问题,所以我写了一个正则表达式。现在,我有了两个问题。”

关于 Regex 难懂且难维护的问题,Swift 的开发团队给出的计划便是:RegexBuilder。

RegexBuilder – 像写代码一样写正则

假设咱们有一个字符串”name: John Appleseed, user_id: 100″,想要提取其中user_id的值。
首要第一步,先导入 RegexBuilder:

import RegexBuilder

接着,经过结构体 Regex 来构建正则句子:

let regex = Regex {
    "user_id:" // 1
    OneOrMore(.whitespace) // 2
    Capture(.localizedInteger(locale: Locale(identifier: "zh-CN"))) // 3
}

第一行代码匹配的是固定字符串:”user_id”,第二行代码匹配的是一个或许多个空格,第三行代码则是匹配的整型数字。

localizedInteger 会将匹配到的数字主动转为整型,比方下面的比如:

let input = "user_id:  100.11"
let regex = Regex {
    Capture(.localizedInteger(locale: Locale(identifier: "zh-CN")))
}
if let match = input.firstMatch(of: regex) {
    print("Matched: \(match.0)") // Matched:  100.11
    print("User ID: \(match.1)") // User ID: 100
}

虽然匹配的是 100.11,但输出的仍然是 100。

最终,就能够经过 macth 的相关函数来进行数据提取了:

if let match = input.firstMatch(of: regex) {
    print("Matched: \(match.0)")
    print("User ID: \(match.1)")
}

RegexRepetitionBehavior

该结构体是用来定义匹配的重复行为的,它有三个值:

  • edger:会尽或许多的去匹配输入的字符,必要的时分会回溯。默以为edger
  • reluctant:会尽或许少的去匹配输入的字符,它会依据你的需求来一点点增大匹配区域,以完结匹配。
  • possessive:会尽或许多的去匹配输入的字符,不会回溯。

比方下面这个比如:

let testSuiteTestInputs = [    "2022-06-06 09:41:00.001",    "2022-06-06 09:41:00.001.",    "2022-06-06 09:41:00.001."]
let regex = Regex {
    Capture(OneOrMore(.any))
    Optionally(".")
}
for line in testSuiteTestInputs {
    if let (dateTime) = line.wholeMatch(of: regex)?.output {
        print("Matched: \(dateTime)\"")
    }
}

由于这三条数据最终的.是不一定有的,所以咱们的正则有一个 Optionally(".")。但匹配出来的 dateTime 还是会带 .。由于 edger 会匹配一切的字符包含最终的点在内,这样 Optionally(".") 根本不会起作用。

改成 Capture(OneOrMore(.any, .reluctant))则会修正这个问题。由于 reluctant 它是匹配尽或许少的输入,所以最终的Optionally(".")会执行。

在 Swift 5.7 中,Foundation 结构也对 RegexBuilder 进行适配。所以关于 Date、URL等类型,咱们能够凭借 Foundation 的强壮功用来进行解析。

Foundation 的支持

假如,咱们在做一个金融相关的 APP,为了兼容一些老数据,需求将一些字符串类型的数据转为结构体。

这是咱们的字符串数据:

let statement = """
CREDIT    2022/03/03    张三     2,000,000.00
DEBIT     03/03/2022    Tom      $2,000,000.00
DEBIT

这是咱们需求转的结构体:

struct Trade {
    let type: String
    let date: Date
    let name: String
    let count: Decimal
}

下面这个便是咱们需求编写的 Regex:

let regex = Regex {
    Capture {
        /CREDIT|DEBIT/
    }
    OneOrMore(.whitespace)
    Capture {
        One(.date(.numeric, locale: Locale(identifier: "zh_CN"), timeZone: .gmt))
    }
    OneOrMore(.whitespace)
    Capture {
        OneOrMore(.word)
    }
    OneOrMore(.whitespace)
    Capture {
        One(.localizedCurrency(code: "CNY", locale: Locale(identifier: "zh_CN")))
    }
}

首要,咱们需求匹配固定的字符串:CREDIT/DEBIT,接着是匹配一个或许多个空格。

接下来便是 Foundation 的重头戏了,关于日期类型的字符串,咱们并不需求写一些匹配年月日规矩的正则,只需求凭借 Foundation 内嵌的功用即可。这样做不仅省去了咱们自己编写的时间,更重要的是:官方写的要比咱们自己写的更能保证代码的正确性。

需求留意的是,Apple 引荐咱们显式的写出 locale 属性,而不是下面这种跟随体系写法 :

One(.date(.numeric, locale: Locale.current, timeZone: TimeZone.current))

由于这种写法会带来多种预期,并不能保证数据的确定性。

匹配完日期,接着便是对空格和用户名的匹配。最终,是对买卖金额的匹配,金额也是 Foundation 供给的函数来进行的匹配。

测试代码:

let result = statement.matches(of: regex)
var trades = [Trade]()
result.forEach { match in
    let (_, type, date, name, count) = match.output
    trades.append(Trade(type: String(type), date: date, name: String(name), count: count))
}
print(trades) 
// [SwiftDemo.Trade(type: "CREDIT", date: 2022-03-03 00:00:00 +0000, name: "张三", count: 2000000), SwiftDemo.Trade(type: "DEBIT", date: 2022-03-05 00:00:00 +0000, name: "李三", count: 33.27)]

经过打印能够得知,输出的成果并不契合预期,漏掉了 Tom 那条数据。漏掉的原因能够经过代码一眼得知:由于对日期和金额咱们显式的指定了是中国的格局,明显03/03/2022 这种格局是不契合年月日的格局的。这也体现了显式指定格局的好处:便利排查问题。

咱们只要将日期格局转为年月日格局,再将 $ 转为 ¥ 即可让正则正确匹配。

首要,咱们需求依据 currency 来来回来正确的 Date 类型:

func pickStrategy(_ currency: Substring) -> Date.ParseStrategy {
  switch currency {
  case "$": return .date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt)
  case "": return .date(.numeric, locale: Locale(identifier: "zh_CN"), timeZone: .gmt)
  default: fatalError("We found another one!")
  }
}

接着,编写正则表达式来获取相应的字符串字段:

let regex1 = #/
  (?<date>     \d{2} / \d{2} / \d{4})
  (?<name>   \P{currencySymbol}+)
  (?<currency> \p{currencySymbol})
/#

注:#//#格局为 Swift 中运行时正则表达式的格局。

最终,再调用 replace 函数来进行契合正则的字符替换:

statement.replace(regex1) { match -> String in
    print(match.currency)
    let date = try! Date(String(match.date), strategy: pickStrategy(match.currency))
    // ISO 8601, it's the only way to be sure
    let newDate = date.formatted(.iso8601.year().month().day())
    return newDate + match.name + ""
  }
statement = statement.replacingOccurrences(of: "-", with: "/")

这样,咱们就能解析出契合咱们需求的 Trade 类型的数据了。

总结

  • RegexBuilder 会使代码愈加易读易维护
  • RegexRepetitionBehavior 的三个值的差异
  • 尽或许多的使用 Foundation 供给的函数来解析数据
  • 使用 Foundation 时要指定格局解析数据,这样能够保证数据的唯一性

参考链接

  • 用户暗码正则表达式来源
  • RegexBuilder