Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to format UILabel text to locale specific NSNumberFormatterStyle.DecimalStyle

I have a UILabel to show input text (which has decimal numbers). What I am trying to do is, that the UILabel text can be shown as per locale. E.g.-

"1234.56" should be shown as "1,234.56" for US locale, "1.234,56" for DE (Germany) locale and so on.

Below is what I have done to achieve this -

NSNumberFormatter.numberStyle = NSNumberFormatterStyle.DecimalStyle
NSNumberFormatter.locale = NSLocale(localeIdentifier: language)
NSNumberFormatter.maximumFractionDigits = 16

let displayVal: Double = NSNumberFormatter.numberFromString(displayText.text!).doubleValue
displayText.text = NSNumberFormatter.stringFromNumber(displayVal)

Where displayText is the UILabel to display input.

It works fine until 4 digits i.e. "1234" was shown as "1,234" (for US locale). But as soon as 5th digit gets added, nil returned for displayVal constant variable.

I could guess from the function definition that there is failure in formatting displayText text string which may be "1,2345" when numberFromString tried to format it as per US locale but failed as this does not comply to US locale format.

Can anyone suggest a way to fix this or nicer way of implementation?

like image 494
CuriousDev Avatar asked Jan 21 '26 10:01

CuriousDev


1 Answers

You can do something like the following, namely strip out any thousands separators, get a number from that, and then reformat with the thousands separator:

@IBAction func didEditingChanged(_ sender: UITextField) {
    guard let text = sender.text else { return }

    let formatter = NumberFormatter()
    formatter.numberStyle = .decimal

    // get thousands separator

    let thousandsSeparator = formatter.groupingSeparator ?? ","

    // strip thousands separators out

    let stripped = String(text.filter { return String($0) != thousandsSeparator })

    // now, re-insert them, if any

    if let number = formatter.numberFromString(stripped) {
        sender.text = formatter.stringFromNumber(number)
    }
}

Assuming you're hooking this up to "Editing Changed" so that it's constantly updated, this introduces a few issues. For example, to enter the trailing decimal place, you might have to manually check for that and allow that to be preserved:

@IBAction func didEditingChanged(_ sender: UITextField) {
    guard let text = sender.text else { return }

    let formatter = NumberFormatter()
    formatter.numberStyle = .decimal

    // get thousands separator

    let thousandsSeparator = formatter.groupingSeparator ?? ","

    // strip thousands separators out

    let stripped = String(text.filter { return String($0) != thousandsSeparator })

    // see if decimal place found, and if so, set fractional digits accordingly

    let decimalSeparator = formatter.decimalSeparator ?? "."
    var fractionalDigits: Int?
    if let decimalSeparatorIndex = text.range(of: decimalSeparator)?.lowerBound {
        fractionalDigits = text[decimalSeparatorIndex...].count(of: "0123456789")
        formatter.minimumFractionDigits = fractionalDigits!
    }

    guard
        let number = formatter.number(from: stripped),
        var result = formatter.string(from: number)
    else {
        // do whatever is appropriate if string isn't a number at all
        return
    }

    // re-add trailing decimal place only if decimal separator was found, but no fractional digits were

    if fractionalDigits == 0 {
        result += decimalSeparator
    }

    // update text field

    sender.text = result
}

Note, that uses this little extension:

extension StringProtocol {
    func count(of string: String) -> Int {
        return reduce(0) { $0 + (string.contains($1) ? 1 : 0) }
    }
}

There are many other refinements you could tackle. For example, if the user is not at the end of the text field when they edit, this will reset the selectedTextRange. You also should probably implement shouldChangeCharactersIn to make sure that you cannot enter value at all where stripped would fail. Etc.

But hopefully this illustrates one approach for capturing text input and updating the result with the formatted decimal number.


For Swift 2 rendition, see previous revision of this answer.

like image 131
Rob Avatar answered Jan 23 '26 05:01

Rob