Update a Range in Swift 3 - swift

I'm trying to search a String for regex using the following snippet (it's in an extension for String):
var range = self.startIndex..<self.endIndex
while range.lowerBound < range.upperBound {
if let match = self.range(of: regex, options: .regularExpression, range: range, locale: nil) {
print(match)
range = ????? <============ this line, how do I update the range?
}
}
It will correctly find the first occurance, but then I don't know how to change the range to the position of the match to search the remainder of the string.

lowerBound and upperBound are immutable properties of the range,
so you have to create a new range, starting at match.upperBound.
Also the loop should terminate if no match is found.
That can be achieved by moving the binding
let match = ... into the where condition.
var range = self.startIndex..<self.endIndex
while range.lowerBound < range.upperBound,
let match = self.range(of: regex, options: .regularExpression, range: range) {
print(match) // the matching range
print(self.substring(with: match)) // the matched string
range = match.upperBound..<self.endIndex
}
This can still lead to an infinite loop if an empty string matches
the pattern (e.g. for regex = "^"). This can be solved, but
as an alternative, use NSRegularExpression to get a list of all
matches (see for example Swift extract regex matches).

Related

Split String or Substring with Regex pattern in Swift

First let me point out... I want to split a String or Substring with any character that is not an alphabet, a number, # or #. That means, I want to split with whitespaces(spaces & line breaks) and special characters or symbols excluding # and #
In Android Java, I am able to achieve this with:
String[] textArr = text.split("[^\\w_##]");
Now, I want to do the same in Swift. I added an extension to String and Substring classes
extension String {}
extension Substring {}
In both extensions, I added a method that returns an array of Substring
func splitWithRegex(by regexStr: String) -> [Substring] {
//let string = self (for String extension) | String(self) (for Substring extension)
let regex = try! NSRegularExpression(pattern: regexStr)
let range = NSRange(string.startIndex..., in: string)
return regex.matches(in: string, options: .anchored, range: range)
.map { match -> Substring in
let range = Range(match.range(at: 1), in: string)!
return string[range]
}
}
And when I tried to use it, (Only tested with a Substring, but I also think String will give me the same result)
let textArray = substring.splitWithRegex(by: "[^\\w_##]")
print("substring: \(substring)")
print("textArray: \(textArray)")
This is the out put:
substring: This,is a #random #text written for debugging
textArray: []
Please can Someone help me. I don't know if the problem if from my regex [^\\w_##] or from splitWithRegex method
The main reason why the code doesn't work is range(at: 1) which returns the content of the first captured group, but the pattern does not capture anything.
With just range the regex returns the ranges of the found matches, but I suppose you want the characters between.
To accomplish that you need a dynamic index starting at the first character. In the map closure return the string from the current index to the lowerBound of the found range and set the index to its upperBound. Finally you have to add manually the string from the upperBound of the last match to the end.
The Substring type is a helper type for slicing strings. It should not be used beyond a temporary scope.
extension String {
func splitWithRegex(by regexStr: String) -> [String] {
guard let regex = try? NSRegularExpression(pattern: regexStr) else { return [] }
let range = NSRange(startIndex..., in: self)
var index = startIndex
var array = regex.matches(in: self, range: range)
.map { match -> String in
let range = Range(match.range, in: self)!
let result = self[index..<range.lowerBound]
index = range.upperBound
return String(result)
}
array.append(String(self[index...]))
return array
}
}
let text = "This,is a #random #text written for debugging"
let textArray = text.splitWithRegex(by: "[^\\w_##]")
print(textArray) // ["This", "is", "a", "#random", "#text", "written", "for", "debugging"]
However in macOS 13 and iOS 16 there is a new API quite similar to the java API
let text = "This,is a #random #text written for debugging"
let textArray = Array(text.split(separator: /[^\w_##]/))
print(textArray)
The forward slashes indicate a regex literal

Replace in string with regex

I am struggling to modify captured value with regex.
For example, I wanna change "Hello, he is hero" to "HEllo, HE is HEro" using Regex.
I know there are ways to change this without regex, but it is just an example to show the problem. I actually use the regex instead of just he, but I cannot provide it here. That is why using regex is required.
The code below somehow does not work. Are there any ways to make it work?
"Hello, he is hero".replacingOccurrences(
of: #"(he)"#,
with: "$1".uppercased(), // <- uppercased is not applied
options: .regularExpression
)
You need to use your regex in combination with Range (range(of:)) to find matches and then replace each found range separately
Here is a function as an extension to String that does this by using range(of:) starting from the start of the string and then moving the start index to match from forward to after the last match. The actual replacement is done inside a separate function that is passed as an argument
extension String {
func replace(regex: String, with replace: (Substring) -> String) -> String {
var string = self
var startIndex = self.startIndex
let endIndex = self.endIndex
while let range = string.range(of: regex, options: [.regularExpression] , range: startIndex..<endIndex) {
if range.isEmpty {
startIndex = string.index(startIndex, offsetBy: 1)
if startIndex >= endIndex { break }
continue
}
string.replaceSubrange(range, with: replace(string[range]))
startIndex = range.upperBound
}
return string
}
}
Example where we do an case insensitive search for words starting with "he" and replace each match with the uppercased version
let result = "Hello, he is hero. There he is".replace(regex: #"(?i)\bhe"#) {
$0.uppercased()
}
Output
HEllo, HE is HEro. There HE is
You can try NSRegularExpression. Something like:
import Foundation
var sourceStr = "Hello, he is hero"
let regex = try! NSRegularExpression(pattern: "(he)")
let matches = regex.matches(in: sourceStr, range: NSRange(sourceStr.startIndex..., in: sourceStr))
regex.enumerateMatches(in: sourceStr, range: NSRange(sourceStr.startIndex..., in: sourceStr)) { (match, _, _) in
guard let match = match else { return }
guard let range = Range(match.range, in: sourceStr) else { return }
let sub = sourceStr[range]
sourceStr = sourceStr.replacingOccurrences(of: sub, with: sub.uppercased(), options: [], range: range)
}
print(sourceStr)
this is the solution i can provide
var string = "Hello, he is hero"
let occurrence = "he"
string = string.lowercased().replacingOccurrences(
of: occurrence,
with: occurrence.uppercased(),
options: .regularExpression
)
print(string)

How to exit a function where no regex matches

Attempting to create a function that uses regex matches to return an array of NSRange values to use with a UITextView to allow the user to click through the matched words using animation.
I assume the solution is to break out of the function if there is no regex match. I cannot figure out how to do this where the function requires a NSRange value.
Moreover, when there is no match, the regex function matches does not return nil. Instead, it automatically returns an empty array which appears to make the guard statement useless.
Here is the function:
func rangeOfSearchText(searchString: String, UIText: String) -> [NSRange] {
var matches:[NSTextCheckingResult]?
let regex = try! NSRegularExpression(pattern: searchString, options: .caseInsensitive)
matches = regex.matches(in: UIText, options: [], range: NSRange(location: 0, length: UIText.count))
guard let find = matches else {
//return need to find a way to break out of function if nil without returning an NSRange object...
}
var rangeArray:[NSRange] = []
for match in find {
rangeArray.append(match.range(at: 0))
}
return rangeArray
}
let sString = "z"
let longString = "I need a solution."
let test = rangeOfSearchText(searchString: sString, UIText: longString)
The above returns an empty array.

how to run multiples NSRegularExpression once

I have a bunch of NSRegularExpression and I want to run it once. Anyone knows how to do it ?
For the moment I do it in a .forEach, for performance reasons I do not think this is the best idea
Each NSRegularExpression needs to match a different pattern, after the matching I need to deal with each different kind of match. As example if I match with the first regex in my array I need to make something different from the second etc...
let test: String = "Stuff"
let range: NSRange = // a range
var regexes = [NSRegularExpression] = // all of my regexes
regexes.forEach { $0.matches(in: text, options: [], range: range) }
Thanks for you help
You may be able to evaluate several regular expressions as one if you concatenate them using capture groups and an OR expressions.
If you want to search for: language, Objective-C and Swift strings you should use a pattern like this: (language)|(Objective-C)|(Swift). Each capture group has an order number, so if language is found in the source string the match object provides the index number.
You can used the code in this playground sample:
import Foundation
let sourceString: String = "Swift is a great language to program, but don't forget Objective-C."
let expresions = [ "language", // Expression 0
"Objective-C", // Expression 1
"Swift" // Expression 2
]
let pattern = expresions
.map { "(\($0))" }
.joined(separator: "|") // pattern is defined as : (language)|(Objective-C)|(Swift)
let regex = try? NSRegularExpression(pattern: pattern, options: [])
let matches = regex?.matches(in: sourceString, options: [],
range: NSRange(location: 0, length: sourceString.utf16.count))
let results = matches?.map({ (match) -> (Int, String) in // Array of type (Int: String) which
// represents index of expression and
// string capture
let index = (1...match.numberOfRanges-1) // Go through all ranges to test which one was used
.map{ Range(match.range(at: $0), in: sourceString) != nil ? $0 : nil }
.compactMap { $0 }.first! // Previous map return array with nils and just one Int
// with the correct position, lets apply compactMap to
// get just this number
let foundString = String(sourceString[Range(match.range(at: 0), in: sourceString)!])
let position = match.range(at: 0).location
let niceReponse = "\(foundString) [position: \(position)]"
return (index - 1, niceReponse) // Let's substract 1 to index in order to match zero based array index
})
print("Matches: \(results?.count ?? 0)\n")
results?.forEach({ result in
print("Group \(result.0): \(result.1)")
})
If you run it the result is:
How many matches: 3
Expression 2: Swift [position: 0]
Expression 0: language [position: 17]
Expression 1: Objective-C [position: 55]
I hope I understood correctly your question and this code helps you.

How can I substring this string?

how can I substring the next 2 characters of a string after a certian character. For example I have a strings str1 = "12:34" and other like str2 = "12:345. I want to get the next 2 characters after : the colons.
I want a same code that will work for str1 and str2.
Swift's substring is complicated:
let str = "12:345"
if let range = str.range(of: ":") {
let startIndex = str.index(range.lowerBound, offsetBy: 1)
let endIndex = str.index(startIndex, offsetBy: 2)
print(str[startIndex..<endIndex])
}
It is very easy to use str.index() method as shown in #MikeHenderson's answer, but an alternative to that, without using that method is iterating through the string's characters and creating a new string for holding the first two characters after the ":", like so:
var string1="12:458676"
var nr=0
var newString=""
for c in string1.characters{
if nr>0{
newString+=String(c)
nr-=1
}
if c==":" {nr=2}
}
print(newString) // prints 45
Hope this helps!
A possible solution is Regular Expression,
The pattern checks for a colon followed by two digits and captures the two digits:
let string = "12:34"
let pattern = ":(\\d{2})"
let regex = try! NSRegularExpression(pattern: pattern, options: [])
if let match = regex.firstMatch(in: string, range: NSRange(location: 0, length: string.characters.count)) {
print((string as NSString).substring(with: match.rangeAt(1)))
}