Swift finding and changing range in attributed string - swift

What would be the best way to change this string:
"gain quickness for 5 seconds. <c=#reminder>(Cooldown: 90s)</c> only after"
into an Attributed String while getting rid of the part in the <> and I want to change the font of (Cooldown: 90s). I know how to change and make NSMutableAttributedStrings but I am stuck on how to locate and change just the (Cooldown: 90s) in this case. The text in between the <c=#reminder> & </c> will change so I need to use those to find what I need.
These seem to be indicators meant to be used for this purpose I just don't know ho.

First things first, you'll need a regular expression to find and replace all tagged strings.
Looking at the string, one possible regex could be <c=#([a-zA-Z-9]+)>([^<]*)</c>. Note that will will work only if the string between the tags doesn't contain the < character.
Now that we have the regex, we only need to apply it on the input string:
let str = "gain quickness for 5 seconds. <c=#reminder>(Cooldown: 90s)</c> only after"
let attrStr = NSMutableAttributedString(string: str)
let regex = try! NSRegularExpression(pattern: "<c=#([a-zA-Z-9]+)>([^<]*)</c>", options: [])
while let match = regex.matches(in: attrStr.string, options: [], range: NSRange(location: 0, length: attrStr.string.utf16.count)).first {
let indicator = str[Range(match.range(at: 1), in: str)!]
let substr = str[Range(match.range(at: 2), in: str)!]
let replacement = NSMutableAttributedString(string: String(substr))
// now based on the indicator variable you might want to apply some transformations in the `substr` attributed string
attrStr.replaceCharacters(in: match.range, with: replacement)
}

Related

Make Ints within a string bold - swift

Hi I have a string that I'm displaying in a label that shows a time like this "4hours 27mins 45secs" and this can vary based on the time for example "30mins 5secs"
I want to style the Ints in the string so they are bold and the characters are light so it looks like this...
I've done some searching and found I need to use some sort of attributed string, but everything I've found seems to use some sort of breaking character, or a standard prefix to detect.
As I don't have either is there a way to check if the character is an int and apply bold. Appreciate this might not be possible and would welcome a recommendation for changing my approach.
Still pretty new to coding/swift so any help appreciated!
Quickly done, you can use a Regular Expression to find the ranges of all the numbers. Then, on these ranges, you change the attribute for a bold font.
let attributedString = NSMutableAttributedString(string: str, attributes: [.foregroundColor: UIColor.white])
let regex = try! NSRegularExpression(pattern: "\\d+", options: [])
let matches = regex.matches(in: attributedString.string, options: [], range: NSRange(location: 0, length: attributedString.length))
matches.forEach {
attributedString.addAttributes([.font: UIFont.boldSystemFont(ofSize: 17)], range: $0.range)
}
If your deployment target is iOS15 you can use the newly added AttributedString with markdown support, which is much simple than the very complicated to use NSAttributedString.
https://developer.apple.com/documentation/foundation/attributedstring

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"

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

Case-sensitive character replacement in a Swift string

I need to replace characters in a Swift string case-sensitively.
I've been using the replacingOccurrences(of:with:options:range:) built-in string function to change every "a" to a "/a/", every "b" to a "/b/", and so on, like this:
stringConverted = stringConverted.replacingOccurrences(of: "a", with: "/a/", options: [])
Then I change every "/a/" to its corresponding letter, which is "a". I change every "/b/" to its corresponding letter, which is "q", and so on.
My problem is that I need this replacement to be case-sensitive. I've looked this up, but I've tried what I found and it didn't help.
Do I need to use the range argument? Or am I doing something else wrong?
As #Orkhan mentioned you can pass options: .caseInsensitive like below
let a = "a"
let start = a.index(a.startIndex, offsetBy: 0)
let end = a.index(a.startIndex, offsetBy: a.count)
let range = start..<end
let value = a.replacingOccurrences(of: "a", with: "/a", options: .caseInsensitive, range: range)
print(value)

How can we remove every characters other than numbers, dot and colon in swift?

I am stuck at getting a string from html body
<html><head>
<title>Uaeexchange Mobile Application</title></head><body>
<div id='ourMessage'>
49.40:51.41:50.41
</div></body></html>
I Would like to get the string containing 49.40:51.41:50.41 . I don't want to do it by string advance or index. Can I get this string by specifying I need only numbers,dot(.) and colon(:) in swift. I mean some numbers and some special characters?
I tried
let stringArray = response.componentsSeparatedByCharactersInSet(
NSCharacterSet.decimalDigitCharacterSet().invertedSet)
let newString = stringArray.joinWithSeparator("")
print("Trimmed\(newString)and count\(newString.characters.count)")
but this obviously trims away dot and colon too. any suggestions friends?
The simple answer to your question is that you need to include "." & ":" in the set that you want to keep.
let response: String = "<html><head><title>Uaeexchange Mobile Application</title></head><body><div id='ourMessage'>49.40:51.41:50.41</div></body></html>"
var s: CharacterSet = CharacterSet.decimalDigits
s.insert(charactersIn: ".:")
let stringArray: [String] = response.components(separatedBy: s.inverted)
let newString: String = stringArray.joined(separator: "")
print("Trimmed '\(newString)' and count=\(newString.characters.count)")
// "Trimmed '49.40:51.41:50.41' and count=17\n"
Without more information on what else your response might be, I can't really give a better answer, but fundamentally this is not a good solution. What if the response had been
<html><head><title>Uaeexchange Mobile Application</title></head><body>
<div id='2'>Some other stuff: like this</div>
<div id='ourMessage'>49.40:51.41:50.41</div>
</body></html>
Using a replace/remove solution to this is a hack, not an algorithm - it will work until it doesn't.
I think you should probably be looking for the <div id='ourMessage'> and reading from there to the next <, but again, we'd need more information on the specification of the format of the response.
I'd recommend to use an HTML parser, nevertheless this is a simple solution with regular expression:
let extractedString = response.replacingOccurrences(of: "[^\\d:.]+", with: "", options: .regularExpression)
Or the positive regex search which is more code but also more reliable:
let pattern = ">\\s?([\\d:.]+)\\s?<"
let regex = try! NSRegularExpression(pattern: pattern)
if let match = regex.firstMatch(in: response, range: NSMakeRange(0, response.utf8.count)) {
let range = match.rangeAt(1)
let startIndex = response.index(response.startIndex, offsetBy: range.location)
let endIndex = response.index(startIndex, offsetBy: range.length)
let extractedString = response.substring(with: startIndex..<endIndex)
print(extractedString)
}
While the simple (negative) regex search removes all characters which don't match digits, dots and colons the positive search considers also the closing (>) and opening tags (<) around the desired result so an accidental digit, dot or colon doesn't match the pattern.
You can also use the String.replacingOccurrences() method in other ways, without regex, as follows:
import Foundation
var response: String = "<html><head><title>Uaeexchange Mobile Application</title></head><body><div id='ourMessage'>49.40:51.41:50.41</div></body></html>"
let charsNotToBeTrimmed = (0...9).map{String($0)} + ["." ,":"] // you can add any character you want here, that's the advantage
for i in response.characters{
if !charsNotToBeTrimmed.contains(String(i)){
response = response.replacingOccurrences(of: String(i), with: "")
}
}
print(response)
Basically, this creates an array of characters which should not be trimmed and if a character is not out there, it gets removed in the for-loop
But you have to be warned that what you're trying to do isn't quite right...