Generatesdecimalnumbers for Numberformatter Does Not Work

generatesDecimalNumbers for NumberFormatter does not work

Setting generatesDecimalNumbers to true does not work as one might expect. The returned value is an instance of NSDecimalNumber (which can represent the value 8.3 exactly), but apparently the formatter converts the string to a binary floating number first (and that can not represent 8.3 exactly). Therefore the returned decimal value is only approximately correct.

That has also been reported as a bug:

  • NSDecimalNumbers from NSNumberFormatter are affected by binary approximation error

Note also that (contrary to the documentation), the maximumFractionDigits property has no effect when parsing a string
into a number.

There is a simple solution: Use

NSDecimalNumber(string: strValue) // or
NSDecimalNumber(string: strValue, locale: Locale.current)

instead, depending on whether the string is localized or not.

Or with the Swift 3 Decimal type:

Decimal(string: strValue) // or
Decimal(string: strValue, locale: .current)

Example:

if let d = Decimal(string: "8.2") {
print(d) // 8.2
}

Large decimal number formatting using NumberFormatter in Swift

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

NumberFormatter return the wrong number

If you do the same thing with 71.8, it will work. This is just because 71.9 can't be represented precisely in floating point numbers ( due to the finitude of bits )

Use integers ( price in cents ) or decimal numbers is the only issue to deal correctly with prices. Check the Decimal and NSDecimalNumber classes to see what you can do with this.

It is specially recommended if you do some computations on prices, ( If you pay 10$ with two friends in cash, two will pay 3.33, and one will pay 3.34 - so depending of your context, divide by 3 might not be enough)

let number = currencyFormatter.number(from: price) ?? NSNumber(value: 0)
var value = number.decimalValue
var roundedValue: Decimal = 0

// Right way: ( choose your precisions - 3 decimals here):
NSDecimalRound(&roundedValue, &value, 3, .bankers)

print(number)
print(value)
print(roundedValue)
71.90000000000001
71.90000000000001
71.9

If you just need to log your prices, simply use Int(price*100)/100
It will do the job

If you need more... Good luck :)

Edit

Following the excellent remark of @Nicholas Rees, I add this variation:

currencyFormatter.generatesDecimalNumbers = true

let number = (currencyFormatter.number(from: price) as? NSDecimalNumber) ?? NSDecimalNumber(value: 0)
var value = number.decimalValue
var roundedValue: Decimal = 0

// Right way: ( choose your precisions - 3 decimals here):
NSDecimalRound(&roundedValue, &value, 3, .bankers)

print(number)
print(value)
print(roundedValue)

There, the result in the same when logged, but I suppose the internal format of the 'value' is correct

Another approach is to remove currency and create decimal from string:

print(Decimal(string: "71.9") ?? 0)

71.9

NumberFormatter issue for large number

NumberFormatter produces an Int64 when it parses the string in the example. This type can exactly represent the numeric value. But the formatter uses double arithmetic even for 64 bit integers to compute the string representation of a number. A double can represent approximately 16 or 17 decimal digits at most. This leads to the suprising different results.

You can avoid this by using Decimals:

let formatter: NumberFormatter = NumberFormatter()
formatter.locale = Locale(identifier: "en_US")
formatter.generatesDecimalNumbers = true

let r1 = formatter.number(from: "10123456789012349")
let r2 = formatter.string(from: r1!)
print(r1!) // 10123456789012349
print(r2!) // 10123456789012349

EDIT: Using Decimals for formatting is sufficient for exact results:

let formatter: NumberFormatter = NumberFormatter()
formatter.locale = Locale(identifier: "en_US")
let d1 = 10123456789012349 as Decimal

print(formatter.string(from: d1 as NSNumber)!)

Euro errors with NumberFormatter and Currency Strings

You can simply trim all non digits from your string and use two number formatters, one with dot and the other with comma and use the nil coalescing operator in chain as follow:

extension Formatter {
static let decimalWithDotSeparator: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.decimalSeparator = "."
return numberFormatter
}()
static let decimalWithCommaSeparator: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.decimalSeparator = ","
return numberFormatter
}()
}

extension String {
var numberFromCurrency: NSDecimalNumber? {
var string = self
while let first = string.characters.first, Int("\(first)") == nil {
string = String(string.characters.dropFirst())
}
while let last = string.characters.last, Int("\(last)") == nil {
string = String(string.characters.dropLast())
}
return (Formatter.decimalWithDotSeparator.number(from: string) ??
Formatter.decimalWithCommaSeparator.number(from: string) ?? 0)
.decimalValue.decimalNumber
}
}

extension Decimal {
var decimalNumber: NSDecimalNumber { return NSDecimalNumber(decimal: self) }
}

let dollar = "$249.99"
dollar.numberFromCurrency // 249.99

let real = "R$249,99"
real.numberFromCurrency // 249.99

let euro = "0,50€"
euro.numberFromCurrency // 0.5

Issues with setMaximumFractionDigits

setMaximumFractionDigits and setMinimumFractionDigits used to formatting a number. or in other words it get applied while converting a number into string i.e. stringFromNumber. So it will not get applied when you are using numberFromString

How to get rounded value without fraction error when using numberFromString to generate NSDecimalNumber?

There is a factory method defined on NSDecimalNumber that takes a string:

+ (NSDecimalNumber *)decimalNumberWithString:(NSString *)numericString

Example:

$ swift
Welcome to Apple Swift version 2.1.1 (swiftlang-700.1.101.15 clang-700.1.81). Type :help for assistance.
1> import Foundation
2> NSDecimalNumber(string: "93.9")
$R0: NSDecimalNumber = 93.9
3>

Swift lose precision in decimal formatting

Using the new FormatStyle seems to generate the correct result

let format = Decimal.FormatStyle
.number
.precision(.fractionLength(0...2))


let text = "89806.9"
let value = try! format.parseStrategy.parse(text)

Below is an example parsing a currency using the currency code from the locale

let currencyFormat = Decimal.FormatStyle.Currency
.currency(code: Locale.current.currencyCode!)
.precision(.fractionLength(0...2))

let amount = try! currencyFormat.parseStrategy.parse(text)

Swedish example:

let text = "89806,9 kr"
print(amount)

89806.9

Another option is to use the new init for Decimal that takes a String and a FormatStyle.Currency (or a Number or Percent)

let amount = try Decimal(text, format: currencyFormat)

and to format this value we can use formatted(_:) on Decimal

print(amount.formatted(currencyFormat))

Output (still Swedish):

89 806,9 kr

How to get rounded value without fraction error when using numberFromString to generate NSDecimalNumber?

There is a factory method defined on NSDecimalNumber that takes a string:

+ (NSDecimalNumber *)decimalNumberWithString:(NSString *)numericString

Example:

$ swift
Welcome to Apple Swift version 2.1.1 (swiftlang-700.1.101.15 clang-700.1.81). Type :help for assistance.
1> import Foundation
2> NSDecimalNumber(string: "93.9")
$R0: NSDecimalNumber = 93.9
3>


Related Topics



Leave a reply



Submit