Skip to content

int(1.0/7*7) == 0 ??? #1220

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

Closed
notro opened this issue Sep 30, 2018 · 9 comments
Closed

int(1.0/7*7) == 0 ??? #1220

notro opened this issue Sep 30, 2018 · 9 comments

Comments

@notro
Copy link

notro commented Sep 30, 2018

I'm working on converting the datetime module and came across this.
Is it a known limitation or a bug?

Adafruit CircuitPython 3.0.1 on 2018-08-21; Adafruit CircuitPlayground Express with samd21g18
>>> int(1.0)
1
>>> 1.0/7*7
1.0
>>> int(1.0/7*7)
0
>>>
Adafruit CircuitPython patchbase-7-g216f0f952-dirty on 2018-09-30; Adafruit Metro M4 Express with samd51j19
>>> int(1.0)
1
>>> 1.0/7*7
1.0
>>> int(1.0/7*7)
0
>>>
Python 3.4.2 (default, Oct 19 2014, 13:31:11)
[GCC 4.9.1] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> int(1.0/7*7)
1
>>>
@jerryneedell
Copy link
Collaborator

jerryneedell commented Sep 30, 2018

FYI

Press any key to enter the REPL. Use CTRL-D to reload.
Adafruit CircuitPython 4.0.0-alpha.1-5-g48a3aafdd on 2018-09-25; PCA10059 nRF52840 Dongle with nRF52840
>>> 1.0/7*7
1.0
>>> int(1.0/7*7)
1
>>> 

but

Adafruit CircuitPython 4.0.0-alpha.1-2-gf2c960e89 on 2018-09-21; Adafruit Metro M4 Express with samd51j19
>>> 1.0/7*7
1.0
>>> int(1.0/7*7)
0
>>> 

@dhalbert
Copy link
Collaborator

It may print as 1.0 but it's not quite 1.0:


Adafruit CircuitPython 3.0.2 on 2018-09-14; Adafruit Feather M4 Express with samd51j19
>>> int(1.0/7*7)
0
>>> 1.0/7*7
1.0
>>> 1.0 - (1.0/7*7)
2.38419e-07

@jerryneedell
Copy link
Collaborator

interesting

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.

Press any key to enter the REPL. Use CTRL-D to reload.
Adafruit CircuitPython 4.0.0-alpha.1-5-g48a3aafdd on 2018-09-25; PCA10059 nRF52840 Dongle with nRF52840
>>> 1.0 - (1.0/7*7)
0.0

@dhalbert
Copy link
Collaborator

4.0.0 on M4 is still the same:

Adafruit CircuitPython 4.0.0-alpha.1-4-gf09537bbc on 2018-09-30; Adafruit Feather M4 Express with samd51j19
>>> int(1.0/7*7)
0
>>> 1.0 - (1.0/7*7)
2.38419e-07

Not sure what the difference is: it might be a compilation flag or some setup of the floating point unit. I thought both nRF and Atmel used the same conversion routines, so I'm a little surprised, but ma7be the don't.

@dhalbert
Copy link
Collaborator

I thought this might be because we are using the MicroPython-supplied libm on atmel-samd. But if I use the toolchain libm instead (which consumes an extra ~5600 bytes), I still get the same results on the Feather M4.

@notro
Copy link
Author

notro commented Sep 30, 2018

This is the gist of the code from datetime.py that I work on:

import math as _math
class timedelta:

    def __new__(cls, days=0, seconds=0, microseconds=0,
                milliseconds=0, minutes=0, hours=0, weeks=0):
        days += weeks*7

        if isinstance(days, float):
            dayfrac, days = _math.modf(days)
            d = int(days)
        else:
            d = days

        self = object.__new__(cls)
        self._days = d
        return self

The test wants these to be equal:

>>> timedelta(weeks=1.0/7)._days
0
>>> timedelta(days=1)._days
1

Any idea how I can achieve that in a general way under the current circumstances?

@jepler
Copy link

jepler commented Oct 1, 2018

In floating point math, it's not generally true that int(1 / i * i) == 1 for all integers i. On modern 64-bit Linux, the first positive number which is an exception is 49. On CircuitPython, since floating point numbers are stored in a more limited precision (slightly less precision than a 32-bit "float" on modern desktop computers, and a LOT less precision than the standard 64-bit "double" precision float of desktop Python), this problem affects smaller numbers such as 7.

Here's a similar "problem" on desktop Python 3.5.3:

>>> import datetime
>>> datetime.timedelta(weeks=1/11)*11
datetime.timedelta(7, 0, 2)

In this case, the duration is reported as 2 microseconds longer than 7 days.

If this problem is arising from a doctest, I suggest that perhaps the doctest could be rewritten so that it asserts that a timedelta of 1/7 weeks is close to a timedelta of 1 day, instead of asserting that it is equal exactly. For instance, you could assert that both are within 1 second of each other via the total_seconds() method, which should be true on python and circuitpython platforms.

@notro
Copy link
Author

notro commented Oct 1, 2018

Thanks for the explanation, floats and its lack of precision keep tripping me up.
I'll see what I can do with the tests.

Thanks all.

Example of the (im)precision showing the value I need to use to get exactly one day as a result:

>>> datetime.timedelta(weeks=1.0000005/7)
datetime.timedelta(1)

@notro notro closed this as completed Oct 1, 2018
@notro
Copy link
Author

notro commented Oct 1, 2018

total_seconds() was useful for the test case:

 class TestTimeDelta(HarmlessMixedComparison, unittest.TestCase):

     def test_constructor(self):

         # Check float args to constructor
+        def aeq(td1, td2):  ### Handle float imprecision
+            self.assertAlmostEqual(td1.total_seconds(), td2.total_seconds(), delta=1)
-        eq(td(weeks=1.0/7), td(days=1))
+        aeq(td(weeks=1.0/7), td(days=1))
-        eq(td(days=1.0/24), td(hours=1))
+        aeq(td(days=1.0/24), td(hours=1))
-        eq(td(hours=1.0/60), td(minutes=1))
+        aeq(td(hours=1.0/60), td(minutes=1))
         eq(td(minutes=1.0/60), td(seconds=1))
         eq(td(seconds=0.001), td(milliseconds=1))
         eq(td(milliseconds=0.001), td(microseconds=1))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants