// Print examples used in "Regular Expressions in Swift" // // main.swift // RegexInSwift // // Created by Guy Lapalme on 2024-04-25. // // import Foundation import RegexBuilder print("** 1. What is a regex") let è = "e\u{300}" print(è.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""# showMatches(of: "abc|def|ghi", in: "labc ghk ghi def ccc") showMatches(of:"a(bc|de)f|ghi", in: #"abc abcf ghi def adef"#) showMatches(of:#"a(bc|de)f|ghi"#,in: #"abc "abcf" "gh"i"#) showMatches(of: "\".*\"", in: hello) showMatches(of:#"".*?""#, in: hello) showMatches(of:#"".*+""#, in: hello) showMatches(of:#""[^"]*+""#, in: hello) print("** 2. Regex in Swift") print("*** 2.1 Swift Regex Definition") let exprRB = Regex { ChoiceOf { Regex { "a" ChoiceOf { "bc" ; "de" } "f" } "ghi" } } let quoteRB = Regex { /"/ ZeroOrMore(.reluctant) { /./ } /"/ } let quoteEL = #/ " # open quote .*? # up to next quote " # end quote /# showMatches(of: quoteEL, in: hello) print("*** 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) !" print("*** 2.2.1 Checking for an Occurrence of a Pattern") print(subject.contains(identifier)) print("123 + 456".contains(identifier)) print(subject.starts(with:identifier)) print("123 + 456".starts(with:identifier)) print("*** 2.2.2 Looking in a Subject for Matches of a Pattern") print(subject.firstMatch(of: identifier)?.output ?? "no match!") print(subject.firstMatch(of: identifier)?.count ?? "no match!") print(subject.wholeMatch(of: identifier)?.output ?? "no match!") print(subject.prefixMatch(of: identifier)?.output ?? "no match!") print(subject.matches(of: identifier).map{m in m.output.uppercased()}) showMatches(of: identifier, in: subject) print("** 2.2.3 Given a pattern, finding matches in a subject") print(try identifier.firstMatch(in: subject)?.output ?? "no match!") print(try identifier.firstMatch(in: subject)?.count ?? "no match!") print(try identifier.wholeMatch(in: subject)?.output ?? "no match!") print(try identifier.prefixMatch(in: subject)?.output ?? "no match!") print("*** 2.2.4 Replacing the match in the subject") print(subject.replacing(identifier, with: "*id*")) print(subject.replacing(identifier, with: {m in return "[\(m.output)]"})) print(subject.replacing(identifier){"{\($0.output)}"}) var subjectV = subject print(subjectV.replace(identifier){"#\($0.output)#"}) print(subjectV) print("*** 2.2.5 Determining the Ranges of Matches") print(subject.ranges(of: identifier).map{rng in subject[rng]}) print("*** 2.2.6 Splitting a String According to a Pattern") print(subject.split(separator: /\W+/)) print(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 var result=[Substring]() var pos = self.startIndex for rng in self.ranges(of:separator) { if rng.lowerBound != pos { // add substring before the separator result.append(self[pos..\p{alpha}\w*)(\s*=\s*(?\d+))?/ print(type(of: kvPatN_)) let kvPatN = /(?\p{alpha}\w*)(?:\s*=\s*(?\d+))?/ print(type(of: kvPatN)) print(keyValues.matches(of: kvPatN).map{Int($0.value ?? "1")!}) print(keyValues.matches(of: kvPatN).map{Int($0.2 ?? "1")!}) // *** Matching Capture Groups print(" *** 2.3.2 Matching Capture Groups") let test = /(?[a-z]+)\k/ print("wxabcdabcd34".contains(test)) print("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 print("xml_tag") print(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*(?:.*?)(?:\/>|>(.*?))"#) print("xml_tagS") print(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 /# print("xml_tagX") print(xml_string.replacing(xml_tagX){$0.content ?? ""}) print("xml_tagX - repeated") print((xml_string+" ab").replacing(xml_tagX){$0.content ?? ""}) print("** 3 RegexBuilder") print("*** 3.1 Capturing Information") let identifierRB = Regex { CharacterClass(("A"..."Z"),("a"..."z")) ZeroOrMore(.word) } print(subject.firstMatch(of: identifierRB)?.output ?? "no match!") let key = Reference(Substring.self) let kPatRB = Regex { Capture(as: key) { identifierRB // /[A-Za-z]\w*/ } } print(type(of: kPatRB)) let value = Reference(Int.self) let vPatRB = Regex { Capture(as: value) { OneOrMore(.digit) } transform: {Int($0)!} } print(type(of: vPatRB)) let kvPatRB = Regex { kPatRB Optionally { ZeroOrMore(.whitespace); /=/; ZeroOrMore(.whitespace) vPatRB } } print(type(of: kvPatRB)) print(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>)/ print(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 start-tag /.*?/ Capture (as: content) { /.*?/ } transform: {$0} Regex{""} // end-tag } } } print(xml_string.replacing(xml_tag_RB){$0[content] ?? ""}) // RegexComponent print("** 3.2 Foundation Parsers") print("**** 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) } } print("**** 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")) } } print("**** Matching a URL") let link = Reference(URL.self) let linkRB = Regex {Capture (as:link){ // URL.ParseStrategy.url .url() }} print("*** 3.2.2 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" print("**** Separating fields") let sep = Regex{ Local {ZeroOrMore(.horizontalWhitespace); ":"; ZeroOrMore(.horizontalWhitespace)} } print("**** 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 } print("**** 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(14.975) / 100 // rate of Québec sales tax let total = m_price + taxes print(""" 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!) """) print("** 3.3 Custom RegexComponent") print("*** 3.3.1 Matching Balanced Parentheses") // example adapted from // https://developer.apple.com/videos/play/wwdc2022/110358 (18:50) // 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() print("(2+(3+4)) )((())".matches(of: bal).map{"\($0.output)"}) print(" (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' } } } ")" } print(" (2+(3+4)())+( abc (1+2) ".matches(of: balRB).map{"\($0.output)"}) print("*** 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 {"" } print("Within:",xml_string) print("start-tags:",xml_string.matches(of: xml_start_tag).map{$0[tag_name]}) print("empty-tags:",xml_string.matches(of: xml_empty_tag).map{$0[tag_name]}) print("end-tags:",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()).forEach{print("\($0.output)")} print("** 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(#"".*?""#) print(subj1.contains(exprS)) print(subj1.firstMatch(of: exprS) ?? "no match") // printing hard to decipher! should use getGroups(…) defined below print(subj1.firstMatch(of: exprS)?.range ?? "no range") print(subj1.firstMatch(of: exprS)?.count ?? "no count") print(subj1.firstMatch(of: exprS)?.output[0].substring ?? "no output substring") print(subj1.firstMatch(of: exprS)?.output[0].range ?? "no output range") print(subj1.firstMatch(of: exprS)?.output[0].name ?? "no output name") print("*** 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) print(subj1.firstMatch(of: exprSt1)!.output) print(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"]) print(fr2en.replacing(in:"bonjour le monde joyeux et rarement triste")) print("** 5. Use Cases") print("*** 5.1 Parse a Roman Numeral") runRoman() print("*** 5.2 Developing a Tokenizer") runTokenizer() print("*** 5.3 Eliza-Like Chatbot") runElizaExamples() print("=======================") //chat() print("** Showing Matches") // =============================================================================== // function to highlight parts of the subject that are matched func showMatches(of patternS:String,in subject:String){ let pattern = try! Regex(patternS) let rngs = subject.ranges(of: pattern) let nb = rngs.count print("Matching /\(patternS)/ \(nb == 1 ? "once" : nb == 2 ? "twice" : "\(nb) times")") showMatches(of: pattern,in: subject,rngs: rngs) } func showMatches(of pattern:any RegexComponent,in subject:String,rngs:[Range]? = nil){ print("> \(subject)") let rngs = rngs ?? subject.ranges(of: pattern) var positions = "" if rngs.count == 0 { positions = "** no match **" } else { var lastPos = subject.startIndex for rng in rngs { if rng.lowerBound != lastPos { positions += String(repeating: " ", count: subject.distance(from: lastPos, to: rng.lowerBound)) } positions += String(repeating: "↑", count: subject.distance(from: rng.lowerBound, to: rng.upperBound)) lastPos = rng.upperBound } } print("> \(positions)") } print("*** 7.5 Showing Capture Groups") // =============================================================================== // Function to return a list of Strings corresponding to the capture groups // in the Match of a run-time regex // can also be used for a typed regex with a call of the form // getGroups(Regex.Match(...)) which creates type erased Match object func getGroups(_ output:Regex.Match)->[String]{ return( 0 ..< output.count).map{"\(output[$0].substring!)"} } print(#"** getGroups of first match of run-time regex "a(bc|de)f|ghi" in "\#(subj1)""#) print(getGroups(subj1.firstMatch(of: exprS)!)) print(#"** getGroups of first match of regex literal /([A-Za-z]\w*)(\s*=\s*(\d+))?/ in "\#(keyValues)""#) print(getGroups(Regex.Match(keyValues.firstMatch(of: kvPat)!)))