regular expressions: simplified form? - swift

I'm finally learning swift. The documentation that I've seen for regex in swift consist of something like the following:
let testString = "hat"
let range = NSRange(location: 0, length: testString.utf16.count)
let regex = try! NSRegularExpression(pattern: "[a-z]at")
let r = regex.firstMatch(in: testString, options: [], range: range) != nil
print("ns-based regex match?: ", r)
Is this the preferred/only way of doing this or is there an updated technique?
It's a bit verbose.

We’d often just use range(of:options:range:locale:) with a .regularExpression:
let testString = "foo hat"
if let range = testString.range(of: "[a-z]at", options: .regularExpression) {
print(testString[range])
}
If you don't need some of the more advanced NSRegularExpression options, the above is bit simpler.

Related

NSAttributedString: turn (multiple) #username[userid] mentions into clickable #username links

I am in the process of writing code to display mentions within an NSAttributedString, which need to link out to a user profile. The format of the mentions is #username[userid], which would need to be displayed as simply #username, which is tappable.
I have the code working so far that the username becomes clickable, but I now need to remove the [userid] part, which of course modifies the length of the string so that ranges don't match anymore, etc. Not sure how I can solve this.
import Foundation
import UIKit
let comment = "Hello #kevin[1], #john and #andrew[2]!"
let wholeRange = NSRange(comment.startIndex..<comment.endIndex, in: comment)
let regex = try NSRegularExpression(pattern: #"(#[\w.-#]+)\[(\d+)\]"#)
let attributedString = NSMutableAttributedString(string: comment)
regex.enumerateMatches(in: comment, options: [], range: wholeRange) { match, _, _ in
guard let match = match else {
return
}
let userIdRange = Range(match.range(at: 2), in: comment)!
let userId = comment[userIdRange]
let usernameRange = match.range(at: 1)
attributedString.addAttribute(NSAttributedString.Key.link, value: URL(string: "test://profile/\(userId)")!, range: usernameRange)
}
print(attributedString)
The result right now can be represented like this, when printed:
Hello {
}#kevin{
NSLink = "test://profile/1";
}[1], #john and {
}#andrew{
NSLink = "test://profile/2";
}[2]!{
}
So #kevin and #andrew are links, #john is not (which is expected!), but the user ids are still visible. Surely this is a problem that has been solved before but I can't find any examples, not sure what keywords to search for. There are plenty of questions about detecting usernames/mentions in strings, and even more about making links in NSAttributedString, but that's not the problem I am trying to solve.
How would I turn the #username[userid] mentions into clickable #username links, so that the [userid] part is hidden?
You just need to get all the matching ranges, iterate them in reverse order, add the link to it and then replace the whole range with the name. Something like:
let comment = "Hello #kevin[1], #john and #andrew[2]!"
let attributedString = NSMutableAttributedString(string: comment)
let regex = try NSRegularExpression(pattern: #"(#[\w.-#]+)\[(\d+)\]"#)
var ranges: [(NSRange,NSRange,NSRange)] = []
regex.enumerateMatches(in: comment, range: NSRange(comment.startIndex..., in: comment)) { match, _, _ in
guard let match = match else {
return
}
ranges.append((match.range(at: 0),
match.range(at: 1),
match.range(at: 2)))
}
ranges.reversed().forEach {
let userId = attributedString.attributedSubstring(from: $0.2).string
let username = attributedString.attributedSubstring(from: $0.1).string
attributedString.addAttribute(.link, value: URL(string: "test://profile/\(userId)")!, range: $0.0)
attributedString.replaceCharacters(in: $0.0, with: username)
}
print(attributedString)
This will print
Hello {
}#kevin{
NSLink = "test://profile/1";
}, #john and {
}#andrew{
NSLink = "test://profile/2";
}!{
}
Quickly done:
let comment = "Hello #kevin[1], #john and #andrew[2]!"
let attributedString = NSMutableAttributedString(string: comment)
let wholeRange = NSRange(attributedString.string.startIndex..<attributedString.string.endIndex, in: attributedString.string)
let regex = try NSRegularExpression(pattern: #"(#[\w.-#]+)\[(\d+)\]"#)
let matches = regex.matches(in: attributedString.string, options: [], range: wholeRange)
matches.reversed().forEach { aResult in
let fullMatchRange = Range(aResult.range(at: 0), in: attributedString.string)! //#kevin[1]
let replacementRange = Range(aResult.range(at: 1), in: attributedString.string)! //#kevin
let userIdRange = Range(aResult.range(at: 2), in: attributedString.string)! // 1
let atAuthor = String(attributedString.string[replacementRange])
attributedString.addAttribute(.link,
value: URL(string: "test://profile/\(attributedString.string[userIdRange])")!,
range: NSRange(fullMatchRange, in: attributedString.string))
attributedString.replaceCharacters(in: NSRange(fullMatchRange, in: attributedString.string),
with: atAuthor)
}
print(attributedString)
Output:
Hello {
}#kevin{
NSLink = "test://profile/1";
}, #john and {
}#andrew{
NSLink = "test://profile/2";
}!{
}
What's to see:
I changed the pattern, for easy captures. See the sample in comment in the forEach().
I used matches in reverse order, else the ranges won't be accurate anymore!
I kept playing with attributedString.string instead of comment in case it's "unsync".

Swift replace occurrence of string with condition

I have string like below
<p><strong>I am a strongPerson</strong></p>
I want to covert this string like this
<p><strong>I am a weakPerson</strong></p>
When I try below code
let old = "<p><strong>I am a strongPerson</strong></p>"
let new = old.replacingOccurrences(of: "strong", with: "weak")
print("\(new)")
I am getting output like
<p><weak>I am a weakPerson</weak></p>
But I need output like this
<p><strong>I am a weakPerson</strong></p>
My Condition here is
1.It has to replace only if word does not contain these HTML Tags like "<>".
Help me to get it. Thanks in advance.
You can use a regular expression to avoid the word being in a tag:
let old = "strong <p><strong>I am a strong person</strong></p> strong"
let new = old.replacingOccurrences(of: "strong(?!>)", with: "weak", options: .regularExpression, range: nil)
print(new)
I added some extra uses of the word "strong" to test edge cases.
The trick is the use of (?!>) which basically means to ignore any match that has a > at the end of it. Look at the documentation for NSRegularExpression and find the documentation for the "negative look-ahead assertion".
Output:
weak <p><strong>I am a weak person</strong></p> weak
Try the following:
let myString = "<p><strong>I am a strongPerson</strong></p>"
if let regex = try? NSRegularExpression(pattern: "strong(?!>)") {
let modString = regex.stringByReplacingMatches(in: myString, options: [], range: NSRange(location: 0, length: myString.count), withTemplate: "weak")
print(modString)
}

NSRegularExpression does not match anything after CRLF. Why?

In Swift 4.2, NSRegularExpression does not match anything after CRLF. Why?
let str = "\r\nfoo"
let regex = try! NSRegularExpression(pattern: "foo")
print(regex.firstMatch(in: str, range: NSRange(location: 0, length: str.count))) // => nil
If you remove "\r" or "\n", you get a instance of NSTextCheckingResult.
In Swift 4 a new initializer of NSRange was introduced to convert reliably Range<String.Index> to NSRange.
Use it always, it solves your issue.
let str = "\r\nfoo"
let regex = try! NSRegularExpression(pattern: "foo")
print(regex.firstMatch(in: str, range: NSRange(str.startIndex..., in: str)))

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

Find String from String using NSRegularExpression Swift

I want to fetch url of images from the String using NSRegularExpression.
func findURlUsingExpression(urlString: String){
do{
let expression = try NSRegularExpression(pattern: "\\b(http|https)\\S*(jpg|png)\\b", options: NSRegularExpressionOptions.CaseInsensitive)
let arrMatches = expression.matchesInString(urlString, options: NSMatchingOptions(rawValue: 0), range: NSMakeRange(0, urlString.characters.count))
for match in arrMatches{
let matchText = urlString.substringWithRange(Range(urlString.startIndex.advancedBy(match.range.location) ..< urlString.startIndex.advancedBy(match.range.location + match.range.length)))
print(matchText)
}
}catch let error as NSError{
print(error.localizedDescription)
}
}
It works with just the simple string but not with the HTML String.
Working Example:
let tempString = "jhgsfjhgsfhjgajshfgjahksfgjhs http://jhsgdfjhjhggajhdgsf.jpg jahsfgh asdf ajsdghf http://jhsgdfjhjhggajhdgsf.png"
findURlUsingExpression(tempString)
Output:
http://jhsgdfjhjhggajhdgsf.jpg
http://jhsgdfjhjhggajhdgsf.png
But not working with this one: http://www.writeurl.com/text/478sqami3ukuug0r0bdb/i3r86zlza211xpwkdf2m
Don't roll your own regex if you can help it. Easiest and safest way is to use NSDataDetector. By using NSDataDetector you leverage a pre-built, heavily used parsing tool which should already have most of the bugs shaken out of it.
Here is a good article on it: NSData​Detector
NSDataDetector is a subclass of NSRegularExpression, but instead of
matching on an ICU pattern, it detects semi-structured information:
dates, addresses, links, phone numbers and transit information.
import Foundation
let tempString = "jhgsfjhgsfhjgajshfgjahksfgjhs http://example.com/jhsgdfjhjhggajhdgsf.jpg jahsfgh asdf ajsdghf http://example.com/jhsgdfjhjhggajhdgsf.png"
let types: NSTextCheckingType = [.Link]
let detector = try? NSDataDetector(types: types.rawValue)
detector?.enumerateMatchesInString(tempString, options: [], range: NSMakeRange(0, (tempString as NSString).length)) { (result, flags, _) in
if let result = result?.URL {
print(result)
}
}
// => "http://example.com/jhsgdfjhjhggajhdgsf.jpg"
// => "http://example.com/jhsgdfjhjhggajhdgsf.png"
The example is from that site, adapted to search for a link.