classification
Title: round(1.65, 1) return 1.6 with decimal
Type: behavior Stage: resolved
Components: Library (Lib) Versions: Python 3.5
process
Status: closed Resolution: not a bug
Dependencies: Superseder:
Assigned To: Nosy List: Huan, mark.dickinson, umedoblock, valhallasw, zach.ware
Priority: normal Keywords:

Created on 2015-08-08 03:02 by umedoblock, last changed 2017-07-31 07:15 by mark.dickinson. This issue is now closed.

Messages (15)
msg248246 - (view) Author: umedoblock (umedoblock) Date: 2015-08-08 03:02
round(1.65, 1) return 1.6 with decimal.
I feel bug adobe result.
not bug ?

>>> import decimal
>>> d1 = decimal.Decimal("1.65")
>>> d2 = decimal.Decimal(10 ** -2) * 5
>>> d1
Decimal('1.65')
>>> d2
Decimal('0.05000000000000000104083408559')
>>> d1 + d2
Decimal('1.700000000000000001040834086')
>>> data = list(map(decimal.Decimal, "1.05 1.15 1.25 1.35 1.45 1.55 1.65 1.75 1.85 1.95".split()))
>>> for x in data:
...   print("round({}, 1) = {}".format(x, round(x, 1)))
... 
round(1.05, 1) = 1.0
round(1.15, 1) = 1.2
round(1.25, 1) = 1.2
round(1.35, 1) = 1.4
round(1.45, 1) = 1.4
round(1.55, 1) = 1.6
round(1.65, 1) = 1.6
round(1.75, 1) = 1.8
round(1.85, 1) = 1.8
round(1.95, 1) = 2.0
>>> round(2.675, 2)
2.67
>>> d4 = decimal.Decimal("2.675")
>>> round(d4, 2)
Decimal('2.68')
msg248247 - (view) Author: Zachary Ware (zach.ware) * (Python committer) Date: 2015-08-08 03:13
The rounding mode of the default context is ROUND_HALF_EVEN[1]:

>>> import decimal
>>> decimal.getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])

For your examples near the end, see [2]:

>>> round(2.675, 2)
2.67
>>> round(decimal.Decimal('2.675'), 2)
Decimal('2.68')
>>> decimal.Decimal(2.675)
Decimal('2.67499999999999982236431605997495353221893310546875')
>>> round(_, 2)
Decimal('2.67')

[1] https://docs.python.org/3/library/decimal.html#decimal.ROUND_HALF_EVEN
[2] https://docs.python.org/3/tutorial/floatingpoint.html
msg248249 - (view) Author: umedoblock (umedoblock) Date: 2015-08-08 05:38
I don't agree with "not a bug".

>>> s1, v1, ndigits1 = "1.65", 1.65, 1
>>> s2, v2, ndigits2 = "2.675", 2.675, 2

>>> decimal.Decimal(v1)
Decimal('1.649999999999999911182158029987476766109466552734375')
>>> round(v1, ndigits1)
1.6
>>> round(decimal.Decimal(s1), ndigits1)
Decimal('1.6') # EQUAL expression round(v1, ndigits1)

>>> decimal.Decimal(v2)
Decimal('2.67499999999999982236431605997495353221893310546875')
>>> round(v2, ndigits2)
2.67
>>> round(decimal.Decimal(s2), ndigits2)
Decimal('2.68') # DIFFERENT expression round(v2, ndigits2)

decimal module should give me different expression about below.
round(decimal.Decimal(s1), ndigits1) and round(v1, ndigits1).

BECAUSE

round(decimal.Decimal(s2), ndigits2) and round(v2, ndigits2)
give me DIFFERENT expression.
msg248250 - (view) Author: Zachary Ware (zach.ware) * (Python committer) Date: 2015-08-08 06:36
I think the key point that you're missing (and which I could have made clearer in my previous message) is that `Decimal(2.675) != Decimal('2.675')`.  In the first case, a Decimal instance is created from a float, and 2.675 cannot be represented perfectly in base-2.  The float is actually 2.67499999999999982236431605997495353221893310546875, but Python knows you're human and almost certainly didn't want that number, so it shows you 2.675 when asked.  The second Decimal instance is created from the string '2.675', and is converted straight to base-10.

Moving on to the rounding, both the float 2.675 and the Decimal created from the float 2.675 round down to 2.67 (or nearly, in the case of the float), because they're actually 2.674999..., and 4 rounds down.  The Decimal created from a string rounds to 2.68, because it actually is 2.675 and 5 rounds to even (in this case, 8).

>>> from decimal import Decimal as D
>>> f = 2.675
>>> s = str(f)
>>> s # Python chooses the shortest representation
'2.675'
>>> df = D(f)
>>> ds = D(s)
>>> f, df, ds
(2.675, Decimal('2.67499999999999982236431605997495353221893310546875'), Decimal('2.675'))
>>> f == df
True
>>> f == ds
False
>>> df == ds
False
>>> D(round(f, 2)), D(round(df, 2)), D(round(ds, 2))
(Decimal('2.6699999999999999289457264239899814128875732421875'), Decimal('2.67'), Decimal('2.68'))

The moral of the story is: everything is working as expected and don't create Decimals from floats unless you want the base-2 approximation of the value.
msg248251 - (view) Author: umedoblock (umedoblock) Date: 2015-08-08 07:11
last compared results are different.
should be bug or at least think that how to get a same result
about "D(round(df2, 2)) == D(round(ds2, 2))"

>>> from decimal import Decimal as D
>>> f1 = 1.65
>>> s1 = str(f1)
>>> df1 = D(f1)
>>> ds1 = D(s1)
>>> f2 = 2.675
>>> s2 = str(f2)
>>> df2 = D(f2)
>>> ds2 = D(s2)

>>> f1, df1, ds1
(1.65, Decimal('1.649999999999999911182158029987476766109466552734375'), Decimal('1.65'))
>>> f2, df2, ds2
(2.675, Decimal('2.67499999999999982236431605997495353221893310546875'), Decimal('2.675'))

>>> D(round(df1, 1)) == D(round(ds1, 1))
True
>>> D(round(df2, 2)) == D(round(ds2, 2))
False
msg248252 - (view) Author: umedoblock (umedoblock) Date: 2015-08-08 07:20
In addition.
>>> D(round(D("2.675"), 2)) == D("2.68")
True
>>> D(round(D("1.65"), 1)) == D("1.7")
False

I believe a bug or at least change the __round__().
msg248253 - (view) Author: umedoblock (umedoblock) Date: 2015-08-08 07:30
In this case.
>>> round(1.65, 1) == 1.7
False
>>> round(2.675, 2) == 2.68
False

I never say anything.
Because I understand what you said.
But I use the decimal module.
please pay attention to use decimal module.
msg248256 - (view) Author: umedoblock (umedoblock) Date: 2015-08-08 08:45
I have a headache.
because python reports many error after I patched below patches.

--- Lib/test/test_decimal.py.orig       2015-08-08 17:41:01.986316738 +0900
+++ Lib/test/test_decimal.py    2015-08-08 17:41:05.470316878 +0900
@@ -1935,6 +1935,7 @@
             ('123.456', 4, '123.4560'),
             ('123.455', 2, '123.46'),
             ('123.445', 2, '123.44'),
+            ('1.65', 1, '1.7'),
             ('Inf', 4, 'NaN'),
             ('-Inf', -23, 'NaN'),
             ('sNaN314', 3, 'NaN314'),

--- ./Lib/decimal.py.orig       2015-08-08 17:42:20.662319881 +0900
+++ ./Lib/decimal.py    2015-08-08 17:39:40.210313472 +0900
@@ -1782,7 +1782,7 @@
     def _round_half_even(self, prec):
         """Round 5 to even, rest to nearest."""
         if _exact_half(self._int, prec) and \
-                (prec == 0 or self._int[prec-1] in '02468'):
+                (prec == 0 or self._int[prec-1] in '01234'):
             return -1
         else:
             return self._round_half_up(prec)
msg248260 - (view) Author: Merlijn van Deen (valhallasw) * Date: 2015-08-08 10:52
As Zachary explained, the behavior is correct. There are three issues in play here.

1) The rounding method. With the ROUND_HALF_EVEN rounding mode, .5 is rounded to the nearest *even* number, so 1.65 is rounded to 1.6, while 1.75 is rounded to 1.8.

2) Rounding of floats. Floats cannot represent every number, and numbers are therefore rounded.

 - round(2.675, 2) = round(2.6749999999999998, 2) and is thus rounded to 2.67
 - round(1.65, 1) = round(1.6499999999999999, 1) and is thus rounded to 1.6

3a) In Python 2, round returns a float, so Decimal(round(Decimal("1.65"))) = Decimal(1.6) =  Decimal('1.600000000000000088817841970012523233890533447265625') != Decimal('1.6')

3b) In Python 3, Decimal.__round__ is implemented, so round(D("1.65"), 1) == D("1.6") as expected.
msg248268 - (view) Author: umedoblock (umedoblock) Date: 2015-08-08 13:14
excuse me.
I understand ROUND_HALF_EVEN meaning.
I think that __round__() function work ROUND_HALF_UP.
so sorry.
I don't have exactly knowledge about ROUND_HALF_EVEN.
I misunderstand about ROUND_HALF_EVEN.
I have thought ROUND_HALF_EVEN means ROUND_HALF_UP.

SO SORRY.
msg248309 - (view) Author: Zachary Ware (zach.ware) * (Python committer) Date: 2015-08-09 02:27
I'm glad you understand it now :)
msg299493 - (view) Author: Huan Wang (Huan) Date: 2017-07-30 09:00
Hello,
I was confused by the decimal module. The problem is that I want to 

from decimal import Decimal, ROUND_HALF_UP
def rounded(number, n):
    ''' Round the digits after the n_th decimal point by using
    decimal module in python.
    
    For example:
    2.453 is rounded by the function of deal_round(2.453, 1),
    it will return 2.5.
    2.453 is rounded by the function of deal_round(2.453, 2),
    it will return 2.45.
    '''
    val = Decimal(number)
    acc = str(n)  # n = 0.1 or 0.01 or 0.001
    return Decimal(val.quantize(Decimal(acc), rounding=ROUND_HALF_UP))

for x in np.arange(1.0, 4.01, 0.01):
    rounded_val = rounded(x, 0.1)
    print("{:}\t{:}".format(x, rounded_val))



The results obtained from the numpy array looks fine, but if I directly used rounded(1.45, 0.1), it yielded Decimal('1.4'), rather than Decimal('1.5').


I think it would be a bug.
msg299497 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2017-07-30 11:27
Huan,

This isn't a bug: see the earlier comments from Zachary Ware on this issue for explanations. When you compute `rounded(1.45, 0.1)`, you convert the *float* 1.45 to a Decimal instance. Thanks to the What You See Is Not What You Get nature of binary floating point, the actual value stored for 1.45 is:

1.4499999999999999555910790149937383830547332763671875

Conversion from float to Decimal is exact, so the Decimal value you're working with is also a touch under 1.45:

>>> from decimal import Decimal
>>> Decimal(1.45)
Decimal('1.4499999999999999555910790149937383830547332763671875')

And so it correctly rounds down to `1.4`.
msg299498 - (view) Author: Huan Wang (Huan) Date: 2017-07-30 11:45
Hi Mark,

Thank you for your reply.

I went over again the answer from Zachary Ware published on 2015-08-08 09:36. I got the point that it is better to use string type of number.

>>> from decimal import Decimal, ROUND_HALF_UP
>>> Decimal("1.45")
Decimal('1.45')

>>> Decimal(Decimal("1.45").quantize(Decimal("0.1"), rounding=ROUND_HALF_UP))
Decimal('1.5')

I think it is better to make a tip in the Python tutorial.
msg299540 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2017-07-31 07:15
> I think it is better to make a tip in the Python tutorial.

I'd recommend opening a separate issue (or pull request, if you're feeling adventurous) for that; this issue is old and closed, and it's unlikely many will be following it.
History
Date User Action Args
2017-07-31 07:15:26mark.dickinsonsetmessages: + msg299540
2017-07-30 11:45:01Huansetmessages: + msg299498
2017-07-30 11:27:45mark.dickinsonsetnosy: + mark.dickinson
messages: + msg299497
2017-07-30 09:00:00Huansetnosy: + Huan

messages: + msg299493
versions: + Python 3.5, - Python 3.3
2015-08-09 02:27:11zach.waresetmessages: + msg248309
2015-08-08 13:14:47umedoblocksetmessages: + msg248268
2015-08-08 10:52:29valhallaswsetnosy: + valhallasw
messages: + msg248260
2015-08-08 08:45:58umedoblocksetmessages: + msg248256
2015-08-08 07:30:08umedoblocksetmessages: + msg248253
2015-08-08 07:20:03umedoblocksetmessages: + msg248252
2015-08-08 07:11:41umedoblocksetmessages: + msg248251
2015-08-08 06:36:25zach.waresetmessages: + msg248250
title: round(1.65, 1) return 1.6 with decima modulel -> round(1.65, 1) return 1.6 with decimal
2015-08-08 05:38:41umedoblocksetmessages: + msg248249
title: round(1.65, 1) return 1.6 with decimal -> round(1.65, 1) return 1.6 with decima modulel
2015-08-08 03:13:53zach.waresetstatus: open -> closed

nosy: + zach.ware
messages: + msg248247

resolution: not a bug
stage: resolved
2015-08-08 03:02:03umedoblockcreate