Regex to do something only if string has prefix - swift

I have string "something/w1/w2/". I want to get all string between "/" characters, only if I my string is prefixed by "something".
For example, if string is "something/w1/w2/" I want to get matches "w1", "w2".
And if it is "otherThing/w1/w2/" I don't want to get any matches.
Currently, I am using "(?<=something/).+?(?=/)", but in "something/w1/w2/" it returns only "w1". How can I get also "w2"?

You could use match something at the start of the string or get iterative matches using the \G anchor matching / and a capturing group that matches any char except a /
The matches are in the first capturing group.
(?:^something|\G(?!^))/([^/\r\n]+)
With double escapes:
(?:^something|\\G(?!^))/([^/\\r\\n]+)
(?: Non capturing group
^something Match something from the start of the string
| Or
\G(?!^) Assert position at the end of previous match, not at the start
) Close non capturing group
/ match literally
([^/\r\n]+) Capture group 1 Match 1+ times any char except a / or newline
Regex demo

You can achieve this without using regex but plain Swift. Check if the string has a prefix and then split by slashes.
func extractStringsBetweenSlashes(from string: String, ifPrefix prefix: String) -> [Substring]? {
guard string.hasPrefix(prefix) else { return nil }
return string.dropFirst(prefix.count).split(separator: "/")
}
print(extractStringsBetweenSlashes(from: "something/a/b/c/d/e", ifPrefix: "something/")) // Optional(["a", "b", "c", "d", "e"])
print(extractStringsBetweenSlashes(from: "something/abcdef/", ifPrefix: "something/")) // Optional(["abcdef"])
print(extractStringsBetweenSlashes(from: "else/a/b/c/d/e", ifPrefix: "something/")) // nil

You may use
let string = "something/w1/w2/"
extension String {
func findconsecutiveMatches() -> [[String]] {
let regex = try? NSRegularExpression(pattern: "(?:(?<!\\A)\\G|^something)/([^/]+)", options: [])
if let matches = regex?.matches(in: self, options:[], range:NSMakeRange(0, self.count)) {
return matches.map { match in
return (1..<match.numberOfRanges).map {
let rangeBounds = match.range(at: $0)
guard let range = Range(rangeBounds, in: self) else {
return ""
}
return String(self[range])
}
}
} else {
return []
}
}
}
let result = string.findconsecutiveMatches().flatMap { $0 }
print(result)
// => ["w1", "w2"]
The regex is
(?:(?<!\A)\G|^something)/([^/]+)
Details
(?:(?<!\A)\G|^something) - either the end of the previous match or something at the start of the string
/ - a / char
([^/]+) - Group 1: any 1+ more chars other than /.

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)

Find multiple quoted words in a string with regex

My app supports 5 languages. I have a string which has some double quotes in it. This string is translated into 5 languages in the localizable.strings files.
Example:
title_identifier = "Hi \"how\", are \"you\"";
I would like to bold out "how" and "you" in this string by finding the range of these words. So I am trying to fetch these quoted words out of the string and the result would be an array containing "how" and "you" or their range.
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 []
}
}
matches(for: "(?<=\")[^\"]*(?=\")", in: str)
The result is: ["how", ", are ", "you"] rather than ["how","you"]. I think this regex needs some addition to allow it to search for next quote once two quotes are found, so to avoid the words in between quotes.
Your problem is in the use of lookarounds that do not consume text but check if their patterns match and return either true or false. See your regex in action, the , are matches because the last " in the previous match was not consumed, the regex index remained right after w, so the next match could start with ". You need to use a consuming pattern here, "([^"]*)".
However, your code will only return full matches. You can just trim the first and last "s here with .map {$0.trimmingCharacters(in: ["\""])}, as the regex only matches one quote at the start and end:
matches(for: "\"[^\"]*\"", in: str).map {$0.trimmingCharacters(in: ["\""])}
Here is the regex demo.
Alternatively, access Group 1 value by appending (at: 1) after $0.range:
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(at: 1), in: text)!])
}
} catch let error {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
let str = "Hi \"how\", are \"you\""
print(matches(for: "\"([^\"]*)\"", in: str))
// => ["how", "you"]

Regular expressions in swift

I'm bit confused by NSRegularExpression in swift, can any one help me?
task:1 given ("name","john","name of john")
then I should get ["name","john","name of john"]. Here I should avoid the brackets.
task:2 given ("name"," john","name of john")
then I should get ["name","john","name of john"]. Here I should avoid the brackets and extra spaces and finally get array of strings.
task:3 given key = value // comment
then I should get ["key","value","comment"]. Here I should get only strings in the line by avoiding = and //
I have tried below code for task 1 but not passed.
let string = "(name,john,string for user name)"
let pattern = "(?:\\w.*)"
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!")
}
Thanks in advance.
RegEx in Swift
These posts might help you to explore regular expressions in swift:
Does a string match a pattern?
Swift extract regex matches
How can I use String slicing subscripts in Swift 4?
How to use regex with Swift?
Swift 3 - How do I extract captured groups in regular expressions?
How to group search regular expressions using swift?
Task 1 & 2
This expression might help you to match your desired outputs for both Task 1 and 2:
"(\s+)?([a-z\s]+?)(\s+)?"
Based on Rob's advice, you could much reduce the boundaries, such as the char list [a-z\s]. For example, here, we can also use:
"(\s+)?(.*?)(\s+)?"
or
"(\s+)?(.+?)(\s+)?"
to simply pass everything in between two " and/or space.
RegEx
If this wasn't your desired expression, you can modify/change your expressions in regex101.com.
RegEx Circuit
You can also visualize your expressions in jex.im:
JavaScript Demo
const regex = /"(\s+)?([a-z\s]+?)(\s+)?"/gm;
const str = `"name","john","name of john"
"name"," john","name of john"
" name "," john","name of john "
" name "," john"," name of john "`;
const subst = `\n$2`;
// The substituted value will be contained in the result variable
const result = str.replace(regex, subst);
console.log('Substitution result: ', result);
Task 3
This expression might help you to design an expression for the third task:
(.*?)([a-z\s]+)(.*?)
const regex = /(.*?)([a-z\s]+)(.*?)/gm;
const str = `key = value // comment
key = value with some text // comment`;
const subst = `$2,`;
// The substituted value will be contained in the result variable
const result = str.replace(regex, subst);
console.log('Substitution result: ', result);
Separate the string by non alpha numeric characters except white spaces. Then trim the elements with white spaces.
extension String {
func words() -> [String] {
return self.components(separatedBy: CharacterSet.alphanumerics.inverted.subtracting(.whitespaces))
.filter({ !$0.isEmpty })
.map({ $0.trimmingCharacters(in: .whitespaces) })
}
}
let string1 = "(name,john,string for user name)"
let string2 = "(name, john,name of john)"
let string3 = "key = value // comment"
print(string1.words())//["name", "john", "string for user name"]
print(string2.words())//["name", "john", "name of john"]
print(string3.words())//["key", "value", "comment"]
Here I have done with after understanding all of above comments.
let text = """
Capturing and non-capturing groups are somewhat advanced topics. You’ll encounter examples of capturing and non-capturing groups later on in the tutorial
"""
extension String {
func rex (_ expr : String)->[String] {
return try! NSRegularExpression(pattern: expr, options: [.caseInsensitive])
.matches(in: self, options: [], range: NSRange(location: 0, length: self.count))
.map {
String(self[Range($0.range, in: self)!])
}
}
}
let r = text.rex("(?:\\w+-\\w+)") // pass any rex
A single pattern, works for test:1...3, in Swift.
let string =
//"(name,john,string for user name)" //test:1
//#"("name"," john","name of john")"# //test:2
"key = value // comment" //test:3
let pattern = #"(?:\w+)(?:\s+\w+)*"# //Swift 5+ only
//let pattern = "(?:\\w+)(?:\\s+\\w+)*"
do {
let regex = try NSRegularExpression(pattern: pattern)
let matches = regex.matches(in: string, range: NSRange(0..<string.utf16.count))
let matchingWords = matches.map {
String(string[Range($0.range, in: string)!])
}
print(matchingWords) //(test:3)->["key", "value", "comment"]
} catch {
print("Regex was bad!")
}
Let’s consider:
let string = "(name,José,name is José)"
I’d suggest a regex that looks for strings where:
It’s the substring either after the ( at the start of the full string or after a comma, i.e., look behind assertion of (?<=^\(|,);
It’s the substring that does not contain , within it, i.e., [^,]+?;
It’s the substring that is terminated by either a comma or ) at the end of the full string, i.e., look ahead assertion of (?=,|\)$), and
If you want to have it skip white space before and after the substrings, throw in the \s*+, too.
Thus:
let pattern = #"(?<=^\(|,)\s*+([^,]+?)\s*+(?=,|\)$)"#
let regex = try! NSRegularExpression(pattern: pattern)
regex.enumerateMatches(in: string, range: NSRange(string.startIndex..., in: string)) { match, _, _ in
if let nsRange = match?.range(at: 1), let range = Range(nsRange, in: string) {
let substring = String(string[range])
// do something with `substring` here
}
}
Note, I’m using the Swift 5 extended string delimiters (starting with #" and ending with "#) so that I don’t have to escape my backslashes within the string. If you’re using Swift 4 or earlier, you’ll want to escape those back slashes:
let pattern = "(?<=^\\(|,)\\s*+([^,]+?)\\s*+(?=,|\\)$)"

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