Parse string with currency symbol and value - swift

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

Related

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

How to filter non-digits from string

My phone numbers are like +7 (777) 777-7777.
I need only digits and plus symbol: +77777777777 to make calls.
This returns only digits:
let stringArray = origString.componentsSeparatedByCharactersInSet(NSCharacterSet.decimalDigitCharacterSet().invertedSet)
let newString = NSArray(array: stringArray).componentsJoinedByString("")
One of the many ways to do that:
let isValidCharacter: (Character) -> Bool = {
($0 >= "0" && $0 <= "9") || $0 == "+"
}
let newString = String(origString.characters.filter(isValidCharacter))
or using a regular expression:
// not a +, not a number
let pattern = "[^+0-9]"
// replace anything that is not a + and not a number with an empty string
let newString = origString.replacingOccurrences(
of: pattern,
with: "",
options: .regularExpression
)
or, if you really want to use your original solution with a character set.
let validCharacters = CharacterSet(charactersIn: "0123456789+")
let newString = origString
.components(separatedBy: validCharacters.inverted)
.joined()
In keeping with the spirit of your partial solution,
let origString:NSString = "+7 (777) 777-7777"
let cs = NSCharacterSet(charactersIn: "0123456789+")
let final = origString.components(separatedBy: cs.inverted).joined()

How do I create CharacterSet from other CharacterSet?

let firstSet = CharacterSet(charactersIn: "-()")
let secondSet = CharacterSet.whitespaces
I need to replace +48 (23) 899899 098 with +4823899899098.
let output = "+48 (23) 899899 098".components(separatedBy: firstSet).joined(separator: "")
but here I need to use two CharacterSets. How can I join them into one?
You can use union(_:) for that.
let output = "+48 (23) 899899 098".components(separatedBy: firstSet.union(secondSet)).joined()
Alternatively use regular expression:
let output = "+48 (23) 899899 098"
let trimmedOutput = output.replacingOccurrences(of: "[^0-9+]", with: "", options: .regularExpression)
// -> +4823899899098"
you can use function for phone formatting, so it would be reusable through app
//removes "(", ")", "-", " " etc. and adds "+" for region code format
extension String {
func phoneToString() -> String {
var value = "+"
for character in self.characters {
if Int(String(character)) != nil {
value = value + String(character)
}
}
return value
}
}
let phoneWithoutSpaces = "+48 (23) 899899 098".phoneToString()

How to get the first characters in a string? (Swift 3)

I want to get a substring out of a string which starts with either "<ONLINE>" or "<OFFLINE>" (which should become my substring). When I try to create a Range object, I can easily access the the first character by using startIndex but how do I get the index of the closing bracket of my substring which will be either the 8th or 9th character of the full string?
UPDATE:
A simple example:
let onlineString:String = "<ONLINE> Message with online tag!"
let substring:String = // Get the "<ONLINE> " part from my string?
let onlineStringWithoutTag:String = onlineString.replaceOccurances(of: substring, with: "")
// What I should get as the result: "Message with online tag!"
So basically, the question is: what do I do for substring?
let name = "Ajay"
// Use following line to extract first chracter(In String format)
print(name.characters.first?.description ?? "");
// Output : "A"
If you did not want to use range
let onlineString:String = "<ONLINE> Message with online tag!"
let substring:String = onlineString.components(separatedBy: " ")[0]
print(substring) // <ONLINE>
The correct way would be to use indexes as following:
let string = "123 456"
let firstCharIndex = string.index(string.startIndex, offsetBy: 1)
let firstChar = string.substring(to: firstCharIndex)
print(firstChar)
This Code provides you the first character of the string.
Swift provides this method which returns character? you have to wrap it before use
let str = "FirstCharacter"
print(str.first!)
Similar to OOPer's:
let string = "<ONLINE>"
let closingTag = CharacterSet(charactersIn: ">")
if let closingTagIndex = string.rangeOfCharacter(from: closingTag) {
let mySubstring = string.substring(with: string.startIndex..<closingTagIndex.upperBound)
}
Or with regex:
let string = "<ONLINE>jhkjhkh>"
if let range = string.range(of: "<[A-Z]+>", options: .regularExpression) {
let mySubstring = string.substring(with: range)
}
This code be some help for your purpose:
let myString = "<ONLINE>abc"
if let rangeOfClosingAngleBracket = myString.range(of: ">") {
let substring = myString.substring(to: rangeOfClosingAngleBracket.upperBound)
print(substring) //-><ONLINE>
}
Swift 4
let firstCharIndex = oneGivenName.index(oneGivenName.startIndex, offsetBy: 1)
let firstChar = String(oneGivenName[..<firstCharIndex])
let character = MyString.first
it's an simple way to get first character from string in swift.
In swift 5
let someString = "Stackoverflow"
let firstChar = someString.first?.description ?? ""
print(firstChar)
Swift 5 extension
extension String {
var firstCharactor: String? {
guard self.count > 0 else {
return nil
}
return String(self.prefix(1))
}
}

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
}