Python: Decimal addition and subtraction not giving exact result

Viewed 826

Python (3.8) code:

#!/usr/bin/env python3

from decimal import Decimal
from decimal import getcontext

x = Decimal('0.6666666666666666666666666667')
y = x;
print(getcontext().prec)
print(y)
print(y == x)
y += x; y += x; y += x;
y -= x; y -= x; y -= x;
print(y)
print(y == x)

Python output:

28
0.6666666666666666666666666667
True
0.6666666666666666666666666663
False

Java code:

import java.math.BigDecimal;

public class A
{
    public static void main(String[] args)
    {
        BigDecimal x = new BigDecimal("0.6666666666666666666666666667");
        BigDecimal y = new BigDecimal("0.6666666666666666666666666667");

        System.out.println(x.precision());
        System.out.println(y.precision());

        System.out.println(y);
        System.out.println(y.equals(x));

        y = y.add(x); y = y.add(x); y = y.add(x);
        y = y.subtract(x); y = y.subtract(x); y = y.subtract(x);

        System.out.println(y);
        System.out.println(y.equals(x));
    }
}

Java output:

28
28
0.6666666666666666666666666667
true
0.6666666666666666666666666667
true

What would be the way to achieve arbitrary precision in Python? By setting a very large prec?

4 Answers

From the Decimal docs:

The use of decimal floating point eliminates decimal representation error (making it possible to represent 0.1 exactly); however, some operations can still incur round-off error when non-zero digits exceed the fixed precision.

The effects of round-off error can be amplified by the addition or subtraction of nearly offsetting quantities resulting in loss of significance. Knuth provides two instructive examples where rounded floating point arithmetic with insufficient precision causes the breakdown of the associative and distributive properties of addition:

# Examples from Seminumerical Algorithms, Section 4.2.2.
>>> from decimal import Decimal, getcontext
>>> getcontext().prec = 8

>>> u, v, w = Decimal(11111113), Decimal(-11111111), Decimal('7.51111111')
>>> (u + v) + w 
Decimal('9.5111111')
>>> u + (v + w) 
Decimal('10')

>>> u, v, w = Decimal(20000), Decimal(-6), Decimal('6.0000003')
>>> (u*v) + (u*w) 
Decimal('0.01')
>>> u * (v+w) 
Decimal('0.0060000') 

From Python documentation:

The decimal module incorporates a notion of significant places so that 1.30 + 1.20 is 2.50.

Moreover, the following also need to be considered:

The context precision does not affect how many digits are stored. That is determined exclusively by the number of digits in value. For example, Decimal('3.00000') records all five zeros even if the context precision is only three.

Context precision and rounding only come into play during arithmetic operations.

Therefore:

import decimal
from decimal import Decimal

decimal.getcontext().prec = 4

a = Decimal('1.22222')

#1.22222 
#what you put in is what you get even though the prec was set to 4
print(a)        

b = Decimal('0.22222')

#0.22222
#Same reasoning as above
print(b)

a += 0; b += 0

#a will be printed as 1.222 (4 significant figures)
#b will be printed as 0.2222 (Leading zeroes are not significant!)
print('\n', a, '\n', b, sep='')

Your original value has 28 digits of precision, but when added to itself, the result is >0 and would have 29 digits, but rounded to 28 so it loses a digit of precision after the decimal.

Original result with intermediate results printed

28
0.6666666666666666666666666667   # 28 digits, leading zeros don't count
True
1.333333333333333333333333333    # note 28 digits, lost the last digit (4)
2.000000000000000000000000000
2.666666666666666666666666667
2.000000000000000000000000000
1.333333333333333333333333333
0.6666666666666666666666666663
False

Set the precision to 29 and it is correct:

29
0.6666666666666666666666666667
True
1.3333333333333333333333333334
2.0000000000000000000000000001
2.6666666666666666666666666668
2.0000000000000000000000000001
1.3333333333333333333333333334
0.6666666666666666666666666667
True

You can set precision to a very large value as per your need as follows:

getcontext().prec = 4000000000000 

It can be any integer value. Note that you cannot set this value to math.inf or to any infinity (for arbitrary precision - if that matters) as infinities do not opt for int class which is a check while setting precision. So do set a value which you will definitely not cross. Of course it does not hurt to have an enormous precision. For your case even a nominal value of 40 does the trick (In fact any 29+ value):

from decimal import Decimal
from decimal import getcontext
getcontext().prec = 40 # here cause its enough
x = Decimal('0.6666666666666666666666666667')
y = x;
print(getcontext().prec)
print(y)
print(y == x)
y += x; y += x; y += x;
y -= x; y -= x; y -= x;
print(y)
print(y == x)

results in:

40
0.6666666666666666666666666667
True
0.6666666666666666666666666667
True
Related