Large decimal number formatting using NumberFormatter in Swift
Asked Answered
C

2

6

I've done this to format the number, but it fails for large numbers

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


    if let number = formatter.number(from: "123456789123456789123") , let str = formatter.string(from:number){

        print(number)
        print(str)
    }

It prints

123456789123456800000
123,456,789,123,456,800,000

It should print

123456789123456789123
123,456,789,123,456,789,123

I think there should be the number overflow, is there any alternative to achieve this kind of thing.

Cooksey answered 22/3, 2020 at 10:7 Comment(5)
You would have to set formatter.generatesDecimalNumbers = true but even that does not work due to a bug in NumberFormatter, compare https://mcmap.net/q/1078469/-generatesdecimalnumbers-for-numberformatter-does-not-work/1187415 which also has a workaround.Loritalorn
Possible duplicate of generatesDecimalNumbers for NumberFormatter does not work.Loritalorn
Not at all here I don't even use the decimal part just use the number style as decimalCooksey
Without the suggested options you create a Number using the binary floating point type Double for its internal representation. And the precision of Double is restricted to approx. 17 decimal digits.Loritalorn
@MartinR Do you know any other way to format such large numbers ?Cooksey
L
0

You could create a Decimal explicitly to work around the mentioned bug

let formatter = NumberFormatter()
formatter.numberStyle = .decimal
if let decimalNumber = Decimal(string: "123456789123456789123"), let str = formatter.string(from:decimalNumber as NSNumber) {
    print(decimalNumber)
    print(str)
}
Lax answered 22/3, 2020 at 10:31 Comment(1)
NumberFormatter does not accurately produce strings for all large Decimal() values.Wineskin
W
6

The root of this problem is three-fold:

  • As of Version 5.6, Swift does not support Decimal literals.
  • Decimal() supports up to 38 digits of precision.
  • Double() supports about 17 digits of precision.

Hence when using Decimals to represent numbers with more than 17 digits of precision, it is crucial to never do anything that would convert the Decimal's value to a Double. This includes initializing with literals:

print (Decimal(1234567890.12345678901234567890)) 
// 1234567890.1234569216
print (Decimal(string: "1234567890.12345678901234567890")!) 
// 1234567890.12345678901234567890

Decimal(1234567890.12345678901234567890) produces the value 1234567890.1234569216 because 1234567890.12345678901234567890 is a Double literal, limited to about 17 digits of precision.

Sadly, NumberFormatter has evidently never gotten the memo on this problem.

var dec = Decimal(string: "1234567890.12345678901234567890")!
let f = NumberFormatter()
f.numberStyle = .decimal
f.minimumFractionDigits = 20
print (f.string(from: dec as NSDecimalNumber)!) // 1,234,567,890.12346000000000000000

So at the present time, there is no way to make Swift accurately produce localized, decimal numbers with more than about 17 digits of precision. NumberFormatter cannot produce, for example, the String 1,234,567,890.12345678901234567890 even though Decimal can represent that exact value.

You can get non-localized string representations of Decimals using String(describing:)

print (String(describing:(Decimal(string: "1234567890.12345678901234567890")!)))
// 1234567890.1234567890123456789

For one of my own apps I made a little "poor man's" Decimal formatter, which simply outputs the full value using the decimal separator from the desired Locale:

let locale = Locale(identifier: "fr_FR")
dec = Decimal(string: "1234567890,12345678901234567890", locale: locale)!

let formatter = NumberFormatter()
formatter.locale = locale
let localizedSeparator = String(formatter.decimalSeparator.first ?? ".")

let fractionDigits = 20
var strings = String(describing:(dec)).split(separator: (Locale.current.decimalSeparator?.first) ?? "." )
var result = strings[0]
if fractionDigits > 0 {
    if strings.count > 1 {
        while strings[1].count < fractionDigits { strings[1] += "0" }
        result += localizedSeparator + String(strings[1].prefix(fractionDigits))
    }
    else {
        result += localizedSeparator + (0..<fractionDigits).map{_ in "0"}
    }
}

print (result) // 1234567890,12345678901234567890
Wineskin answered 18/3, 2022 at 17:31 Comment(0)
L
0

You could create a Decimal explicitly to work around the mentioned bug

let formatter = NumberFormatter()
formatter.numberStyle = .decimal
if let decimalNumber = Decimal(string: "123456789123456789123"), let str = formatter.string(from:decimalNumber as NSNumber) {
    print(decimalNumber)
    print(str)
}
Lax answered 22/3, 2020 at 10:31 Comment(1)
NumberFormatter does not accurately produce strings for all large Decimal() values.Wineskin

© 2022 - 2024 — McMap. All rights reserved.