Regex find/replace list of keys (Swift 3) - swift

I have a HTML file embedded into my xCode project which has tags within it such as:
{DESCRIPTION}
{LOCATION}
{TIME_SUBMITTED}
I load the contents of the file into a String with:
let url = Bundle.main.url(forResource: "emailTemplate", withExtension: "html")
var messageBody:NSString!
do { messageBody = try String(contentsOf: url!) as NSString! }
catch { messageBody = "" }
Now I have populated "messageBody" I need to find and replace the tags based on my UI, for example:
1) find "{DESCRIPTION}" and replace it with lblDescription.text
2) find "{LOCATION}" and replace it with lblLocation.text
I am trying to use code similar to:
messageBody.enumerateSubstrings(in: NSMakeRange(0, messageBody.length), options: .byWords) { (substring, substringRange, enclosingRange, _) -> () in
print(substring!)
}
However, I am completely useless with regex and could do with some assistance to find and replace if the substring equals a tag. Any ideas please?

You don't need regex for this. Repeated calls to replacingOccurrences will do:
import Foundation
let emailTemplate = "Hello {USER}\n" +
"{DESCRIPTION}\n" +
"\n" +
"Regards."
let email = emailTemplate
.replacingOccurrences(of: "{USER}", with: "John Smith")
.replacingOccurrences(of: "{DESCRIPTION}", with: "Have a nice day")
print(email)

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 5.1 - is there a clean way to deal with locations of substrings/ pattern matches

I'm very, very new to Swift and admittedly struggling with some of its constructs. I have to work with a text file and do many manipulations - here's an example to illustrate the point:
let's say I have a text like this (multi line)
Mary had a little lamb
#name: a name
#summary: a paragraph of text
{{something}}
a whole bunch of multi-line text
x----------------x
I want to be able to do simple things like find the location of #name, then split it to get the name and so on. I've done this in javascript and it was pretty simple with the use of substr and the regex matches.
In swift, which is supposed to be swift and easy and what not, I'm finding this exceedingly confusing.
Can someone help with how one might do
Find the location of the start of a substring
Extract all text between from the end of a substring to the end of text
Sorry if this is trivial - but the Apple documentation feels very complicated, and lots of examples are years old. I can't also seem to find easy application of regex.
You can use string range(of: String) method to find the range of your string, get its upperBound and search for the end of the line from that position of the string:
Playground testing:
let sentence = """
Mary had a little lamb
#name: a name
#summary: a paragraph of text
{{something}}
a whole bunch of multi-line text
"""
if let start = sentence.range(of: "#name:")?.upperBound,
let end = sentence[start...].range(of: "\n")?.lowerBound {
let substring = sentence[start..<end]
print("name:", substring)
}
If you need to get the string from there to the end of the string you can use PartialRangeFrom:
if let start = sentence.range(of: "#summary:")?.upperBound {
let substring = sentence[start...]
print("summary:", substring)
}
If you find yourself using that a lot you can extend StringProtocol and create your own method:
extension StringProtocol {
func substring<S:StringProtocol,T:StringProtocol>(between start: S, and end: T, options: String.CompareOptions = []) -> SubSequence? {
guard
let lower = range(of: start, options: options)?.upperBound,
let upper = self[lower...].range(of: end, options: options)?.lowerBound
else { return nil }
return self[lower..<upper]
}
func substring<S:StringProtocol>(after string: S, options: String.CompareOptions = []) -> SubSequence? {
guard
let lower = range(of: string, options: options)?.upperBound else { return nil }
return self[lower...]
}
}
Usage:
let name = sentence.substring(between: "#name:", and: "\n") // " a name"
let sumary = sentence.substring(after: "#summary:") // " a paragraph of text\n\n{{something}}\n\na whole bunch of multi-line text"
You can use regular expressions as well:
let name = sentence.substring(between: "#\\w+:", and: "\\n", options: .regularExpression) // " a name"
You can do this with range() and distance():
let str = "Example string"
let range = str.range(of: "amp")!
print(str.distance(from: str.startIndex, to: range.lowerBound)) // 2
let lastStr = str[range.upperBound...]
print(lastStr) // "le string"

Find characters inside quotation marks in String

I'm trying to pull out the parts of a string that are in quotation marks, i.e. in "Rouge One" is an awesome movie I want to extract Rouge One.
This is what I have so far but can't figure out where to go from here: I create a copy of the text so that I can remove the first quotation mark so that I can get the index of the second.
if text.contains("\"") {
guard let firstQuoteMarkIndex = text.range(of: "\"") else {return}
var textCopy = text
let textWithoutFirstQuoteMark = textCopy.replacingCharacters(in: firstQuoteMarkIndex, with: "")
let secondQuoteMarkIndex = textCopy.range(of: "\"")
let stringBetweenQuotes = text.substring(with: Range(start: firstQuoteMarkIndex, end: secondQuoteMarkIndex))
}
There is no need to create copies or to replace substrings for this task.
Here is a possible approach:
Use text.range(of: "\"") to find the first quotation mark.
Use text.range(of: "\"", range:...) to find the second quotation mark, i.e. the first one after the range found in step 1.
Extract the substring between the two ranges.
Example:
let text = " \"Rouge One\" is an awesome movie"
if let r1 = text.range(of: "\""),
let r2 = text.range(of: "\"", range: r1.upperBound..<text.endIndex) {
let stringBetweenQuotes = text.substring(with: r1.upperBound..<r2.lowerBound)
print(stringBetweenQuotes) // "Rouge One"
}
Another option is a regular expression search with "positive lookbehind" and "positive lookahead" patterns:
if let range = text.range(of: "(?<=\\\").*?(?=\\\")", options: .regularExpression) {
let stringBetweenQuotes = text.substring(with: range)
print(stringBetweenQuotes)
}
var rouge = "\"Rouge One\" is an awesome movie"
var separated = rouge.components(separatedBy: "\"") // ["", "Rouge One", " is an awesome movie"]
separated.dropFirst().first
I would use .components(separatedBy:)
let stringArray = text.components(separatedBy: "\"")
Check if stringArray count is > 2 (there is at least 2 quotes).
Check if stringArray count is odd, aka count % 2 == 1.
If it is odd, all the even indices are between 2 quotes and they are what you want.
If it is even, all the even indices - 1 are between 2 quotes (the last one doesn't have an end quote).
This will allow you to also capture multiple sets of quoted strings, like:
"Rogue One" is a "Star Wars" movie.
Another option is to use regular expressions to find pairs of quotes:
let pattern = try! NSRegularExpression(pattern: "\\\"([^\"]+)\\\"")
// Small helper methods making it easier to work with enumerateMatches(in:...)
extension String {
subscript(utf16Range range: Range<Int>) -> String? {
get {
let start = utf16.index(utf16.startIndex, offsetBy: range.lowerBound)
let end = utf16.index(utf16.startIndex, offsetBy: range.upperBound)
return String(utf16[start..<end])
}
}
var fullUTF16Range: NSRange {
return NSRange(location: 0, length: utf16.count)
}
}
// Loop through *all* quoted substrings in the original string.
let str = "\"Rogue One\" is an awesome movie"
pattern.enumerateMatches(in: str, range: str.fullUTF16Range) { (result, flags, stop) in
// rangeAt(1) is the range representing the characters in the 1st
// capture group of the regular expression: ([^"]+)
if let result = result, let range = result.rangeAt(1).toRange() {
print("This was in quotes: \(str[utf16Range: range] ?? "<bad range>")")
}
}

Remove suffix from filename in Swift

When trying to remove the suffix from a filename, I'm only left with the suffix, which is exactly not what I want.
What (how many things) am I doing wrong here:
let myTextureAtlas = SKTextureAtlas(named: "demoArt")
let filename = (myTextureAtlas.textureNames.first?.characters.split{$0 == "."}.map(String.init)[1].replacingOccurrences(of: "\'", with: ""))! as String
print(filename)
This prints png which is the most dull part of the whole thing.
If by suffix you mean path extension, there is a method for this:
let filename = "demoArt.png"
let name = (filename as NSString).deletingPathExtension
// name - "demoArt"
Some people here seem to overlook that a filename can have multiple periods in the name and in that case only the last period separates the file extension. So this.is.a.valid.image.filename.jpg and stripping the extension should return this.is.a.valid.image.filename and not this (as two answers here would produce) or anything else in between. The regex answer works correctly but using a regex for that is a bit overkill (probably 10 times slower than using simple string processing). Here's a generic function that works for everyone:
func stripFileExtension ( _ filename: String ) -> String {
var components = filename.components(separatedBy: ".")
guard components.count > 1 else { return filename }
components.removeLast()
return components.joined(separator: ".")
}
print("1: \(stripFileExtension("foo"))")
print("2: \(stripFileExtension("foo.bar"))")
print("3: \(stripFileExtension("foo.bar.foobar"))")
Output:
foo
foo
foo.bar
You can also split the String using componentsSeparatedBy, like this:
let fileName = "demoArt.png"
var components = fileName.components(separatedBy: ".")
if components.count > 1 { // If there is a file extension
components.removeLast()
return components.joined(separator: ".")
} else {
return fileName
}
To clarify:
fileName.components(separatedBy: ".")
will return an array made up of "demoArt" and "png".
In iOS Array start with 0 and you want name of the file without extension, so you have split the string using ., now the name will store in first object and extension in the second one.
Simple Example
let fileName = "demoArt.png"
let name = fileName.characters.split(".").map(String.init).first
If you don't care what the extension is. This is a simple way.
let ss = filename.prefix(upTo: fileName.lastIndex { $0 == "." } ?? fileName.endIndex))
You may want to convert resulting substring to String after this. With String(ss)
#Confused with Swift 4 you can do this:
let fileName = "demoArt.png"
// or on your specific case:
// let fileName = myTextureAtlas.textureNames.first
let name = String(fileName.split(separator: ".").first!)
print(name)
Additionally you should also unwrapp first but I didn't want to complicate the sample code to solve your problem.
Btw, since I've also needed this recently, if you want to remove a specific suffix you know in advance, you can do something like this:
let fileName = "demoArt.png"
let fileNameExtension = ".png"
if fileName.hasSuffix(fileNameExtension) {
let name = fileName.prefix(fileName.count - fileNameExtension.count)
print(name)
}
How about using .dropLast(k) where k is the number of characters you drop from the suffix ?
Otherwise for removing extensions from path properly from filename, I insist you to use URL and .deletingPathExtension().lastPathComponent.
Maybe a bit overhead but at least it's a rock solid Apple API.
You can also use a Regexp to extract all the part before the last dot like that :
let fileName = "test.png"
let pattern = "^(.*)(\\.[a-zA-Z]+)$"
let regexp = try! NSRegularExpression(pattern: pattern, options: [])
let extractedName = regexp.stringByReplacingMatches(in: fileName, options: [], range: NSMakeRange(0, fileName.characters.count), withTemplate: "$1")
print(extractedName) //test
let mp3Files = ["alarm.mp3", "bubbles.mp3", "fanfare.mp3"]
let ringtonsArray = mp3Files.flatMap { $0.components(separatedBy: ".").first }
You can return a new string removing a definite number of characters from the end.
let fileName = "demoArt.png"
fileName.dropLast(4)
This code returns "demoArt"
One liner:
let stringWithSuffixDropped = fileName.split(separator: ".").dropLast().joined(separator: ".")

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.