Without using NSRegularExpression, How can I get all matches of my string regular expression? - regex

Swift 3 introduced String.range(of:options). Then, with this function, is possible match a part of string without creating a NSRegularExpression object, for example:
let text = "it is need #match my both #hashtag!"
let match = text.range(of: "(?:^#|\\s#)[\\p{L}0-9_]*", options: .regularExpression)!
print(text[match]) // #math
But, is possible match both occurrences of the regexp (that is, #match and #hashtag), instead of only the first?

let text = "it is need #match my both #hashtag!"
// create an object to store the ranges found
var ranges: [Range<String.Index>] = []
// create an object to store your search position
var start = text.startIndex
// create a while loop to find your regex ranges
while let range = text.range(of: "(?:^#|\\s#)[\\p{L}0-9_]*", options: .regularExpression, range: start..<text.endIndex) {
// append your range found
ranges.append(range)
// and change the startIndex of your string search
start = range.lowerBound < range.upperBound ? range.upperBound : text.index(range.lowerBound, offsetBy: 1, limitedBy: text.endIndex) ?? text.endIndex
}
ranges.forEach({print(text[$0])})
This will print
#match
#hashtag
If you need to use it more than once in your code you should add this extension to your project:
extension StringProtocol {
func ranges<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Range<Index>] {
var result: [Range<Index>] = []
var start = startIndex
while start < endIndex,
let range = self[start...].range(of: string, options: options) {
result.append(range)
start = range.lowerBound < range.upperBound ?
range.upperBound : index(after: range.lowerBound)
}
return result
}
}
usage:
let text = "it is need #match my both #hashtag!"
let pattern = "(?<!\\S)#[\\p{L}0-9_]*"
let ranges = text.ranges(of: pattern, options: .regularExpression)
let matches = ranges.map{text[$0]}
print(matches) // ["#match", "#hashtag"]

Related

How to use regex to split string into groups of identical characters?

I got a string like this:
var string = "AAAAAAABBBCCCCCCDD"
and like to split the string into an array of this format (same characters --> same group) using regular expressions:
Array: "AAAAAAA", "BBB", "CCCCCC", "DD"
This Is what I got so far but tbh I can not really get it working.
var array = [String]()
var string = "AAAAAAABBBCCCCCCDD"
let pattern = "\\ b([1,][a-z])\\" // mistake?!
let regex = try! NSRegularExpression(pattern: pattern, options: [])
array = regex.matchesInString(string, options: [], range: NSRange(location: 0, length: string.count))
You can achieve that using this function from this answer:
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 []
}
}
Passing (.)\\1+ as regex and AAAAAAABBBCCCCCCDD as text like this:
let result = matches(for: "(.)\\1+", in: "AAAAAAABBBCCCCCCDD")
print(result) // ["AAAAAAA", "BBB", "CCCCCC", "DD"]
You can achieve that with a "back reference", compare
NSRegularExpression:
\n
Back Reference. Match whatever the nth capturing group matched. n must be a number ≥ 1 and ≤ total number of capture groups in the pattern.
Example (using the utility method from Swift extract regex matches):
let string = "AAAAAAABBBCCCCCCDDE"
let pattern = "(.)\\1*"
let array = matches(for: pattern, in: string)
print(array)
// ["AAAAAAA", "BBB", "CCCCCC", "DD", "E"]
The pattern matches an arbitrary character, followed by zero or more
occurrences of the same character. If you are only interested in
repeating word characters use
let pattern = "(\\w)\\1*"
instead.

Extracting words from inside sentence using regex in swift

I want a regex to extract Starboy and The Weekend / Daft Punk out of string:
The Weeknd / Daft Punk - text=\"Starboy\" song_spot=\"M\" MediaBaseId=\"2238986\" itunesTrackId=\"0\" amgTrackId=\"-1\" amgArtistId=\"0\" TAID=\"744880\" TPID=\"43758958\" cartcutId=\"08
So far this is my attempt
do {
let input = "The Weeknd / Daft Punk - text=\"Starboy\" song_spot=\"M\" MediaBaseId=\"2238986\" itunesTrackId=\"0\" amgTrackId=\"-1\" amgArtistId=\"0\" TAID=\"744880\" TPID=\"43758958\" cartcutId=\"0893584001\""
let regex = try NSRegularExpression(pattern: "text=\"(.*)", options: NSRegularExpression.Options.caseInsensitive)
let matches = regex.matches(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count))
if let match = matches.first {
let range = match.range(at:1)
if let swiftRange = Range(range, in: input) {
let name = input[swiftRange]
print(name)
}
}
} catch {
print("Regex was bad!")
}
But this gives me the entire string
Starboy" song_spot="M" MediaBaseId="2238986" itunesTrackId="0" amgTrackId="-1" amgArtistId="0" TAID="744880" TPID="43758958" cartcutId="0893584001"
If you need to capture all text up to the sequence - text= followed by any word(s) between quote marks you can use this regex ".*(?=(text=\"[\\w\\s]+\"))" and to capture any word(s) after the sequence text=" you can use this regex "(?<=text=\")([\\w\\s]+)". If you want to capture both ranges just use "|" between them as follow:
let string = """
The Weeknd / Daft Punk - text=\"Starboy\" song_spot=\"M\" MediaBaseId=\"2238986\" itunesTrackId=\"0\" amgTrackId=\"-1\" amgArtistId=\"0\" TAID=\"744880\" TPID=\"43758958\" cartcutId=\"08
"""
let pattern = ".*(?=( - text=\"[\\w\\s]+\"))|(?<=text=\")([\\w\\s]+)"
do {
let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let matches = regex.matches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count))
for match in matches {
if let range = Range(match.range, in: string) {
let name = string[range]
print(name)
}
}
} catch {
print("Regex was bad!")
}
This will print
The Weeknd / Daft Punk
Starboy

Make sure regex matches the entire string with Swift regex

How to check whether a WHOLE string can be matches to regex? In Java is method String.matches(regex)
You need to use anchors, ^ (start of string anchor) and $ (end of string anchor), with range(of:options:range:locale:), passing the .regularExpression option:
import Foundation
let phoneNumber = "123-456-789"
let result = phoneNumber.range(of: "^\\d{3}-\\d{3}-\\d{3}$", options: .regularExpression) != nil
print(result)
Or, you may pass an array of options, [.regularExpression, .anchored], where .anchored will anchor the pattern at the start of the string only, and you will be able to omit ^, but still, $ will be required to anchor at the string end:
let result = phoneNumber.range(of: "\\d{3}-\\d{3}-\\d{3}$", options: [.regularExpression, .anchored]) != nil
See the online Swift demo
Also, using NSPredicate with MATCHES is an alternative here:
The left hand expression equals the right hand expression using a regex-style comparison according to ICU v3 (for more details see the ICU User Guide for Regular Expressions).
MATCHES actually anchors the regex match both at the start and end of the string (note this might not work in all Swift 3 builds):
let pattern = "\\d{3}-\\d{3}-\\d{3}"
let predicate = NSPredicate(format: "self MATCHES [c] %#", pattern)
let result = predicate.evaluate(with: "123-456-789")
What you are looking for is range(of:options:range:locale:) then you can then compare the result of range(of:option:) with whole range of comparing string..
Example:
let phoneNumber = "(999) 555-1111"
let wholeRange = phoneNumber.startIndex..<phoneNumber.endIndex
if let match = phoneNumber.range(of: "\\(?\\d{3}\\)?\\s\\d{3}-\\d{4}", options: .regularExpression), wholeRange == match {
print("Valid number")
}
else {
print("Invalid number")
}
//Valid number
Edit: You can also use NSPredicate and compare your string with evaluate(with:) method of its.
let pattern = "^\\(?\\d{3}\\)?\\s\\d{3}-\\d{4}$"
let predicate = NSPredicate(format: "self MATCHES [c] %#", pattern)
if predicate.evaluate(with: "(888) 555-1111") {
print("Valid")
}
else {
print("Invalid")
}
Swift extract regex matches
with little bit of edit
import Foundation
func matches(for regex: String, in text: String) -> Bool {
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.isEmpty
} catch let error {
print("invalid regex: \(error.localizedDescription)")
return false
}
}
Example usage from link above:
let string = "19320"
let matched = matches(for: "^[1-9]\\d*$", in: string)
print(matched) // will match
let string = "a19320"
let matched = matches(for: "^[1-9]\\d*$", in: string)
print(matched) // will not match

Number of occurrences of substring in string in Swift

My main string is "hello Swift Swift and Swift" and substring is Swift.
I need to get the number of times the substring "Swift" occurs in the mentioned string.
This code can determine whether the pattern exists.
var string = "hello Swift Swift and Swift"
if string.rangeOfString("Swift") != nil {
println("exists")
}
Now I need to know the number of occurrence.
A simple approach would be to split on "Swift", and subtract 1 from the number of parts:
let s = "hello Swift Swift and Swift"
let tok = s.components(separatedBy:"Swift")
print(tok.count-1)
This code prints 3.
Edit: Before Swift 3 syntax the code looked like this:
let tok = s.componentsSeparatedByString("Swift")
Should you want to count characters rather than substrings:
extension String {
func count(of needle: Character) -> Int {
return reduce(0) {
$1 == needle ? $0 + 1 : $0
}
}
}
Optimising dwsolbergs solution to count faster. Also faster than componentsSeparatedByString.
extension String {
/// stringToFind must be at least 1 character.
func countInstances(of stringToFind: String) -> Int {
assert(!stringToFind.isEmpty)
var count = 0
var searchRange: Range<String.Index>?
while let foundRange = range(of: stringToFind, options: [], range: searchRange) {
count += 1
searchRange = Range(uncheckedBounds: (lower: foundRange.upperBound, upper: endIndex))
}
return count
}
}
Usage:
// return 2
"aaaa".countInstances(of: "aa")
If you want to ignore accents, you may replace options: [] with options: .diacriticInsensitive like dwsolbergs did.
If you want to ignore case, you may replace options: [] with options: .caseInsensitive like ConfusionTowers suggested.
If you want to ignore both accents and case, you may replace options: [] with options: [.caseInsensitive, .diacriticInsensitive] like ConfusionTowers suggested.
If, on the other hand, you want the fastest comparison possible and you can guarantee some canonical form for composed character sequences, then you may consider option .literal and it will only perform exact matchs.
Swift 5 Extension
extension String {
func numberOfOccurrencesOf(string: String) -> Int {
return self.components(separatedBy:string).count - 1
}
}
Example use
let string = "hello Swift Swift and Swift"
let numberOfOccurrences = string.numberOfOccurrencesOf(string: "Swift")
// numberOfOccurrences = 3
I'd recommend an extension to string in Swift 3 such as:
extension String {
func countInstances(of stringToFind: String) -> Int {
var stringToSearch = self
var count = 0
while let foundRange = stringToSearch.range(of: stringToFind, options: .diacriticInsensitive) {
stringToSearch = stringToSearch.replacingCharacters(in: foundRange, with: "")
count += 1
}
return count
}
}
It's a loop that finds and removes each instance of the stringToFind, incrementing the count on each go-round. Once the searchString no longer contains any stringToFind, the loop breaks and the count returns.
Note that I'm using .diacriticInsensitive so it ignore accents (for example résume and resume would both be found). You might want to add or change the options depending on the types of strings you want to find.
I needed a way to count substrings that may contain the start of the next matched substring. Leveraging dwsolbergs extension and Strings range(of:options:range:locale:) method I came up with this String extension
extension String
{
/**
Counts the occurrences of a given substring by calling Strings `range(of:options:range:locale:)` method multiple times.
- Parameter substring : The string to search for, optional for convenience
- Parameter allowOverlap : Bool flag indicating whether the matched substrings may overlap. Count of "🐼🐼" in "🐼🐼🐼🐼" is 2 if allowOverlap is **false**, and 3 if it is **true**
- Parameter options : String compare-options to use while counting
- Parameter range : An optional range to limit the search, default is **nil**, meaning search whole string
- Parameter locale : Locale to use while counting
- Returns : The number of occurrences of the substring in this String
*/
public func count(
occurrencesOf substring: String?,
allowOverlap: Bool = false,
options: String.CompareOptions = [],
range searchRange: Range<String.Index>? = nil,
locale: Locale? = nil) -> Int
{
guard let substring = substring, !substring.isEmpty else { return 0 }
var count = 0
let searchRange = searchRange ?? startIndex..<endIndex
var searchStartIndex = searchRange.lowerBound
let searchEndIndex = searchRange.upperBound
while let rangeFound = range(of: substring, options: options, range: searchStartIndex..<searchEndIndex, locale: locale)
{
count += 1
if allowOverlap
{
searchStartIndex = index(rangeFound.lowerBound, offsetBy: 1)
}
else
{
searchStartIndex = rangeFound.upperBound
}
}
return count
}
}
why not just use some length maths??
extension String {
func occurences(of search:String) -> Int {
guard search.count > 0 else {
preconditionFailure()
}
let shrunk = self.replacingOccurrences(of: search, with: "")
return (self.count - shrunk.count)/search.count
}
}
Try this
var mainString = "hello Swift Swift and Swift"
var count = 0
mainString.enumerateSubstrings(in: mainString.startIndex..<mainString.endIndex, options: .byWords) { (subString, subStringRange, enclosingRange, stop) in
if case let s? = subString{
if s.caseInsensitiveCompare("swift") == .orderedSame{
count += 1
}
}
}
print(count)
For the sake of completeness – and because there is a regex tag – this is a solution with Regular Expression
let string = "hello Swift Swift and Swift"
let regex = try! NSRegularExpression(pattern: "swift", options: .caseInsensitive)
let numberOfOccurrences = regex.numberOfMatches(in: string, range: NSRange(string.startIndex..., in: string))
The option .caseInsensitive is optional.
My solution, maybe it will be better to use String.Index instead of Int range but I think in such way it is a bit easier to read.
extension String {
func count(of char: Character, range: (Int, Int)? = nil) -> Int {
let range = range ?? (0, self.count)
return self.enumerated().reduce(0) {
guard ($1.0 >= range.0) && ($1.0 < range.1) else { return $0 }
return ($1.1 == char) ? $0 + 1 : $0
}
}
}
Solution which uses a higher order functions
func subStringCount(str: String, substr: String) -> Int {
{ $0.isEmpty ? 0 : $0.count - 1 } ( str.components(separatedBy: substr))
}
Unit Tests
import XCTest
class HigherOrderFunctions: XCTestCase {
func testSubstringWhichIsPresentInString() {
XCTAssertEqual(subStringCount(str: "hello Swift Swift and Swift", substr: "Swift"), 3)
}
func testSubstringWhichIsNotPresentInString() {
XCTAssertEqual(subStringCount(str: "hello", substr: "Swift"), 0)
}
}
Another way using RegexBuilder in iOS 16+ & swift 5.7+.
import RegexBuilder
let text = "hello Swift Swift and Swift"
let match = text.matches(of: Regex{"Swift"})
print(match.count) // prints 3
Using this as a function
func countSubstrings(string : String, subString : String)-> Int{
return string.matches(of: Regex{subString}).count
}
print(countSubstrings(string: text, subString: "Swift")) //prints 3
Using this as an Extension
extension String {
func countSubstrings(subString : String)-> Int{
return self.matches(of: Regex{subString}).count
}
}
print(text.countSubstrings(subString: "Swift")) // prints 3

How to use regex with Swift?

I am making an app in Swift and I need to catch 8 numbers from a string.
Here's the string:
index.php?page=index&l=99182677
My pattern is:
&l=(\d{8,})
And here's my code:
var yourAccountNumber = "index.php?page=index&l=99182677"
let regex = try! NSRegularExpression(pattern: "&l=(\\d{8,})", options: NSRegularExpressionOptions.CaseInsensitive)
let range = NSMakeRange(0, yourAccountNumber.characters.count)
let match = regex.matchesInString(yourAccountNumber, options: NSMatchingOptions.Anchored, range: range)
Firstly, I don't know what the NSMatchingOptions means, on the official Apple library, I don't get all the .Anchored, .ReportProgress, etc stuff. Anyone would be able to lighten me up on this?
Then, when I print(match), nothing seems to contain on that variable ([]).
I am using Xcode 7 Beta 3, with Swift 2.0.
ORIGINAL ANSWER
Here is a function you can leverage to get captured group texts:
import Foundation
extension String {
func firstMatchIn(string: NSString!, atRangeIndex: Int!) -> String {
var error : NSError?
let re = NSRegularExpression(pattern: self, options: .CaseInsensitive, error: &error)
let match = re.firstMatchInString(string, options: .WithoutAnchoringBounds, range: NSMakeRange(0, string.length))
return string.substringWithRange(match.rangeAtIndex(atRangeIndex))
}
}
And then:
var result = "&l=(\\d{8,})".firstMatchIn(yourAccountNumber, atRangeIndex: 1)
The 1 in atRangeIndex: 1 will extract the text captured by (\d{8,}) capture group.
NOTE1: If you plan to extract 8, and only 8 digits after &l=, you do not need the , in the limiting quantifier, as {8,} means 8 or more. Change to {8} if you plan to capture just 8 digits.
NOTE2: NSMatchingAnchored is something you would like to avoid if your expected result is not at the beginning of a search range. See documentation:
Specifies that matches are limited to those at the start of the search range.
NOTE3: Speaking about "simplest" things, I'd advise to avoid using look-arounds whenever you do not have to. Look-arounds usually come at some cost to performance, and if you are not going to capture overlapping text, I'd recommend to use capture groups.
UPDATE FOR SWIFT 2
I have come up with a function that will return all matches with all capturing groups (similar to preg_match_all in PHP). Here is a way to use it for your scenario:
func regMatchGroup(regex: String, text: String) -> [[String]] {
do {
var resultsFinal = [[String]]()
let regex = try NSRegularExpression(pattern: regex, options: [])
let nsString = text as NSString
let results = regex.matchesInString(text,
options: [], range: NSMakeRange(0, nsString.length))
for result in results {
var internalString = [String]()
for var i = 0; i < result.numberOfRanges; ++i{
internalString.append(nsString.substringWithRange(result.rangeAtIndex(i)))
}
resultsFinal.append(internalString)
}
return resultsFinal
} catch let error as NSError {
print("invalid regex: \(error.localizedDescription)")
return [[]]
}
}
// USAGE:
let yourAccountNumber = "index.php?page=index&l=99182677"
let matches = regMatchGroup("&l=(\\d{8,})", text: yourAccountNumber)
if (matches.count > 0) // If we have matches....
{
print(matches[0][1]) // Print the first one, Group 1.
}
It may be easier just to use the NSString method instead of NSRegularExpression.
var yourAccountNumber = "index.php?page=index&l=99182677"
println(yourAccountNumber) // index.php?page=index&l=99182677
let regexString = "(?<=&l=)\\d{8,}+"
let options :NSStringCompareOptions = .RegularExpressionSearch | .CaseInsensitiveSearch
if let range = yourAccountNumber.rangeOfString(regexString, options:options) {
let digits = yourAccountNumber.substringWithRange(range)
println("digits: \(digits)")
}
else {
print("Match not found")
}
The (?<=&l=) means precedes but not part of.
In detail:
Look-behind assertion. True if the parenthesized pattern matches text preceding the current input position, with the last character of the match being the input character just before the current position. Does not alter the input position. The length of possible strings matched by the look-behind pattern must not be unbounded (no * or + operators.)
In general performance considerations of a look-behind without instrumented proof is just premature optimization. That being said there may be other valid reasons for and against look-arounds in regular expressions.
ICU User Guide: Regular Expressions
For Swift 2, you can use this extension of String:
import Foundation
extension String {
func firstMatchIn(string: NSString!, atRangeIndex: Int!) -> String {
do {
let re = try NSRegularExpression(pattern: self, options: NSRegularExpressionOptions.CaseInsensitive)
let match = re.firstMatchInString(string as String, options: .WithoutAnchoringBounds, range: NSMakeRange(0, string.length))
return string.substringWithRange(match!.rangeAtIndex(atRangeIndex))
} catch {
return ""
}
}
}
You can get the account-number with:
var result = "&l=(\\d{8,})".firstMatchIn(yourAccountNumber, atRangeIndex: 1)
Replace NSMatchingOptions.Anchored with NSMatchingOptions() (no options)