iOS NSRegularExpression with conditional piece of template - ios

I'm trying to format a phone number to the format (###) ###-####.
I'm using the following regex with replacement template
let regex = try NSRegularExpression(pattern: "([0-9]{3})([0-9]{1,3})([0-9]{0,4})")
regex.stringByReplacingMatches(
in: rawNumber,
options: .reportCompletion,
range: NSRange(location: 0, length: rawNumber.characters.count),
withTemplate: "($1) $2-$3"
)
The problem is that my template string includes the hardcoded - which should not appear if the third capture group $3 isn't found.
For example:
rawNumber = "5125"
would be replaced as (512) 5- when I actually want it in the format (512) 5, because I don't want the - to be shown unless the third capture group was found.
For example I was hoping there might be a way to make a template as something like:
"($1) $2if$3{-}$3"

Without getting fancy, I used the approach of alternate templates in a ternary conditional operator:
let rawNumber = "512512345"
let regex = try NSRegularExpression(pattern: "([0-9]{3})([0-9]{1,3})([0-9]{0,4})")
regex.stringByReplacingMatches(
in: rawNumber,
options: .reportCompletion,
range: NSRange(location: 0, length: rawNumber.characters.count),
withTemplate: "\(rawNumber.count > 6 ? "($1) $2-$3" : "($1) $2")"
)
Result for rawNumber = "5125" is "(512) 5" while rawNumber = "512512345" is "(512) 512-345".
EDIT: Swift 4.2 (and placing result into a constant)
let rawNumber = "512512345"
if let formattedString =
try? NSRegularExpression(pattern: "([0-9]{3})([0-9]{1,3})([0-9]{0,4})", options: []).stringByReplacingMatches(in: rawNumber, options: .reportCompletion, range: NSRange(location: 0, length: rawNumber.count), withTemplate: "\(rawNumber.count > 6 ? "($1) $2-$3" : "($1) $2")") {
print(formattedString)
}

You can write your own subclass of NSRegularExpression for conditional replacement:
class PhoneNumberConverter: NSRegularExpression {
override func replacementString(for result: NSTextCheckingResult, in string: String, offset: Int, template templ: String) -> String {
//Assuming `pattern` has always 3 captures
if result.rangeAt(3).length == 0 {
//$3 isn't found
return super.replacementString(for: result, in: string, offset: offset, template: "($1) $2")
} else {
return super.replacementString(for: result, in: string, offset: offset, template: "($1) $2-$3")
}
}
}
func convertPhoneNumber(rawNumber: String) -> String {
let regex = try! PhoneNumberConverter(pattern: "([0-9]{3})([0-9]{1,3})([0-9]{0,4})")
return regex.stringByReplacingMatches(
in: rawNumber,
options: .reportCompletion,
range: NSRange(location: 0, length: rawNumber.characters.count),
withTemplate: "($1) $2-$3"
)
}
print(convertPhoneNumber(rawNumber: "5125")) //->(512) 5
print(convertPhoneNumber(rawNumber: "512512345")) //->(512) 512-345

Instead of stringByReplacingMatches, use matchesInString. This will give you the list of matches (there should be only one), which itself contains the list of the ranges for each capturing group.
You can then check which capturing group did actually match, and from there, use one template or the other.

It looks like there is no way to do this as all of the template string is interpreted as literal characters except for the capture groups.
https://developer.apple.com/reference/foundation/nsregularexpression:

Related

Swift regular expression for arabic decimals in arabic text

I have some Arabic text which has some decimals as well.
for example this text
"بِسۡمِ اللّٰہِ الرَّحۡمٰنِ الرَّحِیۡمِ ﴿۱﴾"
"وَاِذَا قِیۡلَ لَہُمۡ اٰمِنُوۡا کَمَاۤ اٰمَنَ النَّاسُ قَالُوۡۤا اَنُؤۡمِنُ کَمَاۤ اٰمَنَ السُّفَہَآءُ ؕ اَلَاۤ اِنَّہُمۡ ہُمُ السُّفَہَآءُ وَلٰکِنۡ لَّا یَعۡلَمُوۡنَ ﴿۱۴﴾"
This text has verse numbers as Arabic digits in the end.
I wanted to find out all the matches for the verse numbers in these verses.
In swift I am tring to use the regular expression but somehow i am not coming up with the correct regex.
Here is my code:
func getRegex() {
// unicode for the arabic digits
let regexStr = "[\u{0660}-\u{0669}]+"
//let regexStr = "[\\p{N}]+"
//let regexStr = "[۹۸۷۶۵۴۳۲۱۰]+"
do {
let regex = try NSRegularExpression(pattern: regexStr, options: .caseInsensitive)
let matches = regex.matches(in: self.arabicText, options: .anchored, range: NSRange(location: 0, length: self.arabicText.count))
print("Matches count : \(matches.count)")
} catch {
print(error)
}
}
Can somebody guide me on how I can get the matches for the Arabic digits in the example Arabic text?
The .anchored argument makes the pattern only match at the start of string, so you need to remove it.
Also, as your string is not ASCII, you need to use self.arabicText.utf16.count string property rather than accessing the self.arabicText.count directly.
So, you can use
let regexStr = "[۹۸۷۶۵۴۳۲۱۰]+"
and then
let matches = regex.matches(in: self.arabicText, options: [], range: NSRange(location: 0, length: self.arabicText.utf16.count))

Swift: Getting range of text that includes emojis [duplicate]

This question already has an answer here:
Swift Regex doesn't work
(1 answer)
Closed 5 years ago.
I'm trying to parse out "#mentions" from a user provided string. The regular expression itself seems to find them, but the range it provides is incorrect when emoji are present.
let text = "😂😘🙂 #joe "
let tagExpr = try? NSRegularExpression(pattern: "#\\S+")
tagExpr?.enumerateMatches(in: text, range: NSRange(location: 0, length: text.characters.count)) { tag, flags, pointer in
guard let tag = tag?.range else { return }
if let newRange = Range(tag, in: text) {
let replaced = text.replacingCharacters(in: newRange, with: "[email]")
print(replaced)
}
}
When running this
tag = (location: 7, length: 2)
And prints out
😂😘🙂 [email]oe
The expected result is
😂😘🙂 [email]
NSRegularExpression (and anything involving NSRange) operates on UTF16 counts / indexes. For that matter, NSString.count is the UTF16 count as well.
But in your code, you're telling NSRegularExpression to use a length of text.characters.count. This is the number of composed characters, not the UTF16 count. Your string "😂😘🙂 #joe " has 9 composed characters, but 12 UTF16 code units. So you're actually telling NSRegularExpression to only look at the first 9 UTF16 code units, which means it's ignoring the trailing "oe ".
The fix is to pass length: text.utf16.count.
let text = "😂😘🙂 #joe "
let tagExpr = try? NSRegularExpression(pattern: "#\\S+")
tagExpr?.enumerateMatches(in: text, range: NSRange(location: 0, length: text.utf16.count)) { tag, flags, pointer in
guard let tag = tag?.range else { return }
if let newRange = Range(tag, in: text) {
let replaced = text.replacingCharacters(in: newRange, with: "[email]")
print(replaced)
}
}

Swift Regex to allow only uppercase letters and numbers mixed

In my case, I need to Implement Regex for my UITextField. Here, my textfield should allow only uppercase with number mixed values.
For Example:
AI1234
ER3456
I used below one, but not working
^[A-Z0-9]{3}?$
This regex matches the pattern above
2 Uppercase characters followed by 4 numbers
^[A-Z]{2}\\d{4}
You can test it on https://regexr.com/
Edit:
let str = """
AI1234
ER3456
"""
let pattern = try? NSRegularExpression(pattern: "[A-Z]{2}\\d{4}", options: [])
let range = NSRange(location: 0, length: str.utf16.count)
let matches = pattern?.matches(in: str, options: [], range: range)
print(matches)

Using NSRegularExpression produces incorrect ranges when emoji are present [duplicate]

This question already has an answer here:
Swift Regex doesn't work
(1 answer)
Closed 5 years ago.
I'm trying to parse out "#mentions" from a user provided string. The regular expression itself seems to find them, but the range it provides is incorrect when emoji are present.
let text = "😂😘🙂 #joe "
let tagExpr = try? NSRegularExpression(pattern: "#\\S+")
tagExpr?.enumerateMatches(in: text, range: NSRange(location: 0, length: text.characters.count)) { tag, flags, pointer in
guard let tag = tag?.range else { return }
if let newRange = Range(tag, in: text) {
let replaced = text.replacingCharacters(in: newRange, with: "[email]")
print(replaced)
}
}
When running this
tag = (location: 7, length: 2)
And prints out
😂😘🙂 [email]oe
The expected result is
😂😘🙂 [email]
NSRegularExpression (and anything involving NSRange) operates on UTF16 counts / indexes. For that matter, NSString.count is the UTF16 count as well.
But in your code, you're telling NSRegularExpression to use a length of text.characters.count. This is the number of composed characters, not the UTF16 count. Your string "😂😘🙂 #joe " has 9 composed characters, but 12 UTF16 code units. So you're actually telling NSRegularExpression to only look at the first 9 UTF16 code units, which means it's ignoring the trailing "oe ".
The fix is to pass length: text.utf16.count.
let text = "😂😘🙂 #joe "
let tagExpr = try? NSRegularExpression(pattern: "#\\S+")
tagExpr?.enumerateMatches(in: text, range: NSRange(location: 0, length: text.utf16.count)) { tag, flags, pointer in
guard let tag = tag?.range else { return }
if let newRange = Range(tag, in: text) {
let replaced = text.replacingCharacters(in: newRange, with: "[email]")
print(replaced)
}
}

Add a string at the end of a string for a particular string inside a String

I have a big string that contains several strings like the following:
http://website.com/image.jpg and I want to add ?w=100 to each of them.
So far I thought about creating a regular expression where every time I would encounter that string, it would replace it by the same string with the ?w=100 added to it.
Here is an extension of String to replace the string:
public func stringByReplacingMatches(matchRegEx: String, withMatch: String) -> String {
do {
let regEx = try NSRegularExpression(pattern: matchRegEx, options: NSRegularExpressionOptions())
return regEx.stringByReplacingMatchesInString(self, options: NSMatchingOptions(), range: NSMakeRange(0, self.characters.count), withTemplate: withMatch)
} catch {
fatalError("Error in the regular expression: \"\(matchRegEx)\"")
}
}
And this is how I use it:
myString.stringByReplacingMatches("https:\\/\\/website\\.com\\/[\\w\\D]*.jpg", withMatch: "https:\\/\\/website\\.com\\/[\\w\\D]*.jpg?w=100"))
Problem, this is what I get after replacing the string:
https://website.com/[wD]*.jpg?w=100
How can I solve my problem?
You need to learn about capture groups:
myString.stringByReplacingMatches("(https:\\/\\/website\\.com\\/[\\w\\D]*.jpg)", withMatch: "$1?w=100")
Oh and don't forget to save the return value
let fixedString = myString.stringByReplacingMatches("(https:\\/\\/website\\.com\\/[\\w\\D]*.jpg)", withMatch: "$1?w=100")
myString will not change, but fixedString will have all the updates.
UPDATE
[\\w\\D]* matches any character. You need to craft a more specific regular expression.
For the input "https://website.com/x.jpg" "https://website.com/x.jpg", the regex \w works.
var myString = "\"https://website.com/x.jpg\" \"https://website.com/x.jpg\""
myString = myString.stringByReplacingMatches("(https:\\/\\/website\\.com\\/\\w*.jpg)", withMatch: "$1?w=100")
But this will not work for every possible URL.
You should call it as
myString.stringByReplacingMatches("https:\\/\\/website\\.com\\/[\\w\\D]*.jpg", withMatch: "$0?w=100"))
The template can use $0 to show the matching string. You should not repeat the matching regular expression there.
do {
let regex = try NSRegularExpression(pattern: "http:\\/\\/[a-z]*.[a-z]*\\/[a-z]*.[a-z]*", options: .CaseInsensitive)
var newString = regex.stringByReplacingMatchesInString(a, options: NSMatchingOptions(rawValue: 0), range: NSMakeRange(0, a.characters.count), withTemplate: "$0?w=100")
print(newString)
}

Resources