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:
NSDecimalNumber
s fromNSNumberFormatter
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 Decimal
s:
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 Decimal
s 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
What Is the Use of the Validate() Method in Alamofire.Request
Retrieve Multiple Photos Under a Node from Firebase Storage
Suppressing Implicit Returns in Swift
Nsdatecomponents Weekofyear Returns Wrong Date
Why Is It Legal to Mutate an Actor's Nonsendable Property
Injecting a New Stylesheet into a Website via Uiwebview Using iOS8 Swift Xcode 6
How to Print Call Stack in Swift
Singleton and Init with Parameter
How to Create an Array with Incremented Values in Swift
Using Compiler Variables in Swift
Nsimage Getting Resized When I Draw Text on It
How to Pass Int.Init to a Function in Swift
In Swift, Does Int Have a Hidden Initializer That Takes a String
In Swift,There's No Way to Get the Returned Function's Argument Names
"Cgfloat Is Not Convertible to Int" When Trying to Calculate an Expression