Skip to content

Commit 6256833

Browse files
committed
BUG: Timestamp.round precision error for ns (#15578)
1 parent 09360d8 commit 6256833

File tree

5 files changed

+41
-9
lines changed

5 files changed

+41
-9
lines changed

doc/source/whatsnew/v0.20.0.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ Bug Fixes
652652
- Bug in ``Index`` power operations with reversed operands (:issue:`14973`)
653653
- Bug in ``TimedeltaIndex`` addition where overflow was being allowed without error (:issue:`14816`)
654654
- Bug in ``TimedeltaIndex`` raising a ``ValueError`` when boolean indexing with ``loc`` (:issue:`14946`)
655-
- Bug in ``DatetimeIndex.round()`` and ``Timestamp.round()`` floating point accuracy when rounding by milliseconds (:issue: `14440`)
655+
- Bug in ``DatetimeIndex.round()`` and ``Timestamp.round()`` floating point accuracy when rounding by milliseconds or less (:issue: `14440`, :issue:`15578`)
656656
- Bug in ``astype()`` where ``inf`` values were incorrectly converted to integers. Now raises error now with ``astype()`` for Series and DataFrames (:issue:`14265`)
657657
- Bug in ``DataFrame(..).apply(to_numeric)`` when values are of type decimal.Decimal. (:issue:`14827`)
658658
- Bug in ``describe()`` when passing a numpy array which does not contain the median to the ``percentiles`` keyword argument (:issue:`14908`)

pandas/tests/indexes/datetimes/test_ops.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -175,17 +175,25 @@ def test_round(self):
175175
tm.assertRaisesRegexp(ValueError, msg, rng.round, freq='M')
176176
tm.assertRaisesRegexp(ValueError, msg, elt.round, freq='M')
177177

178-
# GH 14440
178+
# GH 14440 & 15578
179179
index = pd.DatetimeIndex(['2016-10-17 12:00:00.0015'], tz=tz)
180180
result = index.round('ms')
181181
expected = pd.DatetimeIndex(['2016-10-17 12:00:00.002000'], tz=tz)
182182
tm.assert_index_equal(result, expected)
183183

184+
for freq in ['us', 'ns']:
185+
tm.assert_index_equal(index, index.round(freq))
186+
184187
index = pd.DatetimeIndex(['2016-10-17 12:00:00.00149'], tz=tz)
185188
result = index.round('ms')
186189
expected = pd.DatetimeIndex(['2016-10-17 12:00:00.001000'], tz=tz)
187190
tm.assert_index_equal(result, expected)
188191

192+
index = pd.DatetimeIndex(['2016-10-17 12:00:00.001501031'])
193+
result = index.round('10ns')
194+
expected = pd.DatetimeIndex(['2016-10-17 12:00:00.001501030'])
195+
tm.assert_index_equal(result, expected)
196+
189197
def test_repeat_range(self):
190198
rng = date_range('1/1/2000', '1/1/2001')
191199

pandas/tests/scalar/test_timestamp.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ def test_round(self):
732732
for freq in ['Y', 'M', 'foobar']:
733733
self.assertRaises(ValueError, lambda: dti.round(freq))
734734

735-
# GH 14440
735+
# GH 14440 & 15578
736736
result = pd.Timestamp('2016-10-17 12:00:00.0015').round('ms')
737737
expected = pd.Timestamp('2016-10-17 12:00:00.002000')
738738
self.assertEqual(result, expected)
@@ -741,6 +741,14 @@ def test_round(self):
741741
expected = pd.Timestamp('2016-10-17 12:00:00.001000')
742742
self.assertEqual(result, expected)
743743

744+
ts = pd.Timestamp('2016-10-17 12:00:00.0015')
745+
for freq in ['us', 'ns']:
746+
self.assertEqual(ts, ts.round(freq))
747+
748+
result = pd.Timestamp('2016-10-17 12:00:00.001501031').round('10ns')
749+
expected = pd.Timestamp('2016-10-17 12:00:00.001501030')
750+
self.assertEqual(result, expected)
751+
744752
def test_class_ops_pytz(self):
745753
tm._skip_if_no_pytz()
746754
from pytz import timezone

pandas/tseries/base.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,19 @@ class TimelikeOps(object):
7878
def _round(self, freq, rounder):
7979

8080
from pandas.tseries.frequencies import to_offset
81-
unit = to_offset(freq).nanos
82-
81+
offset = to_offset(freq)
82+
unit = offset.nanos
83+
time_unit = offset.rule_code
8384
# round the local times
8485
values = _ensure_datetimelike_to_i8(self)
85-
86-
result = (unit * rounder(values / float(unit)).astype('i8'))
86+
if time_unit == 'N':
87+
# for nano rounding, work with the last 6 digits separately
88+
# due to float precision
89+
buff = 1000000
90+
result = (buff * (values // buff) + unit *
91+
(rounder((values % buff) / float(unit))).astype('i8'))
92+
else:
93+
result = (unit * rounder(values / float(unit)).astype('i8'))
8794
result = self._maybe_mask_results(result, fill_value=tslib.NaT)
8895
attribs = self._get_attributes_dict()
8996
if 'freq' in attribs:

pandas/tslib.pyx

+11-2
Original file line numberDiff line numberDiff line change
@@ -416,12 +416,21 @@ class Timestamp(_Timestamp):
416416
cdef object result, value
417417

418418
from pandas.tseries.frequencies import to_offset
419-
unit = to_offset(freq).nanos
419+
offset = to_offset(freq)
420+
unit = offset.nanos
421+
time_unit = offset.rule_code
420422
if self.tz is not None:
421423
value = self.tz_localize(None).value
422424
else:
423425
value = self.value
424-
result = (unit * rounder(value / float(unit)).astype('i8'))
426+
if time_unit == 'N':
427+
# for nano rounding, work with the last 6 digits separately
428+
# due to float precision
429+
buff = 1000000
430+
result = (buff * (value // buff) + unit *
431+
(rounder((value % buff) / float(unit))).astype('i8'))
432+
else:
433+
result = (unit * rounder(value / float(unit)).astype('i8'))
425434
result = Timestamp(result, unit='ns')
426435
if self.tz is not None:
427436
result = result.tz_localize(self.tz)

0 commit comments

Comments
 (0)