Swift replace substring regex - ios

I am attempting to use regular expression to replace all occurrences of UK car registrations within a string.
The following swift code works perfectly for a when the string matches the regex exactly as below.
var myString = "DD11 AAA"
var stringlength = countElements(myString)
var ierror: NSError?
var regex:NSRegularExpression = NSRegularExpression(pattern: "^([A-HK-PRSVWY][A-HJ-PR-Y])\\s?([0][2-9]|[1-9][0-9])\\s?[A-HJ-PR-Z]{3}$", options: NSRegularExpressionOptions.CaseInsensitive, error: &ierror)!
var modString = regex.stringByReplacingMatchesInString(myString, options: nil, range: NSMakeRange(0, stringlength), withTemplate: "XX")
print(modString)
The result is XX
However, the following does not work and the string is not modifed
var myString = "my car reg 1 - DD11 AAA my car reg 2 - AA22 BBB"
var stringlength = countElements(myString)
var ierror: NSError?
var regex:NSRegularExpression = NSRegularExpression(pattern: "^([A-HK-PRSVWY][A-HJ-PR-Y])\\s?([0][2-9]|[1-9][0-9])\\s?[A-HJ-PR-Z]{3}$", options: NSRegularExpressionOptions.CaseInsensitive, error: &ierror)!
var modString = regex.stringByReplacingMatchesInString(myString, options: nil, range: NSMakeRange(0, stringlength), withTemplate: "XX")
print(modString)
The result is my car reg 1 - DD11 AAA my car reg 2 - AA22 BBB
Can anyone give me any pointers?

You need to remove the ^ and $ anchors.
The ^ means start of string and $ means end of string (or line, depending on the options). That's why your first example works: in the first test string, the start of the string is really followed by your pattern and ends with it.
In the second test string, the pattern is found in the middle of the string, thus the ^... can't apply. If you would just remove the ^, the $ would apply on the second occurrence of the registration number and the output would be my car reg 1 - DD11 AAA my car reg 2 - XX.
let myString = "my car reg 1 - DD11 AAA my car reg 2 - AA22 BBB"
let regex = try! NSRegularExpression(pattern: "([A-HK-PRSVWY][A-HJ-PR-Y])\\s?([0][2-9]|[1-9][0-9])\\s?[A-HJ-PR-Z]{3}", options: NSRegularExpression.Options.caseInsensitive)
let range = NSMakeRange(0, myString.count)
let modString = regex.stringByReplacingMatches(in: myString, options: [], range: range, withTemplate: "XX")
print(modString)
// Output: "my car reg 1 - XX my car reg 2 - XX"

Let's use a class extension to wrap this up in Swift 3 syntax:
extension String {
mutating func removingRegexMatches(pattern: String, replaceWith: String = "") {
do {
let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let range = NSRange(location: 0, length: count)
self = regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith)
} catch { return }
}
}
var phoneNumber = "+1 07777777777"
phoneNumber.removingRegexMatches(pattern: "\\+\\d{1,4} (0)?")
Results in 7777777777 (thus removing country code from phone number)

Swift 4.2 Updated
let myString = "my car reg 1 - DD11 AAA my car reg 2 - AA22 BBB"
if let regex = try? NSRegularExpression(pattern: "([A-HK-PRSVWY][A-HJ-PR-Y])\\s?([0][2-9]|[1-9][0-9])\\s?[A-HJ-PR-Z]{3}", options: .caseInsensitive) {
let modString = regex.stringByReplacingMatches(in: myString, options: [], range: NSRange(location: 0, length: myString.count), withTemplate: "XX")
print(modString)
}

Update for Swift 2.1:
var myString = "my car reg 1 - DD11 AAA my car reg 2 - AA22 BBB"
if let regex = try? NSRegularExpression(pattern: "([A-HK-PRSVWY][A-HJ-PR-Y])\\s?([0][2-9]|[1-9][0-9])\\s?[A-HJ-PR-Z]{3}", options: .CaseInsensitive) {
let modString = regex.stringByReplacingMatchesInString(myString, options: .WithTransparentBounds, range: NSMakeRange(0, myString.characters.count), withTemplate: "XX")
print(modString)
}

Warning
Do not use NSRange(location: 0, length: myString.count) as all examples above quoted.
Use NSRange(myString.startIndex..., in: myString) instead!
.count will count newline characters like \r\n as one character - this may result in a shortened, thus invalid, NSRange that does not match the whole string.
(.length should work)

With pattern: "^ ... $" you have specified that the pattern is anchored
to the start and end of the string, in other words, the entire string
must match the pattern. Just remove ^ and $ from the pattern
and you'll get the expected result.

Simple extension:
extension String {
func replacingRegex(
matching pattern: String,
findingOptions: NSRegularExpression.Options = .caseInsensitive,
replacingOptions: NSRegularExpression.MatchingOptions = [],
with template: String
) throws -> String {
let regex = try NSRegularExpression(pattern: pattern, options: findingOptions)
let range = NSRange(startIndex..., in: self)
return regex.stringByReplacingMatches(in: self, options: replacingOptions, range: range, withTemplate: template)
}
}
βœ… Advantages to other answers
Exposed throwing error to the caller
Exposed finding options to the caller with default for the ease of use
Exposed replacing options to the caller with default for the ease of use
Fixed the range BUG 🐞 in the original answer

A notice to all answers that uses .count in their answers:
This will cause problems in cases that the operating target range has surrogate-paired characters.
Please fix your answers by using .utf16.count instead.
Here's Ryan Brodie 's answer with this fix. It works with Swift 5.5.
private extension String {
mutating func regReplace(pattern: String, replaceWith: String = "") {
do {
let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines])
let range = NSRange(location: 0, length: self.utf16.count)
self = regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith)
} catch { return }
}
}
Update: If considering #coyer 's concerns:
private extension String {
mutating func regReplace(pattern: String, replaceWith: String = "") {
do {
let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines])
let range = NSRange(self.startIndex..., in: self)
self = regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith)
} catch { return }
}
}
Also: to #Martin R' :
It is okay to use ^ and $ in Regex as long as you have enabled the ".anchorsMatchLines" in the Regex options. I already applied this option in the codeblocks above.

Related

How to remove specific characters or words from a string in swift?

var myString = "43321 This is example hahaha 4356-13"
And I want result that
var resultString = "This is example"
In other words, I want to erase certain words and numbers.
How is it possible?
I've already done a search, But I could not find exactly what I wanted.
Please answer me. Thanks
Okay, To be precise, I want to erase all numbers and erase only certain words I want.
I made a big mistake. I am so sorry. To be precise, it is correct to erase numbers and certain words. However, I should not always delete all digits, but keep them if number's are next to a string.
example)
let testString1 = "123123123123"
let testString2 = "apple 65456876"
let testString3 = "apple banana3 horse5"
let testString4 = "44 apple banana1banana 5horse"
let testString5 = "123 banana123 999"
And I want remove words "apple".
So result is
let resultString1 = ""
let resultString2 = ""
let resultString3 = "banana3 horse5"
let resultString4 = "banana1banana 5horse"
let resultString5 = "banana123"
You can try applying below code,
var sentence = "43321 This is example hahaha 4356-13"
sentence = (sentence.components(separatedBy: NSCharacterSet.decimalDigits) as NSArray).componentsJoined(by: "")
let wordToRemove = "hahaha"
if let range = sentence.range(of: wordToRemove) {
sentence.removeSubrange(range)
}
print(sentence) // This is example -
I think this will be faster with a regex solution:
//use NSMutableString so the regex.replaceMatches later will work
var myString:NSMutableString = "43321 This is example hahaha 4356-13"
//add more words to match by using | operator e.g. "[0-9]{1,}|apple|orange"
//[0-9]{1,} simply removes all numbers but is more efficient than [0-9]
let regexPattern = "[0-9]{1,}|apple"
//caseInsensitive or the regex will be much longer
let regex = try NSRegularExpression(pattern: regexPattern, options: .caseInsensitive)
var matches = regex.matches(in: myString as String, options: .withoutAnchoringBounds, range: range)
regex.replaceMatches(in: myString, options: .withoutAnchoringBounds, range: range, withTemplate: "")
print(myString) // This is example hahaha -
Subsequent Strings
var testString3: NSMutableString = "apple banana horse"
matches = regex.matches(in: testString3 as String, options: .withoutAnchoringBounds, range: range)
regex.replaceMatches(in: testString3, options: .withoutAnchoringBounds, range: range, withTemplate: "")
print(testString3) // banana horse
You can try this :
let myString = "43321 This is example hahaha 4356-13"
let stringToReplace = "43321"
let outputStr = myString.replacingOccurrences(of: stringToReplace, with: "")
print(outputStr.trimmingCharacters(in: NSCharacterSet.whitespaces))
//output: This is example hahaha 4356-13
Use this string extension .
extension String
{
func removeNumbersAString(str:String)->String
{
var aa = self.components(separatedBy: CharacterSet.decimalDigits).joined(separator: " ")
let regexp = " \\d* "
var present = self
while present.range(of:regexp, options: .regularExpression) != nil {
if let range = present.range(of:regexp, options: .regularExpression) {
let result = present.substring(with:range)
present = present.replacingOccurrences(of: result, with: "")
// print("before \(result)")
}
}
return present
}
}
A test
var str = "dsghdsghdghdhgsdghghdghds 12233 apple"
print("before \(str)") /// before dsghdsghdghdhgsdghghdghds 12233 apple
print("after \(str.removeNumbersAString(str: "apple"))") /// after dsghdsghdghdhgsdghghdghds
Try bellow code:
var myString = "43321 This is example hahaha 4356-13"
//to remove numbers
myString = (myString.components(separatedBy: NSCharacterSet.decimalDigits) as NSArray).componentsJoined(by: "")
// to remove specific word
if let range = myString.range(of: "hahaha") {
myString.removeSubrange(range)
}
print(myString)

How to replace a html tag with nsattribute in swift?

For eg. replace a <strong> tag with bold font. So that it can be applied as attributed text to label, textfield or textview
extension NSAttributedString {
func replaceHTMLTag(tag: String, withAttributes attributes: [String: AnyObject]) -> NSAttributedString {
let openTag = "<\(tag)>"
let closeTag = "</\(tag)>"
let resultingText: NSMutableAttributedString = self.mutableCopy() as! NSMutableAttributedString
while true {
let plainString = resultingText.string as NSString
let openTagRange = plainString.range(of: openTag)
if openTagRange.length == 0 {
break
}
let affectedLocation = openTagRange.location + openTagRange.length
let searchRange = NSRange(location: affectedLocation, length: plainString.length - affectedLocation)
let closeTagRange = plainString.range(of: closeTag, options: NSString.CompareOptions.init(rawValue: 0), range: searchRange)
resultingText.setAttributes(attributes, range: NSRange(location: affectedLocation, length: closeTagRange.location - affectedLocation))
resultingText.deleteCharacters(in: closeTagRange)
resultingText.deleteCharacters(in: openTagRange)
}
return resultingText as NSAttributedString
}
}
You need to remove the ^ and $ anchors.
The ^ means start of string and $ means end of string (or line, depending on the options). That's why your first example works: in the first test string, the start of the string is really followed by your pattern and ends with it.
In the second test string, the pattern is found in the middle of the string, thus the ^... can't apply. If you would just remove the ^, the $ would apply on the second occurrence of the registration number and the output would be my car reg 1 - DD11 AAA my car reg 2 - XX.
let myString = "my car reg 1 - DD11 AAA my car reg 2 - AA22 BBB"
let regex = try! NSRegularExpression(pattern: "([A-HK-PRSVWY][A-HJ-PR-Y])\\s?([0][2-9]|[1-9][0-9])\\s?[A-HJ-PR-Z]{3}", options: NSRegularExpressionOptions.CaseInsensitive)
let range = NSMakeRange(0, myString.characters.count)
let modString = regex.stringByReplacingMatchesInString(myString, options: [], range: range, withTemplate: "XX")
print(modString)
// Output: "my car reg 1 - XX my car reg 2 - XX"
Reference: https://stackoverflow.com/a/28503676/8169585
Try this extension. It will convert HTML string into NSAttributtedString. I also has the 0.75 ratio applied because of the fact that the default conversion increases all the font size
extension String {
func htmlAttributedString() -> NSAttributedString? {
guard let data = self.data(using: String.Encoding.utf16, allowLossyConversion: false) else { return nil }
guard let html = try? NSMutableAttributedString(
data: data,
options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
documentAttributes: nil) else { return nil }
html.beginEditing()
html.enumerateAttribute(NSFontAttributeName, in: NSMakeRange(0, html.length), options: .init(rawValue: 0)) {
(value, range, stop) in
if let font = value as? UIFont {
let resizedFont = font.withSize(font.pointSize * 0.75)
html.addAttribute(NSFontAttributeName,
value: resizedFont,
range: range)
}
}
html.endEditing()
return html
}
}

Remove percent-escaped characters from string

How do I remove, not decode, percent-escaped characters from a string using Swift. For instance:
"hello%20there"
should become
"hellothere"
EDIT:
I would like to replace multiple percent-escaped characters in a string. So:
"hello%20there%0Dperson"
should become
"hellothereperson"
let string = originalString.replacingOccurrences(of: "%[0-9a-fA-F]{2}",
with: "",
options: .regularExpression,
range: nil)
You can use the method "removingPercentEncoding"
let precentEncodedString = "hello%20there%0Dperson"
let decodedString = precentEncodedString.removingPercentEncoding ?? ""
You can use regex for that matching % followed by two numbers: %[0-9a-fA-F]{2}
let myString = "hello%20there%0D%24person"
let regex = try! NSRegularExpression(pattern: "%[0-9a-fA-F]{2}", options: [])
let range = NSMakeRange(0, myString.characters.count)
let modString = regex.stringByReplacingMatchesInString(myString,
options: [],
range: range,
withTemplate: "")
print(modString)
let input:String = "hello%20there%0Dperson"
guard let output = input.stringByRemovingPercentEncoding else{
NSLog("failed to remove percent encoding")
return
}
NSLog(output)
and the result is
hello there
person
then you can just remove the spaces
or you can remove it by regex
"%([0-9a-fA-F]{2})"

Searching for strings starting with \n\n in Swift

Hey I have a requirement to increase the spacing in my UILables for double spaced line breaks. I want to search my string and find all the strings starting with \n\n. For example "Hello world\nI am on the next line\n\nNow I'm on the next line and it's spaced more than before\nNow I'm back to normal spacing". I'm having trouble trying to figure out the regex for this. I am trying:
let regExRule = "^\n\n*"
and passing it into this function:
func matchesForRegexInText(regex: String, text: String) -> [String] {
do {
let regex = try NSRegularExpression(pattern: regex, options: [])
let nsString = text as NSString
let results = regex.matchesInString(text,
options: [], range: NSMakeRange(0, nsString.length))
return results.map { nsString.substringWithRange($0.range)}
} catch let error as NSError {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
However I am getting an empty array. Not really sure how to construct the regex pattern for this. Any pointers would be really appreciated. Thanks!
The primary issue I see is the regex pattern should include a capture group to select the multiple strings needed.
func matchesForRegexInText(regex : String, text: String) -> [String] {
var captured = [String]()
let exp = try! NSRegularExpression(pattern: regex, options: [])
let matches = exp.matchesInString(text, options:[], range: NSMakeRange(0, text.characters.count))
for match in matches {
let c = (text as NSString).substringWithRange(match.rangeAtIndex(1))
captured.append(c)
}
return captured
}
let re = "\\n\\n([\\w\\\\s,']+)"; // selection with (...)
// ["Alpha", "Bravo", "Charlie"]
let strResults = matchesForRegexInText(re, text: "\n\nAlpha\n\nBravo\n\nCharlie\n\n")

Swift extract regex matches

I want to extract substrings from a string that match a regex pattern.
So I'm looking for something like this:
func matchesForRegexInText(regex: String!, text: String!) -> [String] {
???
}
So this is what I have:
func matchesForRegexInText(regex: String!, text: String!) -> [String] {
var regex = NSRegularExpression(pattern: regex,
options: nil, error: nil)
var results = regex.matchesInString(text,
options: nil, range: NSMakeRange(0, countElements(text)))
as Array<NSTextCheckingResult>
/// ???
return ...
}
The problem is, that matchesInString delivers me an array of NSTextCheckingResult, where NSTextCheckingResult.range is of type NSRange.
NSRange is incompatible with Range<String.Index>, so it prevents me of using text.substringWithRange(...)
Any idea how to achieve this simple thing in swift without too many lines of code?
Even if the matchesInString() method takes a String as the first argument,
it works internally with NSString, and the range parameter must be given
using the NSString length and not as the Swift string length. Otherwise it will
fail for "extended grapheme clusters" such as "flags".
As of Swift 4 (Xcode 9), the Swift standard
library provides functions to convert between Range<String.Index>
and NSRange.
func matches(for regex: String, in text: String) -> [String] {
do {
let regex = try NSRegularExpression(pattern: regex)
let results = regex.matches(in: text,
range: NSRange(text.startIndex..., in: text))
return results.map {
String(text[Range($0.range, in: text)!])
}
} catch let error {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
Example:
let string = "πŸ‡©πŸ‡ͺ€4€9"
let matched = matches(for: "[0-9]", in: string)
print(matched)
// ["4", "9"]
Note: The forced unwrap Range($0.range, in: text)! is safe because
the NSRange refers to a substring of the given string text.
However, if you want to avoid it then use
return results.flatMap {
Range($0.range, in: text).map { String(text[$0]) }
}
instead.
(Older answer for Swift 3 and earlier:)
So you should convert the given Swift string to an NSString and then extract the
ranges. The result will be converted to a Swift string array automatically.
(The code for Swift 1.2 can be found in the edit history.)
Swift 2 (Xcode 7.3.1) :
func matchesForRegexInText(regex: String, text: String) -> [String] {
do {
let regex = try NSRegularExpression(pattern: regex, options: [])
let nsString = text as NSString
let results = regex.matchesInString(text,
options: [], range: NSMakeRange(0, nsString.length))
return results.map { nsString.substringWithRange($0.range)}
} catch let error as NSError {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
Example:
let string = "πŸ‡©πŸ‡ͺ€4€9"
let matches = matchesForRegexInText("[0-9]", text: string)
print(matches)
// ["4", "9"]
Swift 3 (Xcode 8)
func matches(for regex: String, in text: String) -> [String] {
do {
let regex = try NSRegularExpression(pattern: regex)
let nsString = text as NSString
let results = regex.matches(in: text, range: NSRange(location: 0, length: nsString.length))
return results.map { nsString.substring(with: $0.range)}
} catch let error {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
Example:
let string = "πŸ‡©πŸ‡ͺ€4€9"
let matched = matches(for: "[0-9]", in: string)
print(matched)
// ["4", "9"]
My answer builds on top of given answers but makes regex matching more robust by adding additional support:
Returns not only matches but returns also all capturing groups for each match (see examples below)
Instead of returning an empty array, this solution supports optional matches
Avoids do/catch by not printing to the console and makes use of the guard construct
Adds matchingStrings as an extension to String
Swift 4.2
//: Playground - noun: a place where people can play
import Foundation
extension String {
func matchingStrings(regex: String) -> [[String]] {
guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { return [] }
let nsString = self as NSString
let results = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))
return results.map { result in
(0..<result.numberOfRanges).map {
result.range(at: $0).location != NSNotFound
? nsString.substring(with: result.range(at: $0))
: ""
}
}
}
}
"prefix12 aaa3 prefix45".matchingStrings(regex: "fix([0-9])([0-9])")
// Prints: [["fix12", "1", "2"], ["fix45", "4", "5"]]
"prefix12".matchingStrings(regex: "(?:prefix)?([0-9]+)")
// Prints: [["prefix12", "12"]]
"12".matchingStrings(regex: "(?:prefix)?([0-9]+)")
// Prints: [["12", "12"]], other answers return an empty array here
// Safely accessing the capture of the first match (if any):
let number = "prefix12suffix".matchingStrings(regex: "fix([0-9]+)su").first?[1]
// Prints: Optional("12")
Swift 3
//: Playground - noun: a place where people can play
import Foundation
extension String {
func matchingStrings(regex: String) -> [[String]] {
guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { return [] }
let nsString = self as NSString
let results = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))
return results.map { result in
(0..<result.numberOfRanges).map {
result.rangeAt($0).location != NSNotFound
? nsString.substring(with: result.rangeAt($0))
: ""
}
}
}
}
"prefix12 aaa3 prefix45".matchingStrings(regex: "fix([0-9])([0-9])")
// Prints: [["fix12", "1", "2"], ["fix45", "4", "5"]]
"prefix12".matchingStrings(regex: "(?:prefix)?([0-9]+)")
// Prints: [["prefix12", "12"]]
"12".matchingStrings(regex: "(?:prefix)?([0-9]+)")
// Prints: [["12", "12"]], other answers return an empty array here
// Safely accessing the capture of the first match (if any):
let number = "prefix12suffix".matchingStrings(regex: "fix([0-9]+)su").first?[1]
// Prints: Optional("12")
Swift 2
extension String {
func matchingStrings(regex: String) -> [[String]] {
guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { return [] }
let nsString = self as NSString
let results = regex.matchesInString(self, options: [], range: NSMakeRange(0, nsString.length))
return results.map { result in
(0..<result.numberOfRanges).map {
result.rangeAtIndex($0).location != NSNotFound
? nsString.substringWithRange(result.rangeAtIndex($0))
: ""
}
}
}
}
The fastest way to return all matches and capture groups in Swift 5
extension String {
func match(_ regex: String) -> [[String]] {
let nsString = self as NSString
return (try? NSRegularExpression(pattern: regex, options: []))?.matches(in: self, options: [], range: NSMakeRange(0, nsString.length)).map { match in
(0..<match.numberOfRanges).map { match.range(at: $0).location == NSNotFound ? "" : nsString.substring(with: match.range(at: $0)) }
} ?? []
}
}
Returns a 2-dimentional array of strings:
"prefix12suffix fix1su".match("fix([0-9]+)su")
returns...
[["fix12su", "12"], ["fix1su", "1"]]
// First element of sub-array is the match
// All subsequent elements are the capture groups
If you want to extract substrings from a String, not just the position, (but the actual String including emojis). Then, the following maybe a simpler solution.
extension String {
func regex (pattern: String) -> [String] {
do {
let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpressionOptions(rawValue: 0))
let nsstr = self as NSString
let all = NSRange(location: 0, length: nsstr.length)
var matches : [String] = [String]()
regex.enumerateMatchesInString(self, options: NSMatchingOptions(rawValue: 0), range: all) {
(result : NSTextCheckingResult?, _, _) in
if let r = result {
let result = nsstr.substringWithRange(r.range) as String
matches.append(result)
}
}
return matches
} catch {
return [String]()
}
}
}
Example Usage:
"someText πŸ‘ΏπŸ…πŸ‘Ώβš½οΈ pig".regex("πŸ‘Ώβš½οΈ")
Will return the following:
["πŸ‘Ώβš½οΈ"]
Note using "\w+" may produce an unexpected ""
"someText πŸ‘ΏπŸ…πŸ‘Ώβš½οΈ pig".regex("\\w+")
Will return this String array
["someText", "️", "pig"]
I found that the accepted answer's solution unfortunately does not compile on Swift 3 for Linux. Here's a modified version, then, that does:
import Foundation
func matches(for regex: String, in text: String) -> [String] {
do {
let regex = try RegularExpression(pattern: regex, options: [])
let nsString = NSString(string: text)
let results = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsString.length))
return results.map { nsString.substring(with: $0.range) }
} catch let error {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
The main differences are:
Swift on Linux seems to require dropping the NS prefix on Foundation objects for which there is no Swift-native equivalent. (See Swift evolution proposal #86.)
Swift on Linux also requires specifying the options arguments for both the RegularExpression initialization and the matches method.
For some reason, coercing a String into an NSString doesn't work in Swift on Linux but initializing a new NSString with a String as the source does work.
This version also works with Swift 3 on macOS / Xcode with the sole exception that you must use the name NSRegularExpression instead of RegularExpression.
Swift 4 without NSString.
extension String {
func matches(regex: String) -> [String] {
guard let regex = try? NSRegularExpression(pattern: regex, options: [.caseInsensitive]) else { return [] }
let matches = regex.matches(in: self, options: [], range: NSMakeRange(0, self.count))
return matches.map { match in
return String(self[Range(match.range, in: self)!])
}
}
}
#p4bloch if you want to capture results from a series of capture parentheses, then you need to use the rangeAtIndex(index) method of NSTextCheckingResult, instead of range. Here's #MartinR 's method for Swift2 from above, adapted for capture parentheses. In the array that is returned, the first result [0] is the entire capture, and then individual capture groups begin from [1]. I commented out the map operation (so it's easier to see what I changed) and replaced it with nested loops.
func matches(for regex: String!, in text: String!) -> [String] {
do {
let regex = try NSRegularExpression(pattern: regex, options: [])
let nsString = text as NSString
let results = regex.matchesInString(text, options: [], range: NSMakeRange(0, nsString.length))
var match = [String]()
for result in results {
for i in 0..<result.numberOfRanges {
match.append(nsString.substringWithRange( result.rangeAtIndex(i) ))
}
}
return match
//return results.map { nsString.substringWithRange( $0.range )} //rangeAtIndex(0)
} catch let error as NSError {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
An example use case might be, say you want to split a string of title year eg "Finding Dory 2016" you could do this:
print ( matches(for: "^(.+)\\s(\\d{4})" , in: "Finding Dory 2016"))
// ["Finding Dory 2016", "Finding Dory", "2016"]
Most of the solutions above only give the full match as a result ignoring the capture groups e.g.: ^\d+\s+(\d+)
To get the capture group matches as expected you need something like (Swift4) :
public extension String {
public func capturedGroups(withRegex pattern: String) -> [String] {
var results = [String]()
var regex: NSRegularExpression
do {
regex = try NSRegularExpression(pattern: pattern, options: [])
} catch {
return results
}
let matches = regex.matches(in: self, options: [], range: NSRange(location:0, length: self.count))
guard let match = matches.first else { return results }
let lastRangeIndex = match.numberOfRanges - 1
guard lastRangeIndex >= 1 else { return results }
for i in 1...lastRangeIndex {
let capturedGroupIndex = match.range(at: i)
let matchedString = (self as NSString).substring(with: capturedGroupIndex)
results.append(matchedString)
}
return results
}
}
Update for iOS 16: Regex, RegexBuilder πŸ‘·β€β™€οΈ
Xcode previously supported Regex with the Find and Search tab. Many found Apple's NSRegularExpressions Swift API verbose and unwieldy, so Apple released Regex literal support and RegexBuilder this year.
The API has been simplified going forward to tidy up complex String range-based parsing logic in iOS 16 / macOS 13 as well as improve performance.
RegEx literals in Swift 5.7
func parseLine(_ line: Substring) throws -> MailmapEntry {
let regex = /\h*([^<#]+?)??\h*<([^>#]+)>\h*(?:#|\Z)/
guard let match = line.prefixMatch(of: regex) else {
throw MailmapError.badLine
}
return MailmapEntry(name: match.1, email: match.2)
}
At the moment, we are able to match using prefixMatch or wholeMatch to find a single match, but the API may improve in the future for multiple matches.
RegexBuilder in Swift 5.7
RegexBuilder is a new API released by Apple aimed at making RegEx code easier to write in Swift. We can translate the Regex literal /\h*([^<#]+?)??\h*<([^>#]+)>\h*(?:#|\Z)/ from above into a more declarative form using RegexBuilder if we want more readability.
Do note that we can use raw strings in a RegexBuilder and also interleave Regex Literals in the builder if we want to balance readability with conciseness.
import RegexBuilder
let regex = Regex {
ZeroOrMore(.horizontalWhitespace)
Optionally {
Capture(OneOrMore(.noneOf("<#")))
}
.repetitionBehavior(.reluctant)
ZeroOrMore(.horizontalWhitespace)
"<"
Capture(OneOrMore(.noneOf(">#")))
">"
ZeroOrMore(.horizontalWhitespace)
/#|\Z/
}
The RegEx literal /Β£|\Z/ is equivalent to:
ChoiceOf {
"#"
Anchor.endOfSubjectBeforeNewline
}
Composable RegexComponent
RegexBuilder syntax is similar to SwiftUI also in terms of composability because we can reuse RegexComponents within other RegexComponents:
struct MailmapLine: RegexComponent {
#RegexComponentBuilder
var regex: Regex<(Substring, Substring?, Substring)> {
ZeroOrMore(.horizontalWhitespace)
Optionally {
Capture(OneOrMore(.noneOf("<#")))
}
.repetitionBehavior(.reluctant)
ZeroOrMore(.horizontalWhitespace)
"<"
Capture(OneOrMore(.noneOf(">#")))
">"
ZeroOrMore(.horizontalWhitespace)
ChoiceOf {
"#"
Anchor.endOfSubjectBeforeNewline
}
}
}
This is how I did it, I hope it brings a new perspective how this works on Swift.
In this example below I will get the any string between []
var sample = "this is an [hello] amazing [world]"
var regex = NSRegularExpression(pattern: "\\[.+?\\]"
, options: NSRegularExpressionOptions.CaseInsensitive
, error: nil)
var matches = regex?.matchesInString(sample, options: nil
, range: NSMakeRange(0, countElements(sample))) as Array<NSTextCheckingResult>
for match in matches {
let r = (sample as NSString).substringWithRange(match.range)//cast to NSString is required to match range format.
println("found= \(r)")
}
This is a very simple solution that returns an array of string with the matches
Swift 3.
internal func stringsMatching(regularExpressionPattern: String, options: NSRegularExpression.Options = []) -> [String] {
guard let regex = try? NSRegularExpression(pattern: regularExpressionPattern, options: options) else {
return []
}
let nsString = self as NSString
let results = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))
return results.map {
nsString.substring(with: $0.range)
}
}
update #Mike Chirico's to Swift 5
extension String{
func regex(pattern: String) -> [String]?{
do {
let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options(rawValue: 0))
let all = NSRange(location: 0, length: count)
var matches = [String]()
regex.enumerateMatches(in: self, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: all) {
(result : NSTextCheckingResult?, _, _) in
if let r = result {
let nsstr = self as NSString
let result = nsstr.substring(with: r.range) as String
matches.append(result)
}
}
return matches
} catch {
return nil
}
}
}
basic phone number matching
let phoneNumbers = ["+79990001101", "+7 (800) 000-11-02", "+34 507 574 147 ", "+1-202-555-0118"]
let match: (String) -> String = {
$0.replacingOccurrences(of: #"[^\d+]"#, with: "", options: .regularExpression)
}
print(phoneNumbers.map(match))
// ["+79990001101", "+78000001102", "+34507574147", "+12025550118"]
Big thanks to Lars Blumberg his answer for capturing groups and full matches with Swift 4, which helped me out a lot. I also made an addition to it for the people who do want an error.localizedDescription response when their regex is invalid:
extension String {
func matchingStrings(regex: String) -> [[String]] {
do {
let regex = try NSRegularExpression(pattern: regex)
let nsString = self as NSString
let results = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))
return results.map { result in
(0..<result.numberOfRanges).map {
result.range(at: $0).location != NSNotFound
? nsString.substring(with: result.range(at: $0))
: ""
}
}
} catch let error {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
}
For me having the localizedDescription as error helped understand what went wrong with escaping, since it's displays which final regex swift tries to implement.
You can use matching(regex:) on the string like:
let array = try "Your String To Search".matching(regex: ".")
using this simple extension:
public extension String {
func matching(regex: String) throws -> [String] {
let regex = try NSRegularExpression(pattern: regex)
let results = regex.matches(in: self, range: NSRange(startIndex..., in: self))
return results.map { String(self[Range($0.range, in: self)!]) }
}
}

Resources