I have a string extension:
extension String {
func split(usingRegex pattern: String) -> [String] {
let regex = try! NSRegularExpression(pattern: pattern)
let matches = regex.matches(in: self, range: NSRange(0..<utf16.count))
let ranges = [startIndex..<startIndex] + matches.map{Range($0.range, in: self)!} + [endIndex..<endIndex]
return (0...matches.count).map {String(self[ranges[$0].upperBound..<ranges[$0+1].lowerBound])}
}
}
and I use it like this:
var string = "Hello45playground23today"
var output = string.split(usingRegex: "[0-9]+")
the output is:
["Hello", "playground", "today"]
But what I need is:
["Hello", "45", "playground", "23", "today"]
Is there a way to achieve that in Swift?
Your code adds only the substrings between the matches (and before the first match and after the last match) to the result. What you need is also the substrings for the matches themselves. This can be done by creating an array with all indices where a match starts or ends, and then taking all substrings between consecutive indices:
extension String {
func split(usingRegex pattern: String) -> [String] {
let regex = try! NSRegularExpression(pattern: pattern)
let matches = regex.matches(in: self, range: NSRange(startIndex..., in: self))
let splits = [startIndex]
+ matches
.map { Range($0.range, in: self)! }
.flatMap { [ $0.lowerBound, $0.upperBound ] }
+ [endIndex]
return zip(splits, splits.dropFirst())
.map { String(self[$0 ..< $1])}
}
}
Example:
let string = "Hello45playground23today"
let output = string.split(usingRegex: "[0-9]+")
print(output) // ["Hello", "45", "playground", "23", "today"]
The same can be done with an explicit loop (less sophisticated, but perhaps better readable):
extension String {
func split(usingRegex pattern: String) -> [String] {
let regex = try! NSRegularExpression(pattern: pattern)
let matches = regex.matches(in: self, range: NSRange(startIndex..., in: self))
var result: [String] = []
var pos = startIndex
for match in matches {
let range = Range(match.range, in: self)!
result.append(String(self[pos..<range.lowerBound]))
result.append(String(self[range]))
pos = range.upperBound
}
result.append(String(self[pos...]))
return result
}
}
Trying to split values in swift, and it works, but it is cAsE sEnSiTiVe. Is there a way of splitting them in swift 4 ignoring case?
"Hello There!"
"hello there!"
I am currently using String.components(seperatedBy: "Th") but that does only split the second string, "hello there!". Is there a way to split them both?
You can write something like this:
import Foundation
let str1 = "Hello There!"
let str2 = "hello there!"
extension String {
func caseInsensitiveSplit(separator: String) -> [String] {
//Thanks for Carpsen90. Please see comments below.
if separator.isEmpty {
return [self] //generates the same output as `.components(separatedBy: "")`
}
let pattern = NSRegularExpression.escapedPattern(for: separator)
let regex = try! NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let matches = regex.matches(in: self, options: [], range: NSRange(0..<self.utf16.count))
let ranges = (0..<matches.count+1).map { (i: Int)->NSRange in
let start = i == 0 ? 0 : matches[i-1].range.location + matches[i-1].range.length
let end = i == matches.count ? self.utf16.count: matches[i].range.location
return NSRange(location: start, length: end-start)
}
return ranges.map {String(self[Range($0, in: self)!])}
}
}
print( str1.caseInsensitiveSplit(separator: "th") ) //->["Hello ", "ere!"]
print( str2.caseInsensitiveSplit(separator: "th") ) //->["hello ", "ere!"]
But I wonder what you want to do with "hello " and "ere!".
(You lose case-info of the separator, if it matched with "th", "tH", "Th" or "TH".)
If you can explain what you really want to do, someone would show you a better solution for it.
Just for fun another version using NSRegularExpression but extending StringProtocol:
Swift 4 or later
extension StringProtocol {
func caseInsensitiveComponents<T>(separatedBy separator: T) -> [SubSequence] where T: StringProtocol, Index == String.Index {
var index = startIndex
return ((try? NSRegularExpression(pattern: NSRegularExpression.escapedPattern(for: String(separator)), options: .caseInsensitive))?.matches(in: String(self), range: NSRange(location: 0, length: utf16.count))
.compactMap {
guard let range = Range($0.range, in: String(self))
else { return nil }
defer { index = range.upperBound }
return self[index..<range.lowerBound]
} ?? []) + [self[index...]]
}
}
Playground testing
let str1 = "Hello There!"
let str2 = "hello there!"
str1.caseInsensitiveComponents(separatedBy: "Th") // ["Hello ", "ere!"]
str2.caseInsensitiveComponents(separatedBy: "Th") // ["Hello ", "ere!"]
An possible solution is to get the range of the separator with .caseInsensitive option and extract the substrings from its lower- and upperBound.
extension StringProtocol where Index == String.Index {
func caseInsensitiveComponents(separatedBy separator: String) -> [SubSequence]
{
var result = [SubSequence]()
var currentIndex = startIndex
while let separatorRange = self[currentIndex...].range(of: separator, options: .caseInsensitive) {
result.append(self[currentIndex..<separatorRange.lowerBound])
currentIndex = separatorRange.upperBound
}
return result + [self[currentIndex...]]
}
}
You could lower case the whole string first:
let s = "Hello There!".lowercased().components(separatedBy: "th")
Not sure if i understood what you are trying to achieve, but you can try this.
var str = "Hello There!"
var str2 = "hello there!"
override func viewDidLoad() {
super.viewDidLoad()
let split1 = str.lowercased().replacingOccurrences(of: "th", with: " ")
let split2 = str2.lowercased().replacingOccurrences(of: "th", with: " ")
print(split1) //hello ere!
print(split2) //hello ere!
}
If you want to filter for a list of items containing a certain keyword, split the strings is the wrong approach. All you need is a simple check whether that keyword appears in the item:
let queryStr = "th"
let items = [
"Hello There!",
"hello there!"
]
let filterdItems = items.filter {
return $0.range(of: queryStr, options: .caseInsensitive) != nil
}
I have following string "#[Hema](hema_ramburuth), #[Ilesh P](ilesh.panchal), #[Lewis Murphy](lewis) how are you?". I want to display this screen like this "Hema, Ilesh P, Lewis Murphy how are you?" also I want to identify the screen for the click event.
I have used the ActiveLabel repo for the click.
Hey I have had encountered a similar requirement. So this is how I have handled.
I have created an extension for String
extension String {
/// Returns range of text in the string
func getRange(OfText text: String) -> NSRange {
let nsRepresentation = self as NSString
return nsRepresentation.range(of: text)
}
}
In your View Controller,
var tapPrivacyGesture = UITapGestureRecognizer()
#IBOutlet weak var yourLabel: UILabel!
var displayText = String()
func matchesForRegexInText(regex: String, text: String, firstBracket: String, lastBracket: String) -> [String] {
do {
let regex = try NSRegularExpression(pattern: regex, options: [])
let nsString = text as NSString
let results = regex.matches(
in: text,
options: [],
range: NSRange(location: 0, length: nsString.length))
return results.map { nsString.substring(with: $0.range) }.map { $0.replacingOccurrences(of: firstBracket, with: "") }.map { $0.replacingOccurrences(of: lastBracket, with: "") }
} catch let error as NSError {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
var givenString = "#[Hema](hema_ramburuth), #[Ilesh P](ilesh.panchal), #[Lewis Murphy](lewis) how are you?"
let nameStrings = matchesForRegexInText(regex: "\\[(.*?)\\]", text: givenString, firstBracket: "[", lastBracket: "]")
let removeForUIStrings = matchesForRegexInText(regex: "\\((.*?)\\)", text: givenString, firstBracket: "(", lastBracket: ")")
removeForUIStrings.forEach {
givenString = givenString.replacingOccurrences(of: "(\($0))", with: "")
}
nameStrings.forEach {
givenString = givenString.replacingOccurrences(of: "[\($0)]", with: $0)
}
givenString = givenString.replacingOccurrences(of: "#", with: "")
print(givenString)
displayText = givenString
tapPrivacyGesture.addTarget(self, action: #selector(self.handlePolicyTap(tap:)))
yourLabel.addGestureRecognizer(tapPrivacyGesture)
yourLabel.isUserInteractionEnabled = true
func handlePolicyTap(tap: UITapGestureRecognizer) {
let storage = NSTextStorage(attributedString: yourLabel.attributedText ?? NSAttributedString())
let layoutManager = NSLayoutManager()
storage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: CGSize(width: yourLabel.frame.size.width, height: yourLabel.frame.size.height+100))
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = (yourLabel.lineBreakMode)
textContainer.maximumNumberOfLines = yourLabel.numberOfLines
layoutManager.addTextContainer(textContainer)
let location: CGPoint = tap.location(in: yourLabel)
let characterIndex: Int = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
guard
characterIndex < storage.length,
let question = currentQuestion else {
return
}
nameStrings.forEach {
let range = displayText.getRange(OfText: $0)
if range.contains(characterIndex) {
/// Perform actions on click of this string
}
}
}
As from your question, just hard-code parsing done below.
let fullString = "#[Hema](hema_ramburuth), #[Ilesh P](ilesh.panchal), #[Lewis Murphy](lewis) how are you?"
let allarray = fullString.split(separator: ",")
let messageArray = allarray.last
let message = messageArray?.split(separator: ")")
let correctMessage = message?.last
var allNames : String = ""
for namesString in allarray {
if allNames.count > 0 {
allNames += ", "
}
let name = String(namesString)
allNames += name.slice(from: "#[", to: "]") ?? ""
}
if allNames.count > 0 {
allNames += correctMessage ?? ""
}
print("Name and Message --- > \(allNames)")
Slicing string using String extension
extension String {
func slice(from: String, to: String) -> String? {
return (range(of: from)?.upperBound).flatMap { substringFrom in
(range(of: to, range: substringFrom..<endIndex)?.lowerBound).map { substringTo in
substring(with: substringFrom..<substringTo)
}
}
}
}
I've printed output as below:
Name and Message --- > Hema, Ilesh P, Lewis Murphy how are you?
I am new to regular expressions and need to perform a specific task with the same. I would require a regex that performs global search and checks if there are 3 or more consecutive digits in a string and if yes, then replaces all the digits with "xxxx".
For example, the string
abcdef 12 quews 4567
should be changed to
abcdef XX quews XXXX
Any help would be appreciated. Thanks.
UPDATED
Use this
func testingRegex6(text:String) ->String{
do{
var finalText = text
let componentNSString = NSString.init(string:text)
let regex = try NSRegularExpression(pattern: "\\d+", options:[.dotMatchesLineSeparators])
let matches = regex.matches(in: text ,options: [], range: NSMakeRange(0, componentNSString.length)).reversed()
if(matches.filter({$0.range.length >= 3}).count > 0) { //here we check if we have any substring containing 3 o more digits
for result3 in matches {
finalText = finalText.replacingOccurrences(of: componentNSString.substring(with: result3.range), with: Array(repeating: "X", count: result3.range.length).joined())
}
}
return finalText
}
catch{
return ""
}
return ""
}
Input "abcdef 12 quews 4567" Log "abcdef XX quews XXXX"
Input "abcdef 12 que82ws 45" Log "abcdef 12 que82ws 45"
With Reg. expression
let txt = "abc 12 wqer 987asd asdf1233sadf"
do {
let regx = try NSRegularExpression(pattern: "[0-9]", options: .caseInsensitive)
let result = regx.stringByReplacingMatches(in: txt, options: .withTransparentBounds, range: NSMakeRange(0, txt.count), withTemplate: "X")
print(result)
} catch {
print(error)
}
Without reg. expression
let txt = "abc 12 wqer 987asd asdf1233sadf"
let replacedStr = String(txt.map {
let c = String($0)
if let digit = Int(c), digit >= 0, digit <= 9 {
return "X"
} else {
return $0
}
})
print(replacedStr)
let str = "this is 123 my vie22w"
Convert string into string array
let characters = Array(str)
Use loop with counter and replace string if digits are more than 3.
var count = 0
for i in characters {
if i >= "0" && i <= "9" {
count += 1
}
}
if count > 3 {
let output = str.replacingOccurrences(of: "[\\[\\]^[0-9]]", with: "X", options: .regularExpression, range: nil)
print(output)
}
Use this,
let testString = "abcdef 12 quews 4567"
let output = testString.replacingOccurrences(of: "[\\[\\]^[0-9]]", with: "X", options: .regularExpression, range: nil)
print(output)
Your output looks like this,
abcdef XX quews XXXX
Update
Find String extension class,
extension String {
func matches(for regex: String) -> [String] {
do {
let regex = try NSRegularExpression(pattern: regex)
let results = regex.matches(in: self, range: NSRange(self.startIndex..., in: self))
return results.map {
String(self[Range($0.range, in: self)!])
}
} catch let error {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
}
and find below code,
let temp = "abc44def 12 quews 4564 1254"
let array = temp.matches(for: "\\d{3,}")
var output = temp
for val in array.enumerated() {
var string = ""
for _ in 0..<val.element.count {
string+="X"
}
output = output.replacingOccurrences(of: val.element, with: string)
}
print(output)
you will get output like this,
abc44def 12 quews XXXX XXXX
Thank you for all your answers, with all your inputs I was able to solve this with following code
extension String {
func replaceDigits() -> String {
do {
let regex = try NSRegularExpression(pattern: "\\d{3,}", options: [])
let matches = regex.numberOfMatches(in: self, options: [], range: NSRange(location: 0, length: self.count))
if matches > 0 {
return replacingOccurrences(of: "\\d", with: "X", options: .regularExpression, range: nil)
}
} catch {
return self
}
return self
}
}
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)!]) }
}
}