Bold words that start with letter - swift

I want to set a label's text with a string that's partly bold. The words I want to make bold all begin with the same letter, say "~".
For example I could have the string, "This ~word is bold, and so is ~this"
Then the label's text would contain the string "This word is bold, and so is this".
Does anybody know if it's possible to make a function like this? I tried the following:
func makeStringBoldForLabel(str: String) {
var finalStr = ""
let words = str.components(separatedBy: " ")
for var word in words {
if word.characters.first == "~" {
var att = [NSFontAttributeName : boldFont]
let realWord = word.substring(from: word.startIndex)
finalStr = finalStr + NSMutableAttributedString(string:realWord, attributes:att)
} else {
finalStr = finalStr + word
}
}
}
but get the error:
Binary operator '+' cannot be applied to operands of type 'String' and 'NSMutableAttributedString'

Easy to solve problem.
use:
func makeStringBoldForLabel(str: String) {
let finalStr = NSMutableAttributedString(string: "")
let words = str.components(separatedBy: " ")
for var word in words {
if word.characters.first == "~" {
var att = [NSFontAttributeName : boldFont]
let realWord = word.substring(from: word.startIndex)
finalStr.append(NSMutableAttributedString(string:realWord, attributes:att))
} else {
finalStr.append(NSMutableAttributedString(string: word))
}
}
}

The error message is clear, you cannot concatenate String and NSAttributedString with the + operator.
You are looking for the API enumerateSubstrings:options. It enumerates strings word by word passing the .byWords option. Unfortunately the tilde (~) is not recognized as a word separator, so we have to check if a word has a preceding tilde. Then change the font attributes at the specific range.
let string = "This ~word is bold, and so is ~this"
let attributedString = NSMutableAttributedString(string: string, attributes:[NSFontAttributeName : NSFont.systemFont(ofSize: 14.0)])
let boldAttribute = NSFont.boldSystemFont(ofSize: 14.0)
string.enumerateSubstrings(in: string.startIndex..<string.endIndex, options: .byWords) { (substring, substringRange, enclosingRange, stop) -> () in
if substring == nil { return }
if substringRange.lowerBound != string.startIndex {
let tildeIndex = string.index(before: substringRange.lowerBound)
if string[tildeIndex..<substringRange.lowerBound] == "~" {
let location = string.distance(from: string.startIndex, to: tildeIndex)
let length = string.distance(from: tildeIndex, to: substringRange.upperBound)
attributedString.addAttribute(NSFontAttributeName, value: boldAttribute, range: NSMakeRange(location, length))
}
}
}

Related

Parse string with currency symbol and value

I am trying to parse a list of input strings from an Excel file that can have a 'currency' value, and it could be in any currency. For e.g.
$200
£300
€200
CA$300
What's the best way to parse out the currency symbol and the numeric value? I'm trying to do this with a NumberFormatter but it doesn't work for the 'euro' or the 'CAD' value.
Here is my code:
let currencyFormatter = NumberFormatter()
currencyFormatter.numberStyle = .currency
currencyFormatter.maximumFractionDigits = 2
let trimmedString = String(currencyString.filter { String($0).rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789.,")) == nil }).trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedString.count > 0 && Locale.current.currencySymbol != trimmedString {
// Currency symbol is *not* local currency, so lookup the locale for it
let allLocales = Locale.availableIdentifiers.map({Locale(identifier: $0)})
if let localeForSymbol = allLocales.filter({$0.currencySymbol == trimmedString}).first {
currencyFormatter.locale = localeForSymbol
}
}
if let numberValue = currencyFormatter.number(from: currencyString) {
print ("\(NSDecimalNumber(decimal: numberValue.decimalValue))")
}
What am I getting wrong here? Or is this not possible without using some regex expressions?
you could try this "...to parse out the currency symbol and the numeric value":
let currencyString = "CA$300"
let valueString = currencyString.filter {
CharacterSet(charactersIn: "0123456789.,").isSuperset(of: CharacterSet(charactersIn: String($0)))
}.trimmingCharacters(in: .whitespacesAndNewlines)
print("---> valueString: \(valueString) ")
let symbol = currencyString.replacingOccurrences(of: valueString, with: "")
print("---> symbol: \(symbol)")
#workingdog has a valid answer. An alternative, and one I always reach for, is to use regex, where 3 simple regex can be used to identify any non-numeric leading characters, the numeric characters, and any non-numeric postfix characters:
let prefixRegex = #"^[^0-9]+"#
let numRegex = #"[0-9]+"#
let postfixRegex = #"[^0-9]+$"#
These can then be used with String's .range(of: options:) using the .regularExpression option to get the element's range, and then use that to extract the relevant SubString.
As a very quick demo of this in action:
["$20", "£300", "€200", "CA$300", "798xyz", "$123abc"]
.forEach{string in
var components: [Range<String.Index>?] = []
components.append( string.range(of: prefixRegex, options: .regularExpression))
components.append( string.range(of: numRegex, options: .regularExpression))
components.append( string.range(of: postfixRegex, options: .regularExpression))
print(components
.map{ $0 != nil ? String(string[$0!]).padded(to: 8) : String(repeating: " ", count: 8) }
.joined(separator: "")
)
}
This provides an output of
$ 20
£ 300
€ 200
CA$ 300
798 xyz
$ 123 abc
.padded(to:) is a utility extension on String that pads the rear of a string to a specified length with any character.
extension String {
func padded(to paddedTo :Int, with padding: String = " ") -> Self {
while count < paddedTo {
return (self + padding).padded(to: paddedTo, with: padding)
}
return self
}
}

SwiftUI - Spell Checking Multi-line String

I am trying to create a function where a multi-line string is spell checked and a single SwiftUI Text() view is returned with any misspelled words highlighted in red.
I have almost cracked it by splitting the string by newlines, then splitting the lines by whitespace and then checking each word.
My main issue is that I am getting an extra new line added at the end of the resulting Text view. Is there anyway I can trim the last Text("\n") or prevent it from being added to the last line?
Also, if there is any way to make it more efficient as there is a slight lag introduced as a lot of text in an array gets checked and so the function is called many times?
Many thanks in advance
func formatText(multiLineText: String) -> Text {
let lineArray = multiLineText.components(separatedBy: .newlines)
let stringToTextView = lineArray.reduce(Text(""), {
return $0 + formatLineText(singleLineText: $1) + Text("\n")
})
return stringToTextView
}
func formatLineText(singleLineText: String) -> Text {
let stringArray = singleLineText.components(separatedBy: .whitespaces)
let stringToTextView = stringArray.reduce(Text(""), {
if !wordIsValid(word: $1) {
return $0 + Text($1).foregroundColor(Color.red).underline() + Text(" ")
}
else {
return $0 + Text($1) + Text(" ")
}
})
return stringToTextView
}
func wordIsValid(word: String) -> Bool {
let checker = UITextChecker()
let range = NSRange(location: 0, length: word.utf16.count)
let misspelledRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en_GB")
return misspelledRange.location == NSNotFound
}
You could use .enumerated in your reduce to check to see if the item is the last one or not -- if it is, don't return the \n.
func formatText(multiLineText: String) -> Text {
let lineArray = multiLineText.components(separatedBy: .newlines)
let stringToTextView = lineArray.enumerated().reduce(Text(""), { (acc,item) in
return acc + formatLineText(singleLineText: item.1) + Text(item.0 != lineArray.endIndex - 1 ? "\n" : "")
})
return stringToTextView
}
In terms of performance, I'd move the let checker = UITextChecker() to somewhere where it doesn't get recreated on every single call to wordIsValid

How to use Swift NSRegularExpression to get uppercased letter?

I have a string like this:
"te_st" and like to replace all underscores followed by a character with the uppercased version of this character.
From "te_st" --> Found (regex: "_.") --------replace with next char (+ uppercase ("s"->"S")--------> "teSt"
From "te_st" ---> to "teSt"
From "_he_l_lo" ---> to "HeLLo"
From "an_o_t_h_er_strin_g" ---> to "anOTHErStrinG"
... but I can not really get it working using Swift's NSRegularExpression like this small snipped does:
var result = "te_st" // result should be teSt
result = try! NSRegularExpression(pattern: "_*").stringByReplacingMatches(in: result, range: NSRange(0..<result.count), withTemplate: ("$1".uppercased()))
There's no regular syntax to convert a match to uppercase. The code you posted is attempting to convert the string $1 to uppercase which is of course just $1. It isn't attempting to convert the value represented by the $1 match at runtime.
Here's another approach using a regular expression to find the _ followed by a lowercase letter. Those are enumerated and replaced with the uppercase letter.
extension String {
func toCamelCase() -> String {
let expr = try! NSRegularExpression(pattern: "_([a-z])")
var res = self
for match in expr.matches(in: self, range: NSRange(0..<res.count)).reversed() {
let range = Range(match.range, in: self)!
let letterRange = Range(match.range(at: 1), in: self)!
res.replaceSubrange(range, with: self[letterRange].uppercased())
}
return res
}
}
print("te_st".toCamelCase())
print("_he_l_lo".toCamelCase())
print("an_o_t_h_er_strin_g".toCamelCase())
This outputs:
teSt
HeLLo
anOTHErStrinG
Here is one implementation using NSRegularExpression. I use group match to get the character after _ and capitalize it and replace the string.
func capitalizeLetterAfterUnderscore(string: String) -> String {
var capitalizedString = string
guard let regularExpression = try? NSRegularExpression(pattern: "_(.)") else {
return capitalizedString
}
let matches = regularExpression.matches(in: string,
options: .reportCompletion,
range: NSMakeRange(0, string.count))
for match in matches {
let groupRange = match.range(at: 1)
let index = groupRange.location
let characterIndex = string.index(string.startIndex,
offsetBy: index)
let range = characterIndex ... characterIndex
let capitalizedCharacter = String(capitalizedString[characterIndex]).capitalized
capitalizedString = capitalizedString.replacingCharacters(in: range,
with: capitalizedCharacter)
}
capitalizedString = capitalizedString.replacingOccurrences(of: "_", with: "")
return capitalizedString
}
capitalizeLetterAfterUnderscore(string: "an_o_t_h_er_strin_g") // anOTHErStrinG
And here is other one without using regular expression. I made extension for method which could also be reused.
extension String {
func indexes(of character: String) -> [Index] {
precondition(character.count == 1, "character should be single letter string")
return enumerated().reduce([]) { (partial, component) in
let currentIndex = index(startIndex,
offsetBy: component.offset)
return String(self[currentIndex]) == character
? partial + [currentIndex]
: partial
}
}
func capitalizeLetter(after indexes: [Index]) -> String {
var modifiedString = self
for currentIndex in indexes {
guard let letterIndex = index(currentIndex,
offsetBy: 1,
limitedBy: endIndex)
else { continue }
let range = letterIndex ... letterIndex
modifiedString = modifiedString.replacingCharacters(in: range,
with: self[range].capitalized)
}
return modifiedString
}
}
let string = "an_o_t_h_er_strin_g"
let newString = string.capitalizeLetter(after: string.indexes(of: "_"))
.replacingOccurrences(of: "_",with: "")
You can use string range(of:, options:, range:) method with .regularExpression options to match the occurrences of "_[a-z]" and replace the subranges iterating the ranges found at reversed order by the character at the index after the range lowerbound uppercased:
let string = "an_o_t_h_er_strin_g"
let regex = "_[a-z]"
var start = string.startIndex
var ranges:[Range<String.Index>] = []
while let range = string.range(of: regex, options: .regularExpression, range: start..<string.endIndex) {
start = range.upperBound
ranges.append(range)
}
var finalString = string
for range in ranges.reversed() {
finalString.replaceSubrange(range, with: String(string[string.index(after: range.lowerBound)]).uppercased())
}
print(finalString) // "anOTHErStrinG\n"
The problem is that it is converting the string "$1" to upper case (which is, unsurprisingly unchanged, just "$1") and using "$1" as the template. If you want to use regex, you will have to enumerate through matches yourself.
The alternative is to split the string by _ characters and uppercase the first character of every substring (except the first) and joining it back together using reduce:
let input = "te_st"
let output = input.components(separatedBy: "_").enumerated().reduce("") { $0 + ($1.0 == 0 ? $1.1 : $1.1.uppercasedFirst()) }
Or, if your goal isn't to write code as cryptic as most regex, we can make that a tad more legible:
let output = input
.components(separatedBy: "_")
.enumerated()
.reduce("") { result, current in
if current.offset == 0 {
return current.element // because you don’t want the first component capitalized
} else {
return result + current.element.uppercasedFirst()
}
}
Resulting in:
teSt
Note, that uses this extension for capitalizing the first character:
extension String {
func uppercasedFirst(with locale: Locale? = nil) -> String {
guard count > 0 else { return self }
return String(self[startIndex]).uppercased(with: locale) + self[index(after: startIndex)...]
}
}
If you want to do sort of dynamic conversion with NSRegularExpression, you can subclass NSRegularExpression and override replacementString(for:in:offset:template:):
class ToCamelRegularExpression: NSRegularExpression {
override func replacementString(for result: NSTextCheckingResult, in string: String, offset: Int, template templ: String) -> String {
if let range = Range(result.range(at: 1), in: string) {
return string[range].uppercased()
} else {
return super.replacementString(for: result, in: string, offset: 0, template: templ)
}
}
}
func toCamelCase(_ input: String) -> String { //Make this a String extension if you prefer...
let regex = try! ToCamelRegularExpression(pattern: "_(.)")
return regex.stringByReplacingMatches(in: input, options: [], range: NSRange(0..<input.utf16.count), withTemplate: "$1")
}
print(toCamelCase("te_st")) //-> teSt
print(toCamelCase("_he_l_lo")) //-> HeLLo
print(toCamelCase("an_o_t_h_er_strin_g")) //-> anOTHErStrinG

swift: how can I delete a specific character?

a string such as ! !! yuahl! ! , I want delete ! and , when I code like this
for index in InputName.characters.indices {
if String(InputName[index]) == "" || InputName.substringToIndex(index) == "!" {
InputName.removeAtIndex(index)
}
}
have this error " fatal error: subscript: subRange extends past String end ", how should I do? THX :D
Swift 5+
let myString = "aaaaaaaabbbb"
let replaced = myString.replacingOccurrences(of: "bbbb", with: "") // "aaaaaaaa"
If you need to remove characters only on both ends, you can use stringByTrimmingCharactersInSet(_:)
let delCharSet = NSCharacterSet(charactersInString: "! ")
let s1 = "! aString! !"
let s1Del = s1.stringByTrimmingCharactersInSet(delCharSet)
print(s1Del) //->aString
let s2 = "! anotherString !! aString! !"
let s2Del = s2.stringByTrimmingCharactersInSet(delCharSet)
print(s2Del) //->anotherString !! aString
If you need to remove characters also in the middle, "reconstruct from the filtered output" would be a little bit more efficient than repeating single character removal.
var tempUSView = String.UnicodeScalarView()
tempUSView.appendContentsOf(s2.unicodeScalars.lazy.filter{!delCharSet.longCharacterIsMember($0.value)})
let s2DelAll = String(tempUSView)
print(s2DelAll) //->anotherStringaString
If you don't mind generating many intermediate Strings and Arrays, this single liner can generate the expected output:
let s2DelAll2 = s2.componentsSeparatedByCharactersInSet(delCharSet).joinWithSeparator("")
print(s2DelAll2) //->anotherStringaString
I find that the filter method is a good way to go for this sort of thing:
let unfiltered = "! !! yuahl! !"
// Array of Characters to remove
let removal: [Character] = ["!"," "]
// turn the string into an Array
let unfilteredCharacters = unfiltered.characters
// return an Array without the removal Characters
let filteredCharacters = unfilteredCharacters.filter { !removal.contains($0) }
// build a String with the filtered Array
let filtered = String(filteredCharacters)
print(filtered) // => "yeah"
// combined to a single line
print(String(unfiltered.characters.filter { !removal.contains($0) })) // => "yuahl"
Swift 3
In Swift 3, the syntax is a bit nicer. As a result of the Great Swiftification of the old APIs, the factory method is now called trimmingCharacters(in:). Also, you can construct the CharacterSet as a Set of single-character Strings:
let string = "! !! yuahl! !"
string.trimmingCharacters(in: [" ", "!"]) // "yuahl"
If you have characters in the middle of the string you would like to remove as well, you can use components(separatedBy:).joined():
let string = "! !! yu !ahl! !"
string.components(separatedBy: ["!", " "]).joined() // "yuahl"
H/T #OOPer for the Swift 2 version
func trimLast(character chars: Set<Character>) -> String {
let str: String = String(self.reversed())
guard let index = str.index(where: {!chars.contains($0)}) else {
return self
}
return String((str[index..<str.endIndex]).reversed())
}
Note:
By adding this function in String extension, you can delete the specific character of string at last.
for index in InputName.characters.indices.reversed() {
if String(InputName[index]) == "" || InputName.substringToIndex(index) == "!" {
InputName.removeAtIndex(index)
}
}
Also you can add such very helpful extension :
import Foundation
extension String{
func exclude(find:String) -> String {
return stringByReplacingOccurrencesOfString(find, withString: "", options: .CaseInsensitiveSearch, range: nil)
}
func replaceAll(find:String, with:String) -> String {
return stringByReplacingOccurrencesOfString(find, withString: with, options: .CaseInsensitiveSearch, range: nil)
}
}
you can use this:
for example if you want to remove "%" the percent from 10%
if let i = text.firstIndex(of: "%") {
text.remove(at: i) //10
}

Swift: Cannot assign value of type 'String.CharacterView.index' to type 'Int'

Does anyone know why I get the following error when I run my code below?
Error: Cannot assign value of type 'String.CharacterView.index' to type 'Int'
var problem = "find the longest word in the problem description"
var last = problem.characters.last
let lastIndex = problem.characters.endIndex;
print(last);
var words:[String] = [String]();
var word:String = "";
var lastCheck:Int = 0;
for i in problem.characters{
lastCheck = lastCheck + 1
if i != " "{
word = word + String(i)
}
else if lastCheck = lastIndex{
words.append(word);
}
else if i == " "{
words.append(word)
word = ""
}
}
print(words)
UPDATE
I have tried changing the let lastIndex to var lastIndex but no luck
You can't compare lastCheck (an Int) and lastIndex (an Index).
You have to convert it:
var problem = "find the longest word in the problem description"
var last = problem.characters.last
let lastIndex = problem.characters.endIndex;
print(last);
var words:[String] = [String]();
var word:String = "";
var lastCheck:Int = 0;
for i in problem.characters{
lastCheck = lastCheck + 1
let lastIndexInt = problem.characters.startIndex.distanceTo(end: lastIndex) // new
if i != " "{
word = word + String(i)
}
else if lastCheck = lastIndex{
words.append(word);
}
else if i == " "{
words.append(word)
word = ""
}
}
print(words)
Here's a simpler solution to your problem:
let input = "find the longest word in the problem description";
let longest = input
.characters //get the input characters
.split(separator:" ", //split by spaces
maxSplits: 1000, //max number of splits
omittingEmptySubsequences: true) //omit "" splits
.map(String.init) //make new strings from the characters
.max{$0.characters.count < $1.characters.count} //get longest length word
print(longest)
Thanks to originaluser2
String.CharacterView.Index is a struct but not Int. So you cannot assign a Int to your String.CharacterView.Index variable. It's normal.
You have to convert to Int as #Amomchilov's answer, or use Index :
...
var lastCheck:String.CharacterView.Index = problem.startIndex
for i in problem.characters {
lastCheck = lastCheck.advancedBy(1)
...
}
Anyway, in order to find the longest word of a phrase, you can use builtIn function of Swift to get all words then compare their length.
For example:
let str = "find the longest word in the problem description"
var longestWord : String?
str.enumerateSubstringsInRange(str.startIndex..<str.endIndex, options:.ByWords) {
(substring, substringRange, enclosingRange, value) in
if let _subString = substring {
if longestWord == nil || longestWord!.characters.count < _subString.characters.count {
longestWord = _subString
}
}
}
Easy way to get all the words in sentence
var problem = "find the longest word in the problem description"
problem.enumerateSubstrings(in: (problem.range(of: problem))!, options: NSString.EnumerationOptions.byWords, { (substring, substringRange, enclosingRange, stop) -> () in
print(substring)
})
Easy way to get longest word in sentence
var longestWord = ""
problem.enumerateSubstrings(in: (problem.range(of: problem))!, options: NSString.EnumerationOptions.byWords, { (substring, substringRange, enclosingRange, stop) -> () in
if longestWord.characters.count < substring?.characters.count{
longestWord = substring!
}
})
print(longestWord)
Note:Code with reference to Swift 3.For lower version of there will be some syntax changes.Let me know if you need the same for lower versions.