Share
Embed
Meet Swift Regex
Meet Swift Regex : WWDC22セッション要約
登壇
https://www.youtube.com/watch?v=IpQAovAzwf8&t=3980s
集まれSwift好き!Swift愛好会スピンオフ WWDC22セッション要約会 @オンライン
https://love-swift.connpass.com/event/247317/
USAMI Kosuke
June 24, 2022
Transcript
19 slides
-
自己紹介 宇佐見公輔 / 株式会社ゆめみ iOSDC Japan 2022 で記事(x2 )を書き、トークをします。 来週6/27
(月)にも、YUMEMI.swift #15 でSwift Regex の話をします。 -
Scenario: Financial investigation 以下のような文字列データ(金融取引の記録)を処理したい。 取引の種類、取引日、個人または機関名、金額 各フィールドは2 つ以上のスペースまたはタブで区切られる。 KIND DATE INSTITUTION
AMOUNT ---------------------------------------------------------------- CREDIT 03/01/2022 Payroll from employer $200.23 CREDIT 03/03/2022 Suspect A $2,000,000.00 DEBIT 03/03/2022 Ted's Pet Rock Sanctuary $2,000,000.00 DEBIT 03/05/2022 Doug's Dugout Dogs $33.27 -
Processing collections split で分割してはどうか。 ["Doug\'s", "Dugout", "Dogs"] は意図しない分割。 「2 つ以上のスペースまたはタブ」で区切りたい。
そこで正規表現を使う。 ` ` let transaction = "DEBIT 03/05/2022 Doug's Dugout Dogs $33.27" let fragments = transaction.split(whereSeparator: \.isWhitespace) // ["DEBIT", "03/05/2022", "Doug\'s", "Dugout", "Dogs", "$33.27"] ` ` -
Processing strings Regex リテラルが使える split(by:) が追加された。 ` ` let transaction
= "DEBIT 03/05/2022 Doug's Dugout Dogs $33.27" let fragments = transaction.split(by: /\s{2,}|\t/) // ["DEBIT", "03/05/2022", "Doug's Dugout Dogs", "$33.27"] -
struct Regex // Regex リテラル let digits = /\d+/ //
実行時に生成 let runtimeString = #"\d+"# // 補足 : #" 〜"# はSwift の文字列リテラルの一種 let digits = try Regex(runtimeString) // Regex ビルダー import RegexBuilder let digits = OneOrMore(.digit) -
Create a Regex builder import RegexBuilder import Foundation let fieldSeparator
= /\s{2,}|\t/ let transactionMatcher = Regex { /CREDIT|DEBIT/ fieldSeparator One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt)) fieldSeparator OneOrMore { NegativeLookahead { fieldSeparator } CharacterClass.any } fieldSeparator One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US"))) } -
Capture let transactionMatcher = Regex { Capture { /CREDIT|DEBIT/ }
fieldSeparator Capture { One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt fieldSeparator Capture { OneOrMore { NegativeLookahead { fieldSeparator } CharacterClass.any } } fieldSeparator Capture { One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US")) } -
Capture Capture した部分は、次のように取り出せる。 let transaction = "DEBIT 03/05/2022 Doug's Dugout
Dogs $33.27" let match = transaction.wholeMatch(of: transactionMatcher) { let (kind, date, institution, amount) = match.output // ... } -
Plot twist 金額の単位に応じて、日付部分のパース処理を変えたい。 金額が $ の場合は month/day/year とみなす。 金額が £
の場合は day/month/year とみなす。 DEBIT 03/05/2022 Doug's Dugout Dogs $33.27 DEBIT 06/03/2022 Oxford Comma Supply Ltd. £57.33 ` ` ` ` ` ` ` ` -
Plot twist Foundation の Date パース時のstrategy 機能を活用する。 ` ` 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: "en_GB"), timeZone: .gmt) default: fatalError("We found another one!") } } -
Plot twist let regex = #/ (?<date> \d{2} / \d{2}
/ \d{4}) (?<middle> \P{currencySymbol}+) (?<currency> \p{currencySymbol}) /# ledger.replace(regex) { match -> String in let date = try! Date(String(match.date), strategy: pickStrategy(match.currency)) let newDate = date.formatted(.iso8601.year().month().day()) return newDate + match.middle + match.currency } -
Unicode switch (" 🧟♀️💖🧠", "The Brain Cafe\u{301}") { case (/.\N{SPARKLING
HEART}./, /.*café/.ignoresCase()): print("Oh no! 🧟♀️💖🧠, but 🧠💖☕️ !") default: print("No conflicts found") } -
TryCapture TryCapture でCapture したデータを変換できる。 let fieldSeparator = /\s{2,}|\t/ let field
= OneOrMore { NegativeLookahead { fieldSeparator } CharacterClass.any } let transactionMatcher = Regex { Capture { /CREDIT|DEBIT/ } fieldSeparator TryCapture(field) { timestamp ~= $0 ? $0 : nil } fieldSeparator TryCapture(field) { details ~= $0 ? $0 : nil } fieldSeparator // ... } -
Local 上述の正規表現は、セパレータの処理に無駄がある。 例えば、8 つの空白を 見つけた場合、次のようになる(グローバルバックトラッキング)。 8 つの空白の残りが正規表現にマッチするか調べる。 マッチしなければ、次に7 つの空白の残りがマッチするか調べる。 マッチしなければ、次に6
つの空白の残りがマッチするか調べる。 これを避けるため、 Local を使うことができる。 ` ` let fieldSeparator = Local { /\s{2,}|\t/ }