// 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*(?:.*?)(?:\/>|>(.*?)\1>)"#)
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
\k>) # 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{"" ; tag_name ; ">"} // 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 {"" ; xml_tagN ; ">" }
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)!)))