How to format numbers in a textField with math equation string? - swift

I'm trying to format numbers in a UITextField consists of math equation string: "number + number".
At the moment I can type just a single number, then convert it to Double -> format with NSNumberFormatter -> convert back to String -> assign to textField.text:
The code:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.locale = .current
formatter.roundingMode = .down
let numberString = textField.text ?? ""
guard let range = Range(range, in: numberString) else { return false }
let updatedString = numberString.replacingCharacters(in: range, with: string)
let correctDecimalString = updatedString.replacingOccurrences(of: formatter.decimalSeparator, with: ".")
let completeString = correctDecimalString.replacingOccurrences(of: formatter.groupingSeparator, with: "")
guard let value = Double(completeString) else { return false }
let formattedNumber = formatter.string(for: value)
textField.text = formattedNumber
return string == formatter.decimalSeparator
}
Now I want to add a calculation functionality and display a simple math equation in a textField as "number + number", but each number should be formatted as shown above. Example (but without formatting):
I can't properly implement that. The logic for me was: track the String each time new char inserts -> if it has math sign extract numbers -> convert them to Double -> format with NSNumberFormatter -> convert back to String -> construct a new String "number + number".
The code I tried:
if let firstString = completeString.split(separator: "+").first, let secondString = completeString.split(separator: "+").last {
guard let firstValue = Double(firstString) else { return false }
guard let secondValue = Double(secondString) else { return false }
let firstFormattedNumber = formatter.string(for: firstValue)
let secondFormattedNumber = formatter.string(for: secondValue)
textField.text = "\(firstFormattedNumber ?? "") + \(secondFormattedNumber ?? "")"
// another try
if completeString.contains("+") {
let stringArray = completeString.components(separatedBy: "+")
for character in stringArray {
print(character)
guard let value = Double(character) else { return false }
guard let formattedNumber = formatter.string(for: value) else { return false }
textField.text = "\(formattedNumber) + "
}
}
But it's not working properly. I tried to search but didn't find any similar questions.
Test project on GitHub
How can I format the numbers from such a string?

Here is how I was able to solve my question:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.locale = .current
formatter.roundingMode = .down
//set of possible math operations
let symbolsSet = Set(["+","-","x","/"])
let numberString = textField.text ?? ""
guard let range = Range(range, in: numberString) else { return false }
let updatedString = numberString.replacingCharacters(in: range, with: string)
let correctDecimalString = updatedString.replacingOccurrences(of: formatter.decimalSeparator, with: ".")
let completeString = correctDecimalString.replacingOccurrences(of: formatter.groupingSeparator, with: "")
//receive math symbol user typed
let symbol = symbolsSet.filter(completeString.contains).first ?? ""
//receive number of symbols in a String. If user wants to type more than one math symbol - do not insert
let amountOfSymbols = completeString.filter({String($0) == symbol}).count
if amountOfSymbols > 1 { return false }
//receive numbers typed by user
let numbersArray = completeString.components(separatedBy: symbol)
//check for each number - if user wants to type more than one decimal sign - do not insert
for number in numbersArray {
let amountOfDecimalSigns = number.filter({$0 == "."}).count
if amountOfDecimalSigns > 1 { return false }
}
guard let firstNumber = Double(String(numbersArray.first ?? "0")) else { return true }
guard let secondNumber = Double(String(numbersArray.last ?? "0")) else { return true }
let firstFormattedNumber = formatter.string(for: firstNumber) ?? ""
let secondFormattedNumber = formatter.string(for: secondNumber) ?? ""
// if user typed math symbol - show 2 numbers and math symbol, if not - show just first typed number
textField.text = completeString.contains(symbol) ? "\(firstFormattedNumber)\(symbol)\(secondFormattedNumber)" : "\(firstFormattedNumber)"
return string == formatter.decimalSeparator
}

Related

UITextField: backspace doesn't delete the first character

I have a UITextField with a formatted string.
I can't figure out why a backspace press works fine for every character deletion except the first character?
My code:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let formatter = NumberFormatter()
formatter.usesGroupingSeparator = true
formatter.numberStyle = .decimal
formatter.decimalSeparator = "."
formatter.groupingSeparator = " "
let textString = textField.text ?? ""
guard let range = Range(range, in: textString) else { return false }
let updatedString = textString.replacingCharacters(in: range, with: string)
let completeString = updatedString.replacingOccurrences(of: formatter.groupingSeparator, with: "")
guard let value = Double(completeString) else { return false }
numberFromTextField = value
let formattedNumber = formatter.string(from: NSNumber(value: value)) ?? ""
textField.text = formattedNumber
return string == formatter.decimalSeparator
}
A gif with the problem behaviour: CLICK
Where is my error hides?

Swift number formater currency problems

I want my textField to show 5.000,00 for instance, I manage to limit the characters that my user can type, but my currency isn't working. Also I am new to Swift, how do I get this currency to work out?
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.allowsFloats = true
formatter.currencyDecimalSeparator = ","
formatter.alwaysShowsDecimalSeparator = true
formatter.locale = Locale(identifier: "pt_BR")
formatter.currencyCode = "BRL"
formatter.maximumFractionDigits = 0
let currentText = textField.text ?? ","
guard let stringRange = Range(range, in: currentText) else { return false}
let updatedText = currentText.replacingCharacters(in: stringRange, with: string)
if let groupingSeparator = formatter.groupingSeparator {
if string == groupingSeparator {
return true
}
if let textWithoutGroupingSeparator = textField.text?.replacingOccurrences(of: groupingSeparator, with: "") {
var totalTextWithoutGroupingSeparators = textWithoutGroupingSeparator + string
if string.isEmpty { // pressed Backspace key
totalTextWithoutGroupingSeparators.removeLast()
}
if let numberWithoutGroupingSeparator = formatter.number(from: totalTextWithoutGroupingSeparators),
let formattedText = formatter.string(from: numberWithoutGroupingSeparator) {
textField.text = formattedText
return false
}
return updatedText.count <= 8
}
}
return true
}
I solve this with a string extension, previously I was using a function on my view controller, doing that way solve for me, you just have to change your locale and will works.
extension String {
// formatting text for currency textField
func currencyInputFormatting() -> String {
var number: NSNumber!
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.locale = Locale(identifier: "pt_BR")
formatter.currencySymbol = ""
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2
var amountWithPrefix = self
// remove from String: "$", ".", ","
let regex = try! NSRegularExpression(pattern: "[^0-9]", options: .caseInsensitive)
amountWithPrefix = regex.stringByReplacingMatches(in: amountWithPrefix, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, self.count), withTemplate: "")
let double = (amountWithPrefix as NSString).doubleValue
number = NSNumber(value: (double / 100))
// if first number is 0 or all numbers were deleted
guard number != 0 as NSNumber else {
return ""
}
print(formatter.string(from: number))
return formatter.string(from: number)!
}
}

Cursor shifts to end on edit of formatted decimal textfield - Swift

I am formatting a UITextField such that it becomes comma separated (Decimal style) while typing.
So 12345678 becomes 12,345,678
Now when I edit the UITextField, say I want to remove 5, at that time I tap after 5 and delete it but the cursor shifts to the end of the text immediately, that's after 8.
Here's my code which I have used to format the decimal while typing:
func checkTextField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if ((string == "0" || string == "") && (textField.text! as NSString).range(of: ".").location < range.location) {
return true
}
let allowedCharacterSet = NSCharacterSet(charactersIn: "0123456789.").inverted
let filtered = string.components(separatedBy: allowedCharacterSet)
let component = filtered.joined(separator: "")
let isNumeric = string == component
if isNumeric {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
let numberWithOutCommas = newString.replacingOccurrences(of: ",", with: "")
let number = formatter.number(from: numberWithOutCommas)
if number != nil {
var formattedString = formatter.string(from: number!)
if string == "." && range.location == textField.text?.count {
formattedString = formattedString?.appending(".")
}
textField.text = formattedString
} else {
textField.text = nil
}
}
return false
}
It is called like this:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let textFieldText = (textField.text! as NSString).replacingCharacters(in: range, with: string)
let checkForCurrency = checkTextField(textField, shouldChangeCharactersIn: range, replacementString: string)
return checkForCurrency
}
I have tried the following but in vain :
Solution 1
Solution 2
What could the reason for cursor shift be?
It should be something like in this link
Any help would be appreciated!
update your custom function with :
func checkTextField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let textFieldText = (textField.text! as NSString).replacingCharacters(in: range, with: string)
if ((string == "0" || string == "") && (textField.text! as NSString).range(of: ".").location < range.location) {
return true
}
var currentPosition = 0
if let selectedRange = textField.selectedTextRange {
currentPosition = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
}
let allowedCharacterSet = NSCharacterSet(charactersIn: "0123456789.").inverted
let filtered = string.components(separatedBy: allowedCharacterSet)
let component = filtered.joined(separator: "")
let isNumeric = string == component
if isNumeric {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
let numberWithOutCommas = newString.replacingOccurrences(of: ",", with: "")
let number = formatter.number(from: numberWithOutCommas)
if number != nil {
var formattedString = formatter.string(from: number!)
if string == "." && range.location == textField.text?.count {
formattedString = formattedString?.appending(".")
}
textField.text = formattedString
if(textFieldText.count < formattedString?.count ?? 0){
currentPosition = currentPosition + 1
}
}else{
textField.text = nil
}
}
if(string == ""){
currentPosition = currentPosition - 1
}else{
currentPosition = currentPosition + 1
}
if let newPosition = textField.position(from: textField.beginningOfDocument, offset: currentPosition) {
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
return false
}

Swift String: append 0s after decimal separator

I need to append 0, 1, or 2 0s to a string, depends on its decimal separator, so that
"100", "100." and "100.0" becomes "100.00"
"100.8" becomes "100.80"
"100.85" remains unchanged
I could find the decimal separator and check its distance to end endIndex of the string, but is there an easier way of doing it?
NumberFormatter does this, but the actual string I have, isn't a plain number that can go through a formatter.
For example:
let amount = "123,456,789"
then formatted amount should be "123,456,789.00"
assumption:
the given string has at most one decimal separator with at most two decimal places
So there can't be string like: "123.4.4.5"
Also I want to use the decimal separator from NumberFormatter().decimalSeparator
You could pass the string through a decimal formatter to get the underlying number, and then back again through the formatter to get a formatted string:
let amount = "123,456,789"
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2
let number = formatter.number(from: amount)
let newAmountString = formatter.string(from: number!) //"123,456,789.00"
(You should check that number is not nil before force unwrapping it, with if letor guard)
You could wrap this in a function:
func zeroPadding(toString: String) -> String? {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2
guard let number = formatter.number(from: toString) else {
return nil
}
return formatter.string(from: number)
}
Here are some test cases:
zeroPadding(toString: "123,456,789") //"123,456,789.00"
zeroPadding(toString: "123,456,789.0") //"123,456,789.00"
zeroPadding(toString: "123,456,789.10") //"123,456,789.10"
zeroPadding(toString: "123,456,789.123") //"123,456,789.12"
zeroPadding(toString: "123.4567") //"123.46"
zeroPadding(toString: "Price: 1€ for a 💩") //nil
Or define it as an extension on String:
extension String {
func withZeroPadding() -> String? {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2
guard let number = formatter.number(from: self) else {
return nil
}
return formatter.string(from: number)
}
}
And use it like this:
"123.4.4.5".withZeroPadding() //nil
"12.".withZeroPadding() //"12.00"
"123,456,789".withZeroPadding() //"123,456,789.00"
This is the following code snippet I have tested on Playground, it can be achieved more smartly but for now it is working.
//let amount = "123,456,789.545222323"
//let amount = "123,456,789."
let amount = "123,456,789"
let removeSpaces = amount.replacingOccurrences(of: " ", with: "")
if removeSpaces.count > 0
{
let arrSTR = removeSpaces.components(separatedBy: ".")
if arrSTR.count > 1
{
var strAfterDecimal = arrSTR[1]
if strAfterDecimal.count >= 2
{
strAfterDecimal = strAfterDecimal[0..<2]
}else if strAfterDecimal.count != 0
{
strAfterDecimal = "\(strAfterDecimal)0"
}else
{
strAfterDecimal = "00"
}
let finalSTR = String("\(arrSTR[0]).\(strAfterDecimal)")
print("Final with Decimal - \(finalSTR)")
}else
{
let finalSTR = String(arrSTR[0] + ".00")
print("Final without Decimal - \(finalSTR)")
}
}
extension String {
subscript(_ range: CountableRange<Int>) -> String {
let idx1 = index(startIndex, offsetBy: max(0, range.lowerBound))
let idx2 = index(startIndex, offsetBy: min(self.count, range.upperBound))
return String(self[idx1..<idx2])
}
}

comma separated number with 2 digit fraction - swift

I have a textfield that it's input is price, so I want to get both like this: 1,111,999.99. I wrote to make it possible but there are two problems. First, after four digits and 2 fraction digit (like 1,234.00) it resets to zero. Second, I can't put fraction in it (fraction is always .00)
how can i make a textfield that receives 1,111,999.99 as input?
in my custom UITextfield:
private var numberFormatter: NumberFormatter {
let formatter = NumberFormatter()
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 0
formatter.numberStyle = .decimal
formatter.decimalSeparator = "."
formatter.groupingSeparator = ","
return formatter
}
var commaValue: String {
return numberFormatter.string(from: value)!
}
var value: NSNumber {
let number = numberFormatter.number(from: self.text ?? "0")
return number!
}
and in my textfieldDidChange method:
#IBAction func textfieldEditingChanged(_ sender: Any) {
let textfield = sender as! UITextField
textfield.text = textfield.commaValue
}
Solved it temporarily this way:
var formattedNumber: String {
guard self.text != "" else {return ""}
var fraction = ""
var digit = ""
let fractionExists = self.text!.contains(".")
let num = self.text?.replacingOccurrences(of: ",", with: "")
let sections = num!.characters.split(separator: ".")
if sections.first != nil
{
let str = String(sections.first!)
let double = Double(str)
guard double != nil else {return self.text ?? ""}
digit = numberFormatter.string(from: NSNumber(value: double ?? 0))!
}
if sections.count > 1
{
fraction = String(sections[1])
if fraction.characters.count > 2
{
fraction = String(fraction.prefix(2))
}
return "\(digit).\(fraction)"
}
if fractionExists
{
return "\(digit)."
}
return digit
}
.
#IBAction func textfieldEditingChanged(_ sender: Any) {
let textfield = sender as! UITextField
textfield.text = textfield.formattedNumber
}