@@ -1908,15 +1908,24 @@ def delta_to_json(delta: timedelta) -> str:
19081908class _Timestamp (Timestamp ):
19091909 @classmethod
19101910 def from_datetime (cls , dt : datetime ) -> "_Timestamp" :
1911- # apparently 0 isn't a year in [0, 9999]??
1912- seconds = int ((dt - DATETIME_ZERO ).total_seconds ())
1913- nanos = int (dt .microsecond * 1e3 )
1914- return cls (seconds , nanos )
1911+ # manual epoch offset calulation to avoid rounding errors,
1912+ # to support negative timestamps (before 1970) and skirt
1913+ # around datetime bugs (apparently 0 isn't a year in [0, 9999]??)
1914+ offset = dt - DATETIME_ZERO
1915+ # below is the same as timedelta.total_seconds() but without dividing by 1e6
1916+ # so we end up with microseconds as integers instead of seconds as float
1917+ offset_us = (
1918+ offset .days * 24 * 60 * 60 + offset .seconds
1919+ ) * 10 ** 6 + offset .microseconds
1920+ seconds , us = divmod (offset_us , 10 ** 6 )
1921+ return cls (seconds , us * 1000 )
19151922
19161923 def to_datetime (self ) -> datetime :
1917- ts = self .seconds + (self .nanos / 1e9 )
1918- # if datetime.fromtimestamp ever supports -62135596800 use that instead see #407
1919- return DATETIME_ZERO + timedelta (seconds = ts )
1924+ # datetime.fromtimestamp() expects a timestamp in seconds, not microseconds
1925+ # if we pass it as a floating point number, we will run into rounding errors
1926+ # see also #407
1927+ offset = timedelta (seconds = self .seconds , microseconds = self .nanos // 1000 )
1928+ return DATETIME_ZERO + offset
19201929
19211930 @staticmethod
19221931 def timestamp_to_json (dt : datetime ) -> str :
0 commit comments