Swift Regex optimization - swift

i want to look in an array of strings to get all strings containing subbstring. This function should also work with wildcard.
I wrote this function:
func wordcontains(word: String, from words: [String]) -> [String] {
//Si il y a des jokers on utilise la methode regex
//Sinon on utilise la methode simple car beaucoup plus rapide
let foundWords = words.filter { otherWord in
let wordregex = word.replacingOccurrences(of: "?", with: ".")
if (otherWord.range(of: "[A-Z]*\(wordregex)[A-Z]*", options: .regularExpression) != nil){
return true
}else {
return false
}
}
return foundWords
}
and it's working like that:
input : anagrams(word: "ARC?", from: ["BOU", "BAC", "ARCS", "ARCH", "TREE","ARCHE","PROUE"])
output : ["ARCS", "ARCH", "ARCHE"]
it's working well with a small array, but i need to check in an array of 300000 words and it take a while.
What is the best way to optimize the regex / function?
Perhaps there is a better approch ?

For your interest, the code I used for testing.
Create a Command Line Tool project.
import Foundation
func wordcontains(word: String, from words: [String]) -> [String] {
...(exactly the same code as yours)...
}
///Creating NSRegularExpression outside of the loop
func wordcontains2(word: String, from words: [String]) -> [String] {
let wordregex = word.replacingOccurrences(of: "?", with: ".")
let pattern = "[A-Z]*\(wordregex)[A-Z]*"
let regex: NSRegularExpression
do {
regex = try NSRegularExpression(pattern: pattern)
} catch {
fatalError(error.localizedDescription)
}
let foundWords = words.filter { otherWord in
regex.firstMatch(in: otherWord, range: NSRange(0..<otherWord.utf16.count)) != nil
}
return foundWords
}
/// Removing `[A-Z]*` from both ends as suggested in rmaddy's comment.
/// This assumes all words in the parameter `words` consists only capital letters.
func wordcontains3(word: String, from words: [String]) -> [String] {
let wordregex = word.replacingOccurrences(of: "?", with: ".")
let regex: NSRegularExpression
do {
regex = try NSRegularExpression(pattern: wordregex)
} catch {
fatalError(error.localizedDescription)
}
let foundWords = words.filter { otherWord in
regex.firstMatch(in: otherWord, range: NSRange(0..<otherWord.utf16.count)) != nil
}
return foundWords
}
Generally, creating an instance of NSRegularExpression is an expensive operation, so moving it outside of the loop may improve the performance (of course, in case the regex does not change), but the effect is very limited.
And I added some code for testing.
func makeRandomWords(_ count: Int) -> [String] {
var words: [String] = []
for _ in 0..<count {
let len = Int.random(in: 3...5)
var word = ""
for _ in 0..<len {
let charCode = UInt32.random(in: UInt32(UInt8(ascii: "A"))...UInt32(UInt8(ascii: "Z")))
word.append(Character(UnicodeScalar(charCode)!))
}
words.append(word)
}
return words
}
let words = makeRandomWords(300_000) //I have found the number of words is `300000` after I wrote my comment...
do {
let date1 = Date()
let w1 = wordcontains(word: "ARC?", from: words)
let date2 = Date()
print(date2.timeIntervalSince(date1), w1)
let date3 = Date()
let w2 = wordcontains2(word: "ARC?", from: words)
let date4 = Date()
print(date4.timeIntervalSince(date3), w2)
let date5 = Date()
let w3 = wordcontains3(word: "ARC?", from: words)
let date6 = Date()
print(date6.timeIntervalSince(date5), w3)
}
Result:
6.443639039993286 ["ARCQJ", "ARCZB", "AARCI", "ARCR", "ARCR", "ARCQS", "ARCGM", "ARCKL", "UARCN", "FARCS", "ARCNA", "ARCZM", "PARCL", "ARCTA", "ARCS", "ARCE", "ARCG", "ARCE"]
1.7534430027008057 ["ARCQJ", "ARCZB", "AARCI", "ARCR", "ARCR", "ARCQS", "ARCGM", "ARCKL", "UARCN", "FARCS", "ARCNA", "ARCZM", "PARCL", "ARCTA", "ARCS", "ARCE", "ARCG", "ARCE"]
1.4359259605407715 ["ARCQJ", "ARCZB", "AARCI", "ARCR", "ARCR", "ARCQS", "ARCGM", "ARCKL", "UARCN", "FARCS", "ARCNA", "ARCZM", "PARCL", "ARCTA", "ARCS", "ARCE", "ARCG", "ARCE"]
The result may change as this code uses random words, but the consumed times may not show big difference for each run.

Related

Swift 5.1 Regex Bug

for the following code:
import Foundation
extension String {
var fullRange: NSRange {
return .init(self.startIndex ..< self.endIndex, in: self)
}
public subscript(range: Range<Int>) -> Self.SubSequence {
let st = self.index(self.startIndex, offsetBy: range.startIndex)
let ed = self.index(self.startIndex, offsetBy: range.endIndex)
let sub = self[st ..< ed]
return sub
}
func split(regex pattern: String) throws -> [String] {
let regex = try NSRegularExpression.init(pattern: pattern, options: [])
let fRange = self.fullRange
let match = regex.matches(in: self, options: [], range: fRange)
var list = [String]()
var start = 0
for m in match {
let r = m.range
let end = r.location
list.append(String(self[start ..< end]))
start = end + r.length
}
if start < self.count {
list.append(String(self[start ..< self.count]))
}
return list
}
}
print(try! "مرتفع جداً\nVery High".split(regex: "\n"))
the output should be :
["مرتفع جداً", "Very High"]
but instead it is:
["مرتفع جداً\n", "ery High"]
that because regex (for this case) matched the \n at the offset 10 instead of 9
is there any thing wrong in my code, or it is a bug in swift with regex !!
It's not a bug. You are trying to use Int indexes which is error-prone and strongly discouraged in an Unicode environment.
This is the equivalent of your code with the proper String.Index type and the dedicated API to convert NSRange to Range<String.Index> and vice versa. fullRange and subscript are obsolete.
I just left out the print line. startIndex and endIndex are properties of String
extension String {
func split(regex pattern: String) throws -> [String] {
let regex = try NSRegularExpression(pattern: pattern)
let matches = regex.matches(in: self, range: NSRange(startIndex..., in: self))
var list = [String]()
var start = startIndex
for match in matches {
let range = Range(match.range, in: self)!
let end = range.lowerBound
list.append(String(self[start..<end]))
start = range.upperBound
}
if start < endIndex {
list.append(String(self[start..<endIndex]))
}
return list
}
}
print(try! "مرتفع جداً\nVery High".split(regex: "\n"))
The result is ["مرتفع جداً", "Very High"]
I found the issue behind this bug?!
Swift Strings are so much weirder than any other language; since every character is 4 bytes length, then a single character (may, would, will, ..) contains 1 or 2 unicode characters (witch what happened in my case), so the solution is to subarray the unicodeScalars of the swift String instead of the string it self !!

Replacing string with substring using NSRegularExpression in Swift [duplicate]

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

Regex get Group Names?

Looking for a way to get Group Names using Swift, I found info using GetGroupNames in C# (How do I get the name of captured groups in a C# Regex?) but can't find anything similar. Want to be able to tell is a group exists.
At the moment using code below ...
let search = "Agora.2010.1080p.BluRay.x264.mp4"
let regex "^(Agora.)(?<"group name">2010)?"
let return_results = regex(for: regex, in: search)
func regex(for expression: String, in string: String) -> [String]? {
guard let regex = try? NSRegularExpression(pattern: expression, options: [.caseInsensitive]) else { return nil }
let results = regex.matches(in: string, range: NSRange(string.startIndex..., in: string))
guard !results.isEmpty else { return nil }
var values: [String] = []
guard regex.numberOfCaptureGroups > 0 else { return nil }
for r in 1...(regex.numberOfCaptureGroups) {
let range = results[0].range(at: r)
if range.length > 0 {
let start = string.index(string.startIndex, offsetBy: range.location)
let end = string.index(start, offsetBy: range.length)
let value = string[start..<end]
if !value.isEmpty {
let value_string = String(value)
values.append(value_string)
}
}
}
return values
At the moment this just looks for any groups. What I want is to return is a Dictionary list of ["group name":"value"]. I've tried from attached link ... "regex.Match(line).Groups" & "regex.GetGroupNames()" which do not exist.

How to use Swift NSRegularExpression to get uppercased letter?

I have a string like this:
"te_st" and like to replace all underscores followed by a character with the uppercased version of this character.
From "te_st" --> Found (regex: "_.") --------replace with next char (+ uppercase ("s"->"S")--------> "teSt"
From "te_st" ---> to "teSt"
From "_he_l_lo" ---> to "HeLLo"
From "an_o_t_h_er_strin_g" ---> to "anOTHErStrinG"
... but I can not really get it working using Swift's NSRegularExpression like this small snipped does:
var result = "te_st" // result should be teSt
result = try! NSRegularExpression(pattern: "_*").stringByReplacingMatches(in: result, range: NSRange(0..<result.count), withTemplate: ("$1".uppercased()))
There's no regular syntax to convert a match to uppercase. The code you posted is attempting to convert the string $1 to uppercase which is of course just $1. It isn't attempting to convert the value represented by the $1 match at runtime.
Here's another approach using a regular expression to find the _ followed by a lowercase letter. Those are enumerated and replaced with the uppercase letter.
extension String {
func toCamelCase() -> String {
let expr = try! NSRegularExpression(pattern: "_([a-z])")
var res = self
for match in expr.matches(in: self, range: NSRange(0..<res.count)).reversed() {
let range = Range(match.range, in: self)!
let letterRange = Range(match.range(at: 1), in: self)!
res.replaceSubrange(range, with: self[letterRange].uppercased())
}
return res
}
}
print("te_st".toCamelCase())
print("_he_l_lo".toCamelCase())
print("an_o_t_h_er_strin_g".toCamelCase())
This outputs:
teSt
HeLLo
anOTHErStrinG
Here is one implementation using NSRegularExpression. I use group match to get the character after _ and capitalize it and replace the string.
func capitalizeLetterAfterUnderscore(string: String) -> String {
var capitalizedString = string
guard let regularExpression = try? NSRegularExpression(pattern: "_(.)") else {
return capitalizedString
}
let matches = regularExpression.matches(in: string,
options: .reportCompletion,
range: NSMakeRange(0, string.count))
for match in matches {
let groupRange = match.range(at: 1)
let index = groupRange.location
let characterIndex = string.index(string.startIndex,
offsetBy: index)
let range = characterIndex ... characterIndex
let capitalizedCharacter = String(capitalizedString[characterIndex]).capitalized
capitalizedString = capitalizedString.replacingCharacters(in: range,
with: capitalizedCharacter)
}
capitalizedString = capitalizedString.replacingOccurrences(of: "_", with: "")
return capitalizedString
}
capitalizeLetterAfterUnderscore(string: "an_o_t_h_er_strin_g") // anOTHErStrinG
And here is other one without using regular expression. I made extension for method which could also be reused.
extension String {
func indexes(of character: String) -> [Index] {
precondition(character.count == 1, "character should be single letter string")
return enumerated().reduce([]) { (partial, component) in
let currentIndex = index(startIndex,
offsetBy: component.offset)
return String(self[currentIndex]) == character
? partial + [currentIndex]
: partial
}
}
func capitalizeLetter(after indexes: [Index]) -> String {
var modifiedString = self
for currentIndex in indexes {
guard let letterIndex = index(currentIndex,
offsetBy: 1,
limitedBy: endIndex)
else { continue }
let range = letterIndex ... letterIndex
modifiedString = modifiedString.replacingCharacters(in: range,
with: self[range].capitalized)
}
return modifiedString
}
}
let string = "an_o_t_h_er_strin_g"
let newString = string.capitalizeLetter(after: string.indexes(of: "_"))
.replacingOccurrences(of: "_",with: "")
You can use string range(of:, options:, range:) method with .regularExpression options to match the occurrences of "_[a-z]" and replace the subranges iterating the ranges found at reversed order by the character at the index after the range lowerbound uppercased:
let string = "an_o_t_h_er_strin_g"
let regex = "_[a-z]"
var start = string.startIndex
var ranges:[Range<String.Index>] = []
while let range = string.range(of: regex, options: .regularExpression, range: start..<string.endIndex) {
start = range.upperBound
ranges.append(range)
}
var finalString = string
for range in ranges.reversed() {
finalString.replaceSubrange(range, with: String(string[string.index(after: range.lowerBound)]).uppercased())
}
print(finalString) // "anOTHErStrinG\n"
The problem is that it is converting the string "$1" to upper case (which is, unsurprisingly unchanged, just "$1") and using "$1" as the template. If you want to use regex, you will have to enumerate through matches yourself.
The alternative is to split the string by _ characters and uppercase the first character of every substring (except the first) and joining it back together using reduce:
let input = "te_st"
let output = input.components(separatedBy: "_").enumerated().reduce("") { $0 + ($1.0 == 0 ? $1.1 : $1.1.uppercasedFirst()) }
Or, if your goal isn't to write code as cryptic as most regex, we can make that a tad more legible:
let output = input
.components(separatedBy: "_")
.enumerated()
.reduce("") { result, current in
if current.offset == 0 {
return current.element // because you don’t want the first component capitalized
} else {
return result + current.element.uppercasedFirst()
}
}
Resulting in:
teSt
Note, that uses this extension for capitalizing the first character:
extension String {
func uppercasedFirst(with locale: Locale? = nil) -> String {
guard count > 0 else { return self }
return String(self[startIndex]).uppercased(with: locale) + self[index(after: startIndex)...]
}
}
If you want to do sort of dynamic conversion with NSRegularExpression, you can subclass NSRegularExpression and override replacementString(for:in:offset:template:):
class ToCamelRegularExpression: NSRegularExpression {
override func replacementString(for result: NSTextCheckingResult, in string: String, offset: Int, template templ: String) -> String {
if let range = Range(result.range(at: 1), in: string) {
return string[range].uppercased()
} else {
return super.replacementString(for: result, in: string, offset: 0, template: templ)
}
}
}
func toCamelCase(_ input: String) -> String { //Make this a String extension if you prefer...
let regex = try! ToCamelRegularExpression(pattern: "_(.)")
return regex.stringByReplacingMatches(in: input, options: [], range: NSRange(0..<input.utf16.count), withTemplate: "$1")
}
print(toCamelCase("te_st")) //-> teSt
print(toCamelCase("_he_l_lo")) //-> HeLLo
print(toCamelCase("an_o_t_h_er_strin_g")) //-> anOTHErStrinG

Replace string between characters in swift

I have many strings, like this:
'This is a "table". There is an "apple" on the "table".'
I want to replace "table", "apple" and "table" with spaces. Is there a way to do it?
A simple regular expression:
let sentence = "This is \"table\". There is an \"apple\" on the \"table\""
let pattern = "\"[^\"]+\"" //everything between " and "
let replacement = "____"
let newSentence = sentence.replacingOccurrences(
of: pattern,
with: replacement,
options: .regularExpression
)
print(newSentence) // This is ____. There is an ____ on the ____
If you want to keep the same number of characters, then you can iterate over the matches:
let sentence = "This is table. There is \"an\" apple on \"the\" table."
let regularExpression = try! NSRegularExpression(pattern: "\"[^\"]+\"", options: [])
let matches = regularExpression.matches(
in: sentence,
options: [],
range: NSMakeRange(0, sentence.characters.count)
)
var newSentence = sentence
for match in matches {
let replacement = Array(repeating: "_", count: match.range.length - 2).joined()
newSentence = (newSentence as NSString).replacingCharacters(in: match.range, with: "\"" + replacement + "\"")
}
print(newSentence) // This is table. There is "__" apple on "___" table.
I wrote an extension to do this:
extension String {
mutating func replace(from: String, to: String, by new: String) {
guard let from = range(of: from)?.lowerBound, let to = range(of: to)?.upperBound else { return }
let range = from..<to
self = replacingCharacters(in: range, with: new)
}
func replaced(from: String, to: String, by new: String) -> String {
guard let from = range(of: from)?.lowerBound, let to = range(of: to)?.upperBound else { return self }
let range = from..<to
return replacingCharacters(in: range, with: new)
}
}