1

I've developed a custom control for money input, which contains UITextField and UILabel. When the user taps on it, it becomes active and switches to the UITextField for data input and accepts only numbers and dot symbol, when the user finishes editing it becomes passive and switches to UILabel just to show formatted money value. But there is one little issue which I'm unable to fix a lot of days already.

Let's say the user writes down 88.99 and presses done, this becomes "$ 88.99" in a UILabel, next when the user again taps on it to edit the initial value I get the following value "88.98999999999999". To not present the entire code I selected the core part in a playground format which gives the same result as in my complete project:

extension NumberFormatter {
    static public func defaultCurrencyFormatter() -> NumberFormatter {
        let formatter = NumberFormatter()
        formatter.usesGroupingSeparator = true
        formatter.numberStyle = .currency
        formatter.currencySymbol = ""
        formatter.minimumFractionDigits = 1
        formatter.maximumFractionDigits = 2
        formatter.currencyGroupingSeparator = ","
        formatter.currencyDecimalSeparator = "."
        return formatter
    }
}

let stringValue = NumberFormatter.defaultCurrencyFormatter().number(from: "88.99")?.stringValue

print(stringValue) // result is Optional("88.98999999999999")

I have no idea why using this NumberFormatter I get such a result. I was thinking that explicitly setting minimumFractionDigits and maximumFractionDigits will solve my issue but it does not affect my result

  • 1
    Actually both `minimumFractionDigits` and `maximumFractionDigits` affect the behavior to format **numbers**, the opposite what you are doing. `stringValue` is related to `NSNumber` not to `NumberFormatter` – vadian Dec 09 '20 at 09:08
  • You might want to set this value to true: https://developer.apple.com/documentation/foundation/numberformatter/1410503-generatesdecimalnumbers – Arik Segal Dec 09 '20 at 09:28
  • If there is no currency symbol you should simply use `.decimal` style. – Leo Dabus Dec 09 '20 at 14:51

2 Answers2

0

NumberFormatter is legacy from objc and it operates with NSNumber/CGFloat etc. and usually it is helpful for localized text formatting. Much powerful and convenient parser for numbers is Scanner but if you don't have complex data structure to parse and don't want to deal with Floating-point error mitigation just use swift's Float:

// Float from string
if let float = Float("88.99") {
    print(float)
    
    // String from float
    let text = String(float)
    print(text)
}

Prints:

88.99
88.99
iUrii
  • 11,742
  • 1
  • 33
  • 48
  • I don't think Float is large enough to keep large money values that can hold Double. In this case, it might lose some parts. I know, ideally, I should use decimal, but this is a legacy project and it will take big time to refactor –  Dec 09 '20 at 11:05
  • @JunoQuantum Agreed, Double is preferred. – iUrii Dec 09 '20 at 11:11
  • @JunoQuantum You should use Swift `Decimal` type. And make sure to use the `Decimal(string:)` initializer – Leo Dabus Dec 09 '20 at 14:50
0

Try this:

extension String {
    var currencyStyle: String? {
        let formatter = NumberFormatter()
        formatter.minimumFractionDigits = 1
        formatter.maximumFractionDigits = 2
        formatter.usesGroupingSeparator = true
        formatter.groupingSize = 3
        formatter.currencyGroupingSeparator = ","
        formatter.currencyDecimalSeparator = "."

        if let double = Double(self) {
            let number = NSNumber(value: double)
            return formatter.string(from: number)
        }
        return nil
    }
}

to use it:

    let str = "12388.98999999999999".currencyStyle
    print(str) // Optional("12,388.99")
Almudhafar
  • 887
  • 1
  • 12
  • 26
  • No need to coerce the double to NSNumber. You can use Formatter's method `string(for: Any)` and pass your double. Note that this will create a new number formatter every time the user changes the field value. – Leo Dabus Dec 09 '20 at 14:56