By mitchkman


2015-01-10 20:04:04 8 Comments

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?

9 comments

@Lars Blumberg 2016-10-14 10:06:19

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))
                    : ""
            }
        }
    }
}

@Martin R 2016-10-17 05:27:43

Good idea about the capture groups. But why is "guard" Swiftier than "do/catch"??

@Lars Blumberg 2016-10-17 08:47:24

I agree with people such as nshipster.com/guard-and-defer who say Swift 2.0 certainly seems to be encouraging a style of early return [...] rather than nested if statements. The same holds true for nested do/catch statements IMHO.

@Martin R 2016-10-17 08:58:28

try/catch is the native error handling in Swift. try? can be used if you are only interested in the outcome of the call, not in a possible error message. So yes, guard try? .. is fine, but if you want to print the error then you need a do-block. Both ways are Swifty.

@Lars Blumberg 2016-10-17 13:18:52

I agree that you need do/catch in your example if you want to see the error in the console. Since I want to provide a function that can be reused in production code without modifying it (print is an unwanted side effect for me), guard try? becomes a little Swiftier (if you don't need the side effect) – modified the answer to clarify. Thanks!

@neoneye 2016-11-28 13:22:34

I have added unittests to your nice snippet, gist.github.com/neoneye/03cbb26778539ba5eb609d16200e4522

@Oritm 2017-05-11 22:30:24

Was about to write my own based on the @MartinR answer until i saw this. Thanks!

@Ahmad 2018-03-09 22:56:23

Don't you think it is time to add Swift 4 version?

@Vasco 2018-12-05 18:00:53

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.

@valexa 2017-12-06 10:05:14

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
    }
}

@Geoff 2018-02-20 16:41:42

This is great if you're wanting just the first result, to get each result it needs for index in 0..<matches.count { around let lastRange... results.append(matchedString)}

@CRE8IT 2018-09-17 11:36:49

the for clause should look like this: for i in 1...lastRangeIndex { let capturedGroupIndex = match.range(at: i) if capturedGroupIndex.location != NSNotFound { let matchedString = (self as NSString).substring(with: capturedGroupIndex) results.append(matchedString.trimmingCharacters(in: .whitespaces)) } }

@Martin R 2015-01-10 20:12:24

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"]

@mitchkman 2015-01-10 20:27:27

You saved me from becoming insane. Not kidding. Thank you so much!

@Martin R 2015-04-16 13:01:48

@MathijsSegers: I have updated the code for Swift 1.2/Xcode 6.3. Thanks for letting me know!

@Peter Kreinz 2015-08-18 21:09:53

but what if i want to search for strings between a tag? I need the same result (match information) like: regex101.com/r/cU6jX8/2. which regex pattern would you suggest?

@PatrickNLT 2015-09-13 19:17:04

The update is for Swift 1.2, not Swift 2. The code doesn't compile with Swift 2.

@Martin R 2015-09-13 19:20:30

@pnollet: Strange. I have just double-checked that the "Swift 2" version compiles and runs with Xcode 7 GM, and it worked as expected. What error message do you get?

@p4bloch 2015-09-23 23:01:46

Thanks! What if you only want to extract what's actually between () in the regex? For example, in "[0-9]{3}([0-9]{6})" I'd only want to get the last 6 numbers.

@Kevin Mann 2015-11-13 14:25:36

If I try using this method to extract text between parentheses I get compile errors (using Swift 2) - the reg ex I am passing in is "\((.*?)\)"can anyone help?

@Declan McKenna 2015-12-14 10:56:53

@KevinMann Double backslash the special characters. You need to escape the string and the special character

@Fuad Kamal 2016-09-26 19:57:48

The Swift 3 example here does not compile. The try statement gives a compile error "Errors thrown from here are not handled"

@Martin R 2016-09-26 20:13:51

@FuadKamal: That is strange. I have double-checked it with Xcode 8, and it compiles and runs as expected.

@Fuad Kamal 2016-09-27 01:27:06

@MartinR it works for me now as well. Didn't even have to do a clean...Xcode strangeness I guess ¯_(ツ)_/¯ Thanks for checking!

@Vyachaslav Gerchicov 2017-07-06 13:07:58

@MartinR shouldn't you check for nil?

@Martin R 2017-07-06 13:11:38

@VyachaslavGerchicov: $0.range is an NSRange returned from regex.matches(...) as the range in the string which matches the given pattern. Therefore I think it is safe to assume that it can be converted back to a Range<String.Index>.

@Adrian 2017-10-02 13:48:33

In Swift 4, the line text.substring(with: Range($0.range, in: text)!) generates a deprecated warning. I looked here developer.apple.com/documentation/foundation/nsstring/… and I don't see anything about deprecation. This answer has a solution to a slightly different problem, but I can't apply it to silence the warning. stackoverflow.com/questions/45562662/…

@Martin R 2017-10-02 14:20:02

@Adrian: You are right. The new version should work without warnings. Thanks for the notice!

@ixany 2017-11-21 20:07:47

It would be great if you could provide a solution without to force-unwrap the range!

@Martin R 2017-11-21 20:18:37

@ixany: Have a look!

@AppHero2 2017-12-15 07:13:03

Thank you guys!

@Leszek Szary 2018-07-14 16:50:19

So much hassle just to extract a substring with a regex. It should be possible to do this with a single line of code. Unbelievable! Thanks!

@Jorge Osorio 2017-10-02 15:47:00

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)
        }
    }

@Rob Mecham 2016-10-17 18:45:38

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:

  1. 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.)

  2. Swift on Linux also requires specifying the options arguments for both the RegularExpression initialization and the matches method.

  3. 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.

@OliverD 2016-08-06 19:18:00

@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"]

@Ahmad 2018-03-09 22:55:26

This answer made my day. I spent 2 hours searching for a solution that can satisfy regualr expression with the additional capturing of groups.

@stef 2018-06-16 03:19:04

This works but it will crash if any range is not found. I modified this code so that the function returns [String?] and in the for i in 0..<result.numberOfRanges block, you have to add a test that only appends the match if the range != NSNotFound, otherwise it should append nil. See: stackoverflow.com/a/31892241/2805570

@Mike Chirico 2015-11-06 13:20:24

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"]

@Kyle KIM 2016-02-03 13:10:41

This is what I wanted

@Jelle 2016-12-11 09:19:06

Nice! It needs a little adjustment for Swift 3, but it's great.

@Dalorzo 2015-11-04 17:18:18

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)")
}

Related Questions

Sponsored Content

15 Answered Questions

[SOLVED] How do you access the matched groups in a JavaScript regular expression?

  • 2009-01-11 07:21:20
  • nickf
  • 628179 View
  • 1129 Score
  • 15 Answer
  • Tags:   javascript regex

36 Answered Questions

[SOLVED] Extract filename and extension in Bash

17 Answered Questions

[SOLVED] #pragma mark in Swift?

  • 2014-06-03 14:05:56
  • Arbitur
  • 198636 View
  • 858 Score
  • 17 Answer
  • Tags:   swift

32 Answered Questions

[SOLVED] Split a String into an array in Swift?

27 Answered Questions

35 Answered Questions

[SOLVED] RegEx match open tags except XHTML self-contained tags

  • 2009-11-13 22:38:26
  • Jeff
  • 2474033 View
  • 1324 Score
  • 35 Answer
  • Tags:   html regex xhtml

5 Answered Questions

[SOLVED] Swift regex: does a string match a pattern?

  • 2015-04-21 22:40:13
  • joelparkerhenderson
  • 25099 View
  • 27 Score
  • 5 Answer
  • Tags:   ios regex string swift

0 Answered Questions

3 Answered Questions

[SOLVED] Match all occurrences of a regex

  • 2008-09-17 05:46:26
  • Chris Bunch
  • 168361 View
  • 545 Score
  • 3 Answer
  • Tags:   ruby regex

1 Answered Questions

[SOLVED] swift Regular Expression and (.*?)

  • 2016-07-12 18:37:39
  • ÇağrΔ± Can Bozkurt
  • 326 View
  • -2 Score
  • 1 Answer
  • Tags:   ios regex swift

Sponsored Content