string editing in ios swift - ios

I am having a string in that I have getting a tag </p> and I want to append </b> ahead of </p>
let str = "<p>A new week-long event is kicking off soon in Pokemon Go. Niantic is holding another Adventure Week beginning Tuesday, June 4, and it'll give players a chance to earn extra rewards, catch some rare Rock-types, and even find a couple of new Shiny Pokemon.</p><p>During Adventure Week, Geodude, Rhyhorn, Omanyte, Aron, Lileep, Anorith, and other Rock Pokemon will appear in the wild much more frequently than normal.</p>"
I want this string as:
let newStr = #"<b><p>A new week-long event is kicking off soon in Pokemon Go. Niantic is holding another Adventure Week beginning Tuesday, June 4, and it'll give players a chance to earn extra rewards, catch some rare Rock-types, and even find a couple of new Shiny Pokemon.</p></b><p>During Adventure Week, Geodude, Rhyhorn, Omanyte, Aron, Lileep, Anorith, and other Rock Pokemon will appear in the wild much more frequently than normal.</p>"#

If you want to replace all occurrences of a string with another string you can use replacingOccurrences(of:with:). If you want to replace the first occurrence find range of the substring and replace with new substring
if let range = str.range(of: "<p>") {
str.replaceSubrange(range, with: "<b><p>")
}
if let range = str.range(of: "</p>") {
str.replaceSubrange(range, with: "</b></p>")
}

Updated due comment (just first tag) -> See history if you want simpler edition
extension String {
enum TagRange {
case first
case last
case any
}
func addingHTMLTag(_ wrapperTag: String, around tagToWrap: String, witchIs tagRange: TagRange = .any) -> String {
let range: NSRange?
let regxPattern = "<\(tagToWrap)>(.+?)</\(tagToWrap)>"
let regx = try! NSRegularExpression(pattern: regxPattern, options: [])
let allRange = NSRange(location: 0, length: self.utf8.count)
switch tagRange {
case .first: range = regx.rangeOfFirstMatch(in: self, options: [], range: allRange)
case .last: range = regx.matches(in: self, options: [], range: allRange).last?.range
case .any: range = nil
}
if let range = range {
let openTagged = (self as NSString).replacingOccurrences(of: "<\(tagToWrap)>", with: "<\(wrapperTag)><\(tagToWrap)>", range: range)
let offsetRange = NSRange(location: range.location + (wrapperTag.count+2) , length: range.length)
return (openTagged as NSString).replacingOccurrences(of: "</\(tagToWrap)>", with: "</\(tagToWrap)></\(wrapperTag)>", range: offsetRange)
} else {
let openTagged = replacingOccurrences(of: "<\(tagToWrap)>", with: "<\(wrapperTag)><\(tagToWrap)>")
return openTagged.replacingOccurrences(of: "</\(tagToWrap)>", with: "</\(tagToWrap)></\(wrapperTag)>")
}
}
}
usage:
let newStr = str.addingHTMLTag("b", around: "p", witchIs: .first)
This method also checks if the tag you want to wrap around is valid. Maybe there is no closing tag or no opening tag or invalid opening and closing tags order. or maybe there is a<p> in the text itself! that you don't want to wrap.

Related

iOS Swift: looking for ranges of matching word in a string

I need to make a function that returns me ranges of matching words in a given string, for example, given the sentence below:
Hey, bro! Your brother is also her brother.
I want to find an array of Range in the sentence that matches the word "bro", it should match the exact word (case insensitive), so "bro" should only match "bro" but not "brother".
I thought about:
split the sentence, e.g. "hey", "bro", "your", "brother", "is", "also", "her", "brother"
map each word to a word with range, e.g. "hey" would become ["hey", 0...2]
filter and map the word and range array, matching "bro"
Step 2 needs some treatment to make sure the range for each word (in the sentence) can be mapped to the right word, e.g. the first "brother" and second "brother" should have different ranges depending on where they are located.
Is there any smarter way of doing this?
Edit:
Sorry, I forgot to mention, the reason for not using Regex was that sometimes the word has a dot in it, for example:
there is orange in the basket.
from the above sentence, finding the string "or.ge" using regex would match "orange" as well.
I have tested in Playground, You can use this extension to get the values matching this reg ex.
extension String {
func ranges(of substring: String, options: CompareOptions = [], locale: Locale? = nil) -> [Range<Index>] {
var ranges: [Range<Index>] = []
while ranges.last.map({ $0.upperBound < self.endIndex }) ?? true,
let range = self.range(of: substring, options: options, range: (ranges.last?.upperBound ?? self.startIndex)..<self.endIndex, locale: locale)
{
ranges.append(range)
}
return ranges
}
}
let searchString = "bro"
var str = "Hey, bro! Your brother is also her brother."
var reg = str.ranges(of: "(?<![\\p{L}\\d])\(searchString)(?![\\p{L}\\d])", options: [.regularExpression, .caseInsensitive])
str.removeSubrange(reg.first!)
print(str)
Credits to,
iOS - regex to match word boundary, including underscore
One simple solution is to use regular expressions with \b to match “word boundaries”, e.g.
let searchString = "bro"
let sentence = "Hey, Bro! Your brother is also her brother."
let regex = try! NSRegularExpression(pattern: #"\b\#(searchString)\b"#, options: .caseInsensitive)
regex.enumerateMatches(in: sentence, range: NSRange(sentence.startIndex..., in: sentence)) { match, _, _ in
guard let match = match else { return }
print(match.range)
// or, if you want a String.Range
if let range = Range(match.range, in: sentence) {
print(sentence[range])
}
}
There are other richer API (e.g. the Natural Language framework), which, while not perfect, provide richer parsing of natural language text. For example, the below will differentiate between the verb “saw” and noun “saw”:
import NaturalLanguage
let text = "I saw the hammer. I did not see a saw."
let tagger = NLTagger(tagSchemes: [.lexicalClass])
tagger.string = text
let options: NLTagger.Options = [.omitWhitespace, .joinContractions]
tagger.enumerateTags(in: text.startIndex..<text.endIndex, unit: .word, scheme: .lexicalClass, options: options) { tag, range in
guard let tag = tag else { return true }
print(tag, String(text[range]))
return true
}
Producing:
NLTag(_rawValue: Pronoun) I
NLTag(_rawValue: Verb) saw
NLTag(_rawValue: Determiner) the
NLTag(_rawValue: Noun) hammer
NLTag(_rawValue: SentenceTerminator) .
NLTag(_rawValue: Pronoun) I
NLTag(_rawValue: Verb) did
NLTag(_rawValue: Adverb) not
NLTag(_rawValue: Verb) see
NLTag(_rawValue: Determiner) a
NLTag(_rawValue: Noun) saw
NLTag(_rawValue: SentenceTerminator) .

How to remove '\u{ef}' character from String Swift

let's say I have a string
var a = "#bb #cccc #ddddd\u{ef}"
and i am setting it to textview like this
let text = a.trimmingCharacters(in: .whitespacesAndNewlines)
let textRemoved = text?.replacingOccurrences(of: "\u{ef}", with: "", options: NSString.CompareOptions.literal, range:nil)
textView.text = textRemove
I am trying to remove the \u{ef} character here. But in textRemoved it is not happening. Please help me how to do it.
I am using Xcode 10. Looks like below Xcode version than 10 is working
fine. is it a bug of Xcode 10?
This is a late answer but I struggled to replace "\u{ef}" in string as well. During debugging when hovered over string it showed presence of \u{ef} but when print in description it only showed space.
let str = "\u{ef} Some Title"
print(str) //" Some Title"
I tried replacingOccurrences(of: "\u{ef}", with: "", options: NSString.CompareOptions.literal, range: nil).trimmingCharacters(in: .whitespaces) but it failed as well.
So I used below snippet and it worked like wonder.
let modifiedStr = str.replacingOccurrences(of: "\u{fffc}", with: "", options: NSString.CompareOptions.literal, range: nil).trimmingCharacters(in: .whitespaces)
print(modifiedStr) //"Some Title"
Hope this helps someone!!
i also faced same issue for "\u{e2}". i have searched a lot but unable to find any answer. then i have tried below code , which works for me.
var newString = ""
for char in strMainString.unicodeScalars{
if char.isASCII{
newString += String(char)
}
}
Hope that will also work for you too.
In Xcode 10 Playground, string replaces for \u{00EF} is working.
var a = "#bb #cccc #ddddd\u{ef}"
a = a.replacingOccurrences(of: "\u{00EF}", with: "")
I hope that will work for you.
I tried the following and it worked like a charm:
replacingOccurrences(of: "�", with: " ", options: NSString.CompareOptions.literal, range: nil)
e.g. 1
let text = "\u{ef}\u{ef}\u{ef}\u{ef}😇哦哦哦"
let text1 = text.replacingOccurrences(of: "\u{fffc}", with: "", options: String.CompareOptions.literal, range: nil)
let text2 = text.replacingOccurrences(of: "\u{ef}", with: "", options: String.CompareOptions.literal, range: nil).trimmingCharacters(in: .whitespaces)
runnable
<img src="https://i.stack.imgur.com/styVo.png"/>
e.g. 2
let strBefore = textDocumentProxy.documentContextBeforeInput
let strAfter = textDocumentProxy.documentContextAfterInput
var textInput = strBefore + strAfter
let textInput2 = textInput.replacingOccurrences(of: "\u{ef}", with: "", options: String.CompareOptions.literal, range: nil)
let textInput1 = textInput.replacingOccurrences(of: "\u{fffc}", with: "", options: String.CompareOptions.literal, range: nil).trimmingCharacters(in: .whitespaces)
runnable
<img src="https://i.stack.imgur.com/xGHtW.png"/>
Similar to question but with \u{e2} symbol (fix is the same):
\u{e2} is not a character rather subset of UTF8 plane which starts with 0xE2 byte.
So look here, E2 are general punctuation symbols.
There many symbols actually which started with \u{e2} but not limited to it and full char can be represented f.e. with e2 80 a8 bytes (line separator).
That explains why shown in Xcode \u{e2} can't be replaced with replacingOccurrences... function. In order to filter out correct symbol you have to know what exact symbol it is, f.e. by using the snippet below:
"\u{2028}&😲".forEach { (char) in
print(Data(char.utf8).map { String(format: "%02x", $0) }.joined(separator: " "))
}
it prints to console:
e2 80 a8
26
f0 9f 98 b2
which are byte representation for each symbol.
Next step is to filter your string, go here and search in 3d column your bytes and unicode code point value is what you need (first column) and write it in swift code like "\u{2028}\u{206A}..." (depending on your sorting).
The final function may look like:
func removingE2Symbols() -> String {
let specialChars = "\u{202A}\u{202C}"
return filter { !specialChars.contains($0) }
}
Try this
extension String {
var asciiString: String {
return String(self.unicodeScalars.filter{ $0.isASCII })
}
}
It,s working Please check again:
let a = "#bb #cccc #ddddd\u{ef}"
let text = a.trimmingCharacters(in: .whitespacesAndNewlines)
let textRemoved = text.replacingOccurrences(of: "\u{ef}", with: "", options: NSString.CompareOptions.literal, range:nil)
print(textRemoved)

Replace just bold word in string, Swift

I have a UILabel with text "hello world, hello". There are 2 hello words.
And I want to replace the only 'bold hello' to 'thanks' without bold.
I use this code:
uiLabel1.text = "hello world, hello"
let target = "hello"
let replace = "thanks"
uiLabel1.text.replacingOccurrences(of: target, with: replace, options:
NSString.CompareOptions.literal, range: nil)
And the result is: "thanks world, thanks"
The result I want: "hello world, thanks"
Okay, so there's probably an easier way to do this...
So, I went through the API (like super quick) and looked for something like lastIndexOf, which lead me on a little trail to String#range(of:options), which allows you to search backwards, hmmm, interesting.
This returns a Range<String.Index> ... okay, so how can I use that?! Hmm, maybe String#replacingOccurrences(of:with:options:range:) 🤔
So, crack open a play ground and...
var str = "hello world, hello"
let lastIndexOf = str.range(of: "hello", options: .backwards)
str = str.replacingOccurrences(of: "hello", with: "thanks", options: .caseInsensitive, range: lastIndexOf)
str now equals "hello world, thanks"
Hi #MadProgrammer, your code is to replace the last hello word to thanks, right? But my question is to replace hello with the bold attribute, it may in the first, middle or at the end of a string.
Okay, so clearly we're missing some context...
Assuming, now, you're using a NSAttributedString, it becomes slightly more complicated
Building the string itself is not hard, figuring out how to find string components by attribute, a little more difficult.
Lucky for us, we have the Internet. So, the following is based on ideas I got from:
NSAttributedString by example
Detect whether a font is bold/italic on iOS?
One of the important things to remember when trying to solve an issue, you'll be lucky to find a single answer which does it all, instead, you need to break your issue down and focus on solving individual elements, and be prepared to go back to the start 😉
So, once again, unto the play ground...
import UIKit
var str = "hello world, "
//let lastIndexOf = str.range(of: "hello", options: .backwards)
//str = str.replacingOccurrences(of: "hello", with: "thanks", options: .caseInsensitive, range: lastIndexOf)
extension UIFont {
var isBold: Bool {
return fontDescriptor.symbolicTraits.contains(.traitBold)
}
var isItalic: Bool {
return fontDescriptor.symbolicTraits.contains(.traitItalic)
}
}
// Just so I can see that the style ;)
let fontSize = CGFloat(24.0)
let boldAttrs = [
NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: fontSize),
NSAttributedString.Key.foregroundColor: UIColor.white // Playground
]
// Playground only
let plainAttrs = [
NSAttributedString.Key.foregroundColor: UIColor.white // Playground
]
let boldText = NSMutableAttributedString(string: "hello", attributes: boldAttrs)
let styledText = NSMutableAttributedString(string: str, attributes: plainAttrs)
let someMoreBoldText = NSMutableAttributedString(string: "not to be replaced", attributes: boldAttrs)
// Attributes can be combined with their appear together ;)
styledText.append(boldText)
styledText.append(NSMutableAttributedString(string: " ", attributes: plainAttrs))
styledText.append(someMoreBoldText)
styledText.append(NSMutableAttributedString(string: " ", attributes: plainAttrs))
styledText.append(boldText)
styledText.enumerateAttribute(NSAttributedString.Key.font, in: NSRange(0..<styledText.length)) { (value, range, stop) in
guard let font = value as? UIFont, font.isBold else {
return;
}
let subText = styledText.attributedSubstring(from: range)
guard subText.string == "hello" else {
return
}
styledText.replaceCharacters(in: range, with: "thanks")
}
styledText
Which outputs...
The important things for me are:
The style has not be changed
Only the individual "hello" values, which are bolded, have been changed
Here is the code. But actually this is hardcoded. If the target enclosed in between <b></b>, it will work.
var text = "hello world, <b>hello</b>"
let target = "hello"
let replace = "thanks"
text = text.replacingOccurrences(of: "<b>\(target)</b>", with: replace, options: .literal, range: nil) //hello world, thanks

Extracting address elements from a String using NSDataDetector in Swift 3.0

I'm attempting to use NSDataDetector to addresses from a string. I've taken a look at NSHipster's article on NSDataDetector as well as Apple's NSDataDetector documentation. I've got the following method to the point where it'll pull addresses out of a string:
func getAddress(from dataString: String) -> [String] {
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.address.rawValue)
let matches = detector.matches(in: dataString, options: [], range: NSRange(location: 0, length: dataString.utf16.count))
var addressArray = [String]()
// put matches into array of Strings
for match in matches {
let address = (dataString as NSString).substring(with: match.range)
addressArray.append(address)
}
return addressArray
}
I'd like to pull out elements of addresses, not the entire address. In NSHipster's NSDataDetector post in the Data Detector Match Types section, I see address components such as NSTextCheckingCityKey, NSTextCheckingStateKey, and NSTextCheckingZIPKey. I'm unable to use those keys in the NSDataDetector's initialization.
I dug around on GitHub to see if I could find an example to crib from, but the only stuff I'm able to find is Objective-C code or declarative stuff in the master Swift repo.
I'm 99% sure I can pull out the individual components of an address, but I'm too dumb to figure it out. Thank you for reading. I welcome suggestions.
I haven't used this class before, but it looks like it returns objects of type NSTextCheckingResult. If you get a result of type NSTextCheckingTypeAddress then you can ask the result for it's addressComponents, which will be a dictionary containing the different parts of the address.
EDIT:
Here is some working playground code I just banged out:
import UIKit
var string = "Now is the time for all good programmers to babble incoherently.\n" +
"Now is the time for all good programmers to babble incoherently.\n" +
"Now is the time for all good programmers to babble incoherently.\n" +
"123 Elm Street\n" +
"Daton, OH 45404\n" +
"Now is the time for all good programmers to babble incoherently.\n" +
"2152 E Street NE\n" +
"Washington, DC 20001"
let results = getAddress(from: string)
print("matched \(results.count) addresses")
for result in results {
let city = result[NSTextCheckingCityKey] ?? ""
print("address dict = \(result).")
print(" City = \"\(city)\"")
}
func getAddress(from dataString: String) -> [[String: String]] {
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.address.rawValue)
let matches = detector.matches(in: dataString, options: [], range: NSRange(location: 0, length: dataString.utf16.count))
var resultsArray = [[String: String]]()
// put matches into array of Strings
for match in matches {
if match.resultType == .address,
let components = match.addressComponents {
resultsArray.append(components)
} else {
print("no components found")
}
}
return resultsArray
}
This code prints:
matched 2 addresses
address dict = ["Street": "123 Elm Street", "ZIP": "45404", "City": "Daton", "State": "OH"].
City = "Daton"
address dict = ["Street": "2152 E Street NE", "ZIP": "20001", "City": "Washington", "State": "DC"].
City = "Washington"
You can easily extract all addresses, URLs and phone numbers using NSDataDetector.
Swift 4.2
let string = "This is an address PO Box 7775, San Francisco, CA. This is a url
http:/
www.swiftdevcenter.com/. This is second url: https://www.google.com/. This is
mobile number
+18987656789. This is a date 01/26/2019"
let detectorType: NSTextCheckingResult.CheckingType = [.address, .phoneNumber,
.link, .date]
do {
let detector = try NSDataDetector(types: detectorType.rawValue)
let results = detector.matches(in: string, options: [], range:
NSRange(location: 0, length: string.utf16.count))
for result in results {
if let range = Range(result.range, in: string) {
let matchResult = string[range]
print("result: \(matchResult), range: \(result.range)")
}
}
} catch {
print("handle error")
}
For address use only
let detectorType: NSTextCheckingResult.CheckingType = [.address]
Credits: http://www.swiftdevcenter.com/how-to-detect-url-address-phone-number-and-dates-using-nsdatadetector/

Find index of Nth instance of substring in string in Swift

My Swift app involves searching through text in a UITextView. The user can search for a certain substring within that text view, then jump to any instance of that string in the text view (say, the third instance). I need to find out the integer value of which character they are on.
For example:
Example 1: The user searches for "hello" and the text view reads "hey hi hello, hey hi hello", then the user presses down arrow to view second instance. I need to know the integer value of the first h in the second hello (i.e. which # character that h in hello is within the text view). The integer value should be 22.
Example 2: The user searches for "abc" while the text view reads "abcd" and they are looking for the first instance of abc, so the integer value should be 1 (which is the integer value of that a since it's the first character of the instance they're searching for).
How can I get the index of the character the user is searching for?
Xcode 11 • Swift 5 or later
let sentence = "hey hi hello, hey hi hello"
let query = "hello"
var searchRange = sentence.startIndex..<sentence.endIndex
var indices: [String.Index] = []
while let range = sentence.range(of: query, options: .caseInsensitive, range: searchRange) {
searchRange = range.upperBound..<searchRange.upperBound
indices.append(range.lowerBound)
}
print(indices) // "[7, 21]\n"
Another approach is NSRegularExpression which is designed to easily iterate through matches in an string. And if you use the .ignoreMetacharacters option, it will not apply any sophisticated wildcard/regex logic, but will just look for the string in question. So consider:
let string = "hey hi hello, hey hi hello" // string to search within
let searchString = "hello" // string to search for
let matchToFind = 2 // grab the second occurrence
let regex = try! NSRegularExpression(pattern: searchString, options: [.caseInsensitive, .ignoreMetacharacters])
You could use enumerateMatches:
var count = 0
let range = NSRange(string.startIndex ..< string.endIndex, in: string)
regex.enumerateMatches(in: string, range: range) { result, _, stop in
count += 1
if count == matchToFind {
print(result!.range.location)
stop.pointee = true
}
}
Or you can just find all of them with matches(in:range:) and then grab the n'th one:
let matches = regex.matches(in: string, range: range)
if matches.count >= matchToFind {
print(matches[matchToFind - 1].range.location)
}
Obviously, if you were so inclined, you could omit the .ignoreMetacharacters option and allow the user to perform regex searches, too (e.g. wildcards, whole word searches, start of word, etc.).
For Swift 2, see previous revision of this answer.

Resources