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