Find multiple quoted words in a string with regex - swift

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

Related

Regex to do something only if string has prefix

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

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*+(?=,|\\)$)"

Swift: regex function return me empty array

hello guys i have problem with working regex in swift
i'm using this regex pattern https://regexr.com/3u4on and this works fine in this website
but in swift code it won't work?
can you help with this problem
I'm using this code in swift:
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 []
}
}
var videoButtonWithAtag = self.matches(for: "<a.[^>]*\\bhref\\s*=\\s*\"[^\"]*mp4.*?</a>", in: html)
print(videoButtonWithAtag) // it's empty
By using .* you are missing matches with line break. You should include them as well. So use [\S\s]* instead.
This should work:
let pattern = "<a.[^>]*\\bhref\\s*=\\s*\"[^\"]*mp4[\\S\\s]*?</a>"

Cannot find Substring "n't"

I am trying to determine whether an input string contains "n't" or "not".
For example, if the input were:
let part = "Hi, I can't be found!"
I want to find the presence of the negation.
I have tried input.contains, .range, and NSRegularExpression. All of these succeed in finding "not", but fail to find "n't". I have tried escaping the character as well.
'//REGEX:
let negationPattern = "(?:n't|[Nn]ot)"
do {
let regex = try NSRegularExpression(pattern: negationPattern)
let results = regex.matches(in: text,range: NSRange(part.startIndex..., in: part))
print("results are \(results)")
negation = (results.count > 0)
} catch let error {
print("invalid regex: \(error.localizedDescription)")
}
//.CONTAINS
if part.contains("not") || part.contains("n't"){
print("negation present in part")
negation = true
}
//.RANGE (showing .regex option; also tried without)
if part.lowercased().range(of:"not", options: .regularExpression) != nil || part.lowercased().range(of:"n't", options: .regularExpression) != nil {
print("negation present in part")
negation = true
}
Here is a picture:
This is a bit tricky, and the screenshot is actually what gives it away: your regex pattern has a plain single quote in it, but the input text has a "smart" or "curly" apostrophe in it. The difference is subtle:
Regular: '
Smart: ’
Lots of text fields will automatically replace regular single quotes with "smart" apostrophes when they think it's appropriate. Your regex, however, only matches the plain single quote, as evidenced by this tiny test:
func isNegation(input text: String) -> Bool {
let negationPattern = "(?:n't|[Nn]ot)"
let regex = try! NSRegularExpression(pattern: negationPattern)
let matches = regex.matches(in: text,range: NSRange(text.startIndex..., in: text))
return matches.count > 0
}
for input in ["not", "n't", "n’t"] {
print("\"\(input)\" is negation: \(isNegation(input: input) ? "YES" : "NO")")
}
This prints:
"not" is negation: YES
"n't" is negation: YES
"n’t" is negation: NO
If you want to continue using a regex for this problem, you'll need to modify it to match this kind of punctuation character, and avoid assuming all your input text includes "plain" single quotes.

I can't include ' symbol to Regular Expressions

I try to include ' symbol to Regular Expressions
I use this function
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 {
text.substring(with: Range($0.range, in: text)!)
}
} catch let error {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
and this Regular Expressions
let matched = matches(for: "^[‘]|[0-9]|[a-zA-Z]+$", in: string)
when I search I can find numbers and english letters
but not ' symbol
I guess that what you really want is this:
"['0-9a-zA-Z]+"
Note that I have removed the ^ (text start) and $ (text end) characters because then your whole text would have to match.
I have merged the groups because otherwise you would not match the text as a whole word. You would get separate apostrophe and then the word.
I have changed the ‘ character into the proper ' character. The automatic conversion from the simple apostrophe is caused by iOS 11 Smart Punctuation. You can turn it off on an input using:
input.smartQuotesType = .no
See https://developer.apple.com/documentation/uikit/uitextinputtraits/2865931-smartquotestype