Swift's Decimal Precision Issue

Swift's Decimal precision issue

NSDecimalNumber (and its overlay type Decimal) can represent

... any number that can be expressed as mantissa x 10^exponent where mantissa is a decimal integer up to 38 digits long, and exponent is an integer from –128 through 127.

So decimal fractions (with up to 38 decimal digits) can be represented
exactly, but not arbitrary numbers. In particular 1/24 = 0.416666666...
has infinitely many decimal digits (a repeating decimal) and cannot be
represented exactly as a Decimal.

Also there is no precision difference between Decimal and NSDecimalNumber. That becomes apparent if we print the difference
between the actual result and the "theoretical result":

let dec24 = Decimal(integerLiteral: 24)
let dec1 = Decimal(integerLiteral: 1)
let decResult = dec1/dec24*dec24

print(decResult - dec1)
// -0.00000000000000000000000000000000000016

let dn24 = NSDecimalNumber(value: 24)
let dn1 = NSDecimalNumber(value: 1)
let dnResult = dn1.dividing(by: dn24).multiplying(by: dn24)

print(dnResult.subtracting(dn1))
// -0.00000000000000000000000000000000000016

How can I initialize Decimal without losing precision in Swift

The problem is that all floating point literals are inferred to have type Double, which results in a loss of precision. Unfortunately Swift can't initialise floating point literals to Decimal directly.

If you want to keep precision, you need to initialise Decimal from a String literal rather than a floating point literal.

let decimalA = Decimal(string: "3.24")!
let double = 3.24
let decimalC: Decimal = 3.0 + 0.2 + 0.04
print(decimalA) // Prints 3.24
print(double) // Prints 3.24
print(decimalC) // Prints 3.24

Bear in mind this issue only happens with floating point literals, so if your floating point numbers are generated/parsed in runtime (such as reading from a file or parsing JSON), you shouldn't face the precision loss issue.

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

Rounding a double value to x number of decimal places in swift

You can use Swift's round function to accomplish this.

To round a Double with 3 digits precision, first multiply it by 1000, round it and divide the rounded result by 1000:

let x = 1.23556789
let y = Double(round(1000 * x) / 1000)
print(y) /// 1.236

Unlike any kind of printf(...) or String(format: ...) solutions, the result of this operation is still of type Double.

EDIT:

Regarding the comments that it sometimes does not work, please read this: What Every Programmer Should Know About Floating-Point Arithmetic

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


Related Topics



Leave a reply



Submit