Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

round(1.65, 1) return 1.6 with decimal #69015

Closed
umedoblock mannequin opened this issue Aug 8, 2015 · 15 comments
Closed

round(1.65, 1) return 1.6 with decimal #69015

umedoblock mannequin opened this issue Aug 8, 2015 · 15 comments
Labels
stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error

Comments

@umedoblock
Copy link
Mannequin

umedoblock mannequin commented Aug 8, 2015

BPO 24827
Nosy @mdickinson, @zware, @LePingKYXK

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = None
closed_at = <Date 2015-08-08.03:13:53.146>
created_at = <Date 2015-08-08.03:02:03.900>
labels = ['invalid', 'type-bug', 'library']
title = 'round(1.65, 1) return 1.6 with decimal'
updated_at = <Date 2017-07-31.07:15:26.646>
user = 'https://bugs.python.org/umedoblock'

bugs.python.org fields:

activity = <Date 2017-07-31.07:15:26.646>
actor = 'mark.dickinson'
assignee = 'none'
closed = True
closed_date = <Date 2015-08-08.03:13:53.146>
closer = 'zach.ware'
components = ['Library (Lib)']
creation = <Date 2015-08-08.03:02:03.900>
creator = 'umedoblock'
dependencies = []
files = []
hgrepos = []
issue_num = 24827
keywords = []
message_count = 15.0
messages = ['248246', '248247', '248249', '248250', '248251', '248252', '248253', '248256', '248260', '248268', '248309', '299493', '299497', '299498', '299540']
nosy_count = 5.0
nosy_names = ['mark.dickinson', 'valhallasw', 'umedoblock', 'zach.ware', 'Huan']
pr_nums = []
priority = 'normal'
resolution = 'not a bug'
stage = 'resolved'
status = 'closed'
superseder = None
type = 'behavior'
url = 'https://bugs.python.org/issue24827'
versions = ['Python 3.5']

@umedoblock
Copy link
Mannequin Author

umedoblock mannequin commented Aug 8, 2015

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

@umedoblock umedoblock mannequin added stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error labels Aug 8, 2015
@zware
Copy link
Member

zware commented Aug 8, 2015

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

@zware zware closed this as completed Aug 8, 2015
@zware zware added the invalid label Aug 8, 2015
@umedoblock
Copy link
Mannequin Author

umedoblock mannequin commented Aug 8, 2015

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.

@umedoblock umedoblock mannequin changed the title round(1.65, 1) return 1.6 with decimal round(1.65, 1) return 1.6 with decima modulel Aug 8, 2015
@zware
Copy link
Member

zware commented Aug 8, 2015

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.

@zware zware changed the title round(1.65, 1) return 1.6 with decima modulel round(1.65, 1) return 1.6 with decimal Aug 8, 2015
@umedoblock
Copy link
Mannequin Author

umedoblock mannequin commented Aug 8, 2015

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

@umedoblock
Copy link
Mannequin Author

umedoblock mannequin commented Aug 8, 2015

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__().

@umedoblock
Copy link
Mannequin Author

umedoblock mannequin commented Aug 8, 2015

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.

@umedoblock
Copy link
Mannequin Author

umedoblock mannequin commented Aug 8, 2015

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)

@valhallasw
Copy link
Mannequin

valhallasw mannequin commented Aug 8, 2015

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.

@umedoblock
Copy link
Mannequin Author

umedoblock mannequin commented Aug 8, 2015

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.

@zware
Copy link
Member

zware commented Aug 9, 2015

I'm glad you understand it now :)

@LePingKYXK
Copy link
Mannequin

LePingKYXK mannequin commented Jul 30, 2017

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.

@mdickinson
Copy link
Member

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.

@LePingKYXK
Copy link
Mannequin

LePingKYXK mannequin commented Jul 30, 2017

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.

@mdickinson
Copy link
Member

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.

@ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

2 participants