Fix accuracy issues in datetime decoding PYTHON-392

This commit is contained in:
Bernie Hackett 2012-12-14 13:21:32 -08:00
parent 6242782e60
commit 168fdc61cd
3 changed files with 42 additions and 9 deletions

View File

@ -217,11 +217,15 @@ def _get_boolean(data, position, as_class, tz_aware, uuid_subtype):
def _get_date(data, position, as_class, tz_aware, uuid_subtype):
seconds = float(struct.unpack("<q", data[position:position + 8])[0]) / 1000.0
millis = struct.unpack("<q", data[position:position + 8])[0]
diff = millis % 1000
seconds = (millis - diff) / 1000
position += 8
if tz_aware:
return EPOCH_AWARE + datetime.timedelta(seconds=seconds), position
return EPOCH_NAIVE + datetime.timedelta(seconds=seconds), position
dt = EPOCH_AWARE + datetime.timedelta(seconds=seconds)
else:
dt = EPOCH_NAIVE + datetime.timedelta(seconds=seconds)
return dt.replace(microsecond=diff * 1000), position
def _get_code(data, position, as_class, tz_aware, uuid_subtype):

View File

@ -117,8 +117,37 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer, int type_by
/* Date stuff */
static PyObject* datetime_from_millis(long long millis) {
int microseconds = (millis % 1000) * 1000;
Time64_T seconds = millis / 1000;
/* To encode a datetime instance like datetime(9999, 12, 31, 23, 59, 59, 999999)
* we follow these steps:
* 1. Calculate a timestamp in seconds: 253402300799
* 2. Multiply that by 1000: 253402300799000
* 3. Add in microseconds divided by 1000 253402300799999
*
* (Note: BSON doesn't support microsecond accuracy, hence the rounding.)
*
* To decode we could do:
* 1. Get seconds: timestamp / 1000: 253402300799
* 2. Get micros: (timestamp % 1000) * 1000: 999000
* Resulting in datetime(9999, 12, 31, 23, 59, 59, 999000) -- the expected result
*
* Now what if the we encode (1, 1, 1, 1, 1, 1, 111111)?
* 1. and 2. gives: -62135593139000
* 3. Gives us: -62135593138889
*
* Now decode:
* 1. Gives us: -62135593138
* 2. Gives us: -889000
* Resulting in datetime(1, 1, 1, 1, 1, 2, 15888216) -- an invalid result
*
* If instead to decode we do:
* diff = ((millis % 1000) + 1000) % 1000: 111
* seconds = (millis - diff) / 1000: -62135593139
* micros = diff * 1000 111000
* Resulting in datetime(1, 1, 1, 1, 1, 1, 111000) -- the expected result
*/
int diff = (int)(((millis % 1000) + 1000) % 1000);
int microseconds = diff * 1000;
Time64_T seconds = (millis - diff) / 1000;
struct TM timeinfo;
gmtime64_r(&seconds, &timeinfo);

View File

@ -224,20 +224,20 @@ class TestBSON(unittest.TestCase):
def test_datetime_encode_decode(self):
# Negative timestamps
dt1 = datetime.datetime(1, 1, 1, 1, 1, 1)
dt1 = datetime.datetime(1, 1, 1, 1, 1, 1, 111000)
dt2 = BSON.encode({"date": dt1}).decode()["date"]
self.assertEqual(dt1, dt2)
dt1 = datetime.datetime(1959, 6, 25, 12, 16, 59)
dt1 = datetime.datetime(1959, 6, 25, 12, 16, 59, 999000)
dt2 = BSON.encode({"date": dt1}).decode()["date"]
self.assertEqual(dt1, dt2)
# Positive timestamps
dt1 = datetime.datetime(9999, 12, 31, 23, 59, 59)
dt1 = datetime.datetime(9999, 12, 31, 23, 59, 59, 999000)
dt2 = BSON.encode({"date": dt1}).decode()["date"]
self.assertEqual(dt1, dt2)
dt1 = datetime.datetime(2011, 6, 14, 10, 47, 53)
dt1 = datetime.datetime(2011, 6, 14, 10, 47, 53, 444000)
dt2 = BSON.encode({"date": dt1}).decode()["date"]
self.assertEqual(dt1, dt2)