// Playground with examples used in "Regular Expressions in Swift" import Foundation import RegexBuilder // ** 1. What is a regex let è = "e\u{300}" è.wholeMatch(of:/./) != nil // true because this is a single unicode char let expr = /a(bc|de)f|ghi/ let quote = /".*?"/ let hello = #""hello" my "friends""# // the following expressions should work, but XCode "dies" when asked to display the values // so we ask only the first match... // "labc ghk ghi def ccc".matches(of:expr) // #"abc abcf ghi def adef"#.matches(of:expr) // #"abc "abcf" "gh"i"#.matches(of:/"a(bc|de)f|ghi"/) try /abc|def|ghi/ .firstMatch(in: #"abc abcf ghi def adef"#) try /a(bc|de)f|ghi/.firstMatch(in: #"abc abcf ghi def adef"#) try /".*"/ .firstMatch(in: #""hello" my "friends""#) try /".*?"/ .firstMatch(in: #""hello" my "friends""#) try /".*="/ .firstMatch(in: #""hello" my "friends""#) try /"[^"]*+"/.firstMatch(in: #""hello" my "friends""#) // *** 2.2 Swift Regex Operations let identifier = /[A-Za-z]\w+/ let IDENTIFIER = /[A-Z]\w*/.ignoresCase() let subject = "Here are 10 tokens to be (matched) !" // *** 2.2.1 Checking for an Occurrence of a Pattern subject[subject.firstMatch(of: identifier)!.range] subject.contains(identifier) "123 + 456".contains(identifier) subject.starts(with:identifier) "123 + 456".starts(with:identifier) // *** 2.2.2 Looking in a Subject for Matches of a Pattern // **** 2.2.2.1 Finding a single match subject.firstMatch(of: identifier)?.output ?? "no match!" subject.firstMatch(of: identifier)?.count ?? 1 subject.wholeMatch(of: identifier)?.output ?? "no match!" subject.prefixMatch(of: identifier)?.output ?? "no match!" // **** 2.2.2.2 Finding all matches subject.matches(of: identifier).map{m in m.output.uppercased()} "12 + (a_bc*435)- 78".matches(of:/\w+|\d+|[-+*\/()]/).map{String($0.output)} // 2.2.3 Given a pattern, finding matches in a subject" try identifier.firstMatch(in: subject)?.output ?? "no match!" try identifier.firstMatch(in: subject)?.count ?? 1 try identifier.wholeMatch(in: subject)?.output ?? "no match!" try identifier.prefixMatch(in: subject)?.output ?? "no match!" // *** 2.2.4 Replacing the match in the subject subject.replacing(identifier, with: "*id*") subject.replacing(identifier, with: {m in return "[\(m.output)]"}) subject.replacing(identifier){"{\($0.output)}"} // *** 2.2.5 Determining the Ranges of Matches subject.ranges(of: identifier).map{rng in subject[rng]} var subjectV = subject subjectV.replace(identifier){"#\($0.output)#"} subjectV // *** 2.2.6 Splitting a String According to a Pattern subject.split(separator: /\W+/) subject.split(separator: /(?=\W+)/) // To keep separators one solution would be // /(?=\W+)|(?<=\W+)/ // But Lookbehind (?<=...) is not yet implemented // so we define the following extension extension String { func splitKeeping(separator:Regex)->[Substring]{ // adapté de https://stackoverflow.com/questions/41587416/split-string-by-components-and-keep-components-in-place let rngs = self.ranges(of:separator) var result=[Substring]() var pos = self.startIndex for rng in rngs { if rng.lowerBound != pos { result.append(self[pos..\p{alpha}\w*)(\s*=\s*(?\d+))?/ type(of: kvPatN_) let kvPatN = /(?\p{alpha}\w*)(?:\s*=\s*(?\d+))?/ type(of: kvPatN) keyValues.matches(of: kvPatN).map{Int($0.value ?? "1")!} keyValues.matches(of: kvPatN).map{Int($0.2 ?? "1")!} // *** 2.3.2 Matching Capture Groups let test = /(?[a-z]+)\k/ "wxabcdabcd34".contains(test) "abaaba".contains(test) let xml_string = "<_a>xx yy content" let xml_tag = /<([a-zA-Z_][-a-zA-Z_0-9.]*)\s*(?:.*?)(?:\/>|>(.*?)<\/\1>)/ // remove tags, keep only content xml_string.replacing(xml_tag){$0.2 ?? ""} let nc_nameS = "[a-zA-Z_][-a-zA-Z_0-9.]*" let xml_tagS:Regex = try! Regex<(Substring,Substring,Substring?)> (#"<(\#(nc_nameS))\s*(?:.*?)(?:\/>|>(.*?))"#) xml_string.replacing(xml_tagS){$0.2 ?? ""} let xml_tagX = #/ < # begin of start-tag (?[a-zA-Z_][-a-zA-Z_0-9.]*) # save name \s*(?:.*?) # skip attributes (?:/> # empty tag | > # end of start-tag (?.*?) # content >) # end-tag /# xml_string.replacing(xml_tagX){$0.content ?? ""} (xml_string+" ab").replacing(xml_tagX){$0.content ?? ""} // ** 3 RegexBuilder let identifierRB = Regex { // like /[A-Za-z]\w*/ CharacterClass(("A"..."Z"),("a"..."z")) ZeroOrMore(.word) // /[A-Za-z]\w*/ could also have been used instead of the lines above } subject.firstMatch(of: identifierRB)?.output ?? "no match!" // RegexBuilder // translation of /(?\p{alpha}\w*)(?:\s*=\s*(?\d+))?/ // but return an Int as value let key = Reference(Substring.self) let kPatRB = Regex<(Substring,Substring)> { Capture(as: key) { identifierRB } } let value = Reference(Int.self) let vPatRB: Regex<(Substring, Int)> = Regex { Capture(as: value) { OneOrMore(.digit) } transform: {Int($0)!} } let kvPatRB: Regex<(Substring, Substring, Int?)> = Regex { kPatRB Optionally { ZeroOrMore(.whitespace); /=/; ZeroOrMore(.whitespace) vPatRB } } keyValues.matches(of: kvPatRB).map{$0.2 != nil ? $0[value] : 1} let xml_tag_named = /<(?[a-zA-Z_][-a-zA-Z_0-9.]*)\s*(?.*?)(\/>|>(?.*?)<\/\1>)/ xml_string.replacing(xml_tag_named){$0.content ?? ""} // letter or underscore followed by letter, digit, underscore, hyphen or period let nc_name = /[a-zA-Z_][-a-zA-Z_0-9.]*/ let tag_name = Reference(Substring.self) let content = Reference(Substring?.self) let xml_tag_RB = Regex { "<" // begin of start-tag Capture (as: tag_name) {nc_name} /.*?/ // skip attributes ChoiceOf { "/>" // empty-element tag Regex { ">" // end of end-tag Capture (as: content) { /.*?/ } transform: {$0} Regex{""} // end-tag } } } xml_string.replacing(xml_tag_RB){$0[content] ?? ""} // ** 3.2 Foundation Parsers // *** 3.2.1 Customizing Foundation Parsers // **** Matching a Date let date = Reference(Date.self) let dateRB = Regex{ Capture (as:date){ // Date.ParseStrategy.date // Date.FormatStyle.Symbol.Day.defaultDigits .date(format:"\(day:.defaultDigits)/\(month:.defaultDigits)/\(year:.defaultDigits)", locale: Locale(identifier: "fr_CA"), timeZone:.current) } } // **** Matching a currency let price = Reference(Decimal.self) let priceRB = Regex { Capture (as:price){ // Decimal.FormatStyle.Currency.localizedCurrency .localizedCurrency(code: Locale.Currency("CAD"), locale: Locale(identifier: "fr_CA")) } } // **** Matching a URL let link = Reference(URL.self) let linkRB = Regex {Capture (as:link){ // URL.ParseStrategy.url .url() }} // *** Using Foundation Parsers let order = "19/2/2024 : Computer Screen : 258,92 $ : https://www.azamon.ca/gp/aw/d/B0CJVK87Y7/?ref_=sbx_be_s_sparkle_mcd_asin_1_img&pd_rd_w=dPFhO&content-id=amzn1.sym" // **** Separating fields // test of atomic/local groupings with tests coming from // https://www.regular-expressions.info/atomic.html let abc = /a(bc|b)c/ "abcc".matches(of: abc).map{m in m.output} "abc".matches(of: abc).map{m in m.output} let abcl = /a(?>bc|b)c/ "abcc".matches(of: abcl).map{m in m.output} "abc".matches(of: abcl).map{m in m.output} let sep = Regex{ Local {ZeroOrMore(.horizontalWhitespace); ":"; ZeroOrMore(.horizontalWhitespace)} } // **** Matching the Order let item = Reference(Substring.self) let itemRB = Regex {Capture (as:item){OneOrMore(.any)}} let orderRB = Regex{ dateRB ; sep ; itemRB ; sep ; priceRB ; sep ; linkRB } // **** Creating an Invoice let en_CA_date = Date.FormatStyle() .year() .day(.defaultDigits) .month(.defaultDigits) .locale(Locale(identifier: "en_CA")) let decimalFormat = Decimal.FormatStyle(locale: Locale(identifier: "fr_CA")) .precision(.integerAndFractionLength(integer: 6, fraction: 2)) func fmt(_ val:Decimal)->String{ val.formatted(decimalFormat).replacing(/^[\s0]*/){String(repeating:" ",count:$0.count)}+" $" } let m = order.firstMatch(of: orderRB)! let m_price = m[price] let taxes = m_price * Decimal(0.14975) let total = m_price + taxes // parentheses around the next literal string to avoir Xcode error message! // error: 'subscript(_:)' is only available in macOS 13.0 or newer; clients of '__lldb_expr_27' may have a lower deployment target (""" On \(m[date].formatted(en_CA_date)), you ordered a \(m[item]) Price: \(fmt(m_price)) Taxes: \(fmt(taxes)) Total: \(fmt(total)) Thank you \(m[link].host!) """) // ** 3.3 Custom RegexComponent // *** 3.3.1 Matching Balanced Parentheses // example adapted from // https://developer.apple.com/videos/play/wwdc2022/110358 (18m50s) // match a balanced parentheses string (e.g. BAL in SNOBOL4) struct BalancedParentheses: CustomConsumingRegexComponent { typealias RegexOutput = Substring func consuming(_ input: String, startingAt index: String.Index, in bounds: Range) throws -> (upperBound: String.Index, output: Substring)? { guard index < input.endIndex && input[index]=="(" else {return nil} var level=1 var pos = input.index(after: index) while pos != input.endIndex { if input[pos] == ")" { level -= 1 if level == 0 { return (input.index(after:pos),input[index ... pos]) } } else if input[pos] == "(" { level += 1 } pos = input.index(after:pos) } return nil } } let bal = BalancedParentheses() "(2+(3+4)) )((())".matches(of: bal).map{"\($0.output)"} " (2+(3+4)())+( abc (1+2) ".matches(of: bal).map{"\($0.output)"} // for matching well parenthetised substrings // https://www.regular-expressions.info/recurse.html // suggests this regex "\((?>[^()]|(?R))*\)" that could be translated as the following // but the recursive call to balRB is not allowed (commented compiler error message) // so this regex only matches one level balanced parentheses let balRB = Regex { "(" ZeroOrMore(.eager) { Local { ChoiceOf { /[^()]/ // balRB // Cannot reference invalid declaration 'balRB' } } } ")" } " (2+(3+4)())+( abc (1+2) ".matches(of: balRB).map{"\($0.output)"} // *** 3.3.2 Matching Nested XML Tags let xml_attr = Regex { let quotesym = Reference(Substring.self) /\s+/ nc_name /\s*=\s*/ Capture (as:quotesym) {/["']/} /.*?/ quotesym } let xml_tagN = Regex { Capture (as:tag_name){nc_name} Regex {ZeroOrMore {xml_attr} /\s*/ } } let xml_start_tag = Regex {"<" ; xml_tagN ; ">" } let xml_empty_tag = Regex {"<" ; xml_tagN ; "/>"} let xml_end_tag = Regex {"" } xml_string.matches(of: xml_start_tag).map{$0[tag_name]} xml_string.matches(of: xml_empty_tag).map{$0[tag_name]} xml_string.matches(of: xml_end_tag).map{$0[tag_name]} let doc = """ hello

info

nothing

test

a value spanning two lines goodfriends """ var tags = [(Substring,String.Index)]() // stack of (start-tag-name, string index of start) struct NestedXML: CustomConsumingRegexComponent { typealias RegexOutput = Substring func consuming(_ input: String, startingAt index: String.Index, in bounds: Range) throws -> (upperBound: String.Index, output: Substring)? { guard index < input.endIndex else {return nil} var pos = index // current position while let m = input[pos...].firstMatch(of: /(?><)/) { // skip to before the next < let start = m.range.lowerBound if let m = input[start...].prefixMatch(of: xml_end_tag){ // end-tag encountered if m[tag_name] == tags.last!.0 { // check if the tag-name is the same as the top of the stack let (_,tag_start) = tags.popLast()! // remove it return (m.range.upperBound,input[tag_start ..< m.range.upperBound]) } else { // should never happen (bad nesting of tags) fatalError("Bad XML: \(m[tag_name]) should match \(tags.last?.0 ?? "strange tag")") } } else if let m = input[start...].prefixMatch(of: xml_empty_tag){ // empty tag return (input.index(after: start),input[start ..< m.range.upperBound]) // return it } else if let m = input[start...].prefixMatch(of: xml_start_tag){ // start tag tags.append((m[tag_name],start)) // add it to the stack pos = input.index(start, offsetBy: m.0.count) // update position } else { // should never happen: < not followed by a tag fatalError("no match of tag: \(start) : \(input[start...])") } } return nil } } doc.matches(of: NestedXML()).map{"\($0.output)"} // ** 4 Run-time Regular Expressions // without type specification let subj1 = "labcf ghk ghi def ccc" let exprS = try! Regex("a(bc|de)f|ghi") let quoteES = try! Regex(#"".*?""#) subj1.contains(exprS) subj1.firstMatch(of: exprS) subj1.firstMatch(of: exprS)?.range subj1.firstMatch(of: exprS)?.count subj1.firstMatch(of: exprS)?.output[0].substring subj1.firstMatch(of: exprS)?.output[0].range subj1.firstMatch(of: exprS)?.output[0].name // ** 4.1 Specifying the Expected Type let quoteESt = try! Regex(#"".*?""#) let exprSt = try! Regex<(Substring,Substring?)>("a(bc|de)f|ghi") let exprSt1 = try! Regex("a(?:bc|de)f|ghi") let quoteESta = try! Regex(#"".*?""#,as:Substring.self) let exprSta = try! Regex("a(bc|de)f|ghi",as:(Substring,Substring?).self) let exprSt1a = try! Regex("a(?:bc|de)f|ghi",as: Substring.self) subj1.firstMatch(of: exprSt1)!.count subj1.matches(of: exprSt).map{"\($0.0)"} // ** 4.2 Some Uses of Run-Time Regexes struct WordsReplace{ let dict:[String:String] let dictRE:Regex init(_ dict:[String:String]){ self.dict = dict let reS = dict.keys.joined(separator: "|") dictRE = try! Regex(#"\b(?:\#(reS))\b"#) } func replacing(in str: String)->String { return str.replacing(dictRE, with: {dict[String($0.output)]!}) } } let fr2en = WordsReplace(["jour":"day", "monde":"world", "joyeux":"happy", "triste":"sad"]) fr2en.replacing(in:"bonjour le monde joyeux et rarement triste")