Reputation: 2799
I'm having some trouble with datetime in Python. I tried converting a datetime to a timestamp and then back again and no matter how I try the end result is not the same. I always end up with a datetime of datetime(2014, 1, 30, 23, 59, 40, 1998).
import datetime
a = datetime.datetime.timestamp(datetime.datetime(2014, 1, 30, 23, 59, 40, 1999))
b = datetime.datetime.fromtimestamp(a)
print(b)
Upvotes: 5
Views: 2078
Reputation: 308520
Very few floating point numbers with a decimal fraction can be exactly represented as a binary floating point number; there will usually be some very small error. Sometimes it will be smaller than the desired number, and sometimes it will be larger, but it should always be very very close. Your example's exact value is 1391147980.0019989013671875
which is within 0.1 microsecond of what you specified.
The conversion from a floating point timestamp
back to a datetime
should employ rounding to make sure the round-trip conversion gives the same value as the original. As noted by J.F. Sebastian, this was entered as a bug against Python 3.4; it is claimed to be fixed in later releases, but it still exists in Python 3.5.0 using the same values as given in the question. Running a test similar to nigel222 shows an almost 50/50 split between exact matches and results that are low by 1 microsecond.
Since you know that the original value was an integral number of microseconds, you can add an offset that ensures the binary floating point value is always higher than the decimal value while still being small enough that it doesn't affect the result when properly rounded. Since rounding should occur at 0.5 microseconds the ideal offset would be half of that, or 0.25 microseconds.
Here are the results in Python 3.5.0:
>>> a = datetime.datetime.timestamp(datetime.datetime(2014, 1, 30, 23, 59, 40, 1999))
>>> b = datetime.datetime.fromtimestamp(a)
>>> a
1391147980.001999
>>> b
datetime.datetime(2014, 1, 30, 23, 59, 40, 1998)
>>> b = datetime.datetime.fromtimestamp(a + 0.00000025)
>>> b
datetime.datetime(2014, 1, 30, 23, 59, 40, 1999)
>>> counter={}
>>> for i in range(0,1000000):
# fuzz up some random-ish dates
d = datetime.datetime(1990+(i%20), 1+(i%12), 1+(i%28), i%24, i%60, i%60, i)
ts = datetime.datetime.timestamp(d)
b = datetime.datetime.fromtimestamp(ts + 0.00000025)
msdif = d.microsecond - b.microsecond
if msdif in counter:
counter[msdif] += 1
else:
counter[msdif]=1
assert b.day==d.day and b.hour==d.hour and b.minute==d.minute and b.second==d.second
>>> counter
{0: 1000000}
Upvotes: 1
Reputation: 414795
It is a known Python 3.4 issue:
>>> from datetime import datetime
>>> local = datetime(2014, 1, 30, 23, 59, 40, 1999)
>>> datetime.fromtimestamp(local.timestamp())
datetime.datetime(2014, 1, 30, 23, 59, 40, 1998)
Note: the microsecond is gone. The .timestamp()
already returns result that is slightly less than 1999
microseconds:
>>> from decimal import Decimal
>>> local.timestamp()
1391126380.001999
>>> Decimal(local.timestamp())
Decimal('1391126380.0019989013671875')
The rounding is fixed in the next 3.4, 3.5, 3.6 releases:
>>> from datetime import datetime
>>> local = datetime(2014, 1, 30, 23, 59, 40, 1999)
>>> datetime.fromtimestamp(local.timestamp())
datetime.datetime(2014, 1, 30, 23, 59, 40, 1999)
To workaround the issue, you could use the explicit formula:
>>> from datetime import datetime, timedelta
>>> local = datetime(2014, 1, 30, 23, 59, 40, 1999)
>>> datetime.utcfromtimestamp(local.timestamp())
datetime.datetime(2014, 1, 30, 23, 59, 40, 1998) # UTC time
>>> datetime(1970, 1, 1) + timedelta(seconds=local.timestamp())
datetime.datetime(2014, 1, 30, 23, 59, 40, 1999) # UTC time
Note: the input in all examples is the local time but the result is UTC time in the last one.
Upvotes: 4
Reputation: 8222
That last number is microseconds ... are the internals that accurate? Let's find out.
counter={}
for i in range(0,1000000,43):
# fuzz up some random-ish dates
d = datetime.datetime( 1990+(i%20), 1+(i%12), 1+(i%28), i%24, i%60, i%60, i)
ts=datetime.datetime.timestamp( d)
b = b=datetime.datetime.fromtimestamp(ts)
msdif=d.microsecond-b.microsecond
if msdif in counter:
counter[msdif] += 1
else:
counter[msdif]=1
assert b.day==d.day and b.hour==d.hour and b.minute==d.minute and b.second=d.second
>>>
>>> counter
{1: 23256}
>>>
I do believe you have found an off-by-one-microsecond error in the datetime library, unless there is something perverse buried in the specifications.
(I was expecting a spread around zero, reflecting rounding errors of some sort)
Upvotes: 3