Skip to content

Commit 3ee7c6c

Browse files
alimcmaster1jorisvandenbossche
authored andcommitted
Fix Timestamp rounding (pandas-dev#21507)
(cherry picked from commit 76ef7c4)
1 parent e490aa1 commit 3ee7c6c

File tree

4 files changed

+62
-13
lines changed

4 files changed

+62
-13
lines changed

doc/source/whatsnew/v0.23.2.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Fixed Regressions
5454

5555
- Fixed regression in :meth:`to_csv` when handling file-like object incorrectly (:issue:`21471`)
5656
- Bug in both :meth:`DataFrame.first_valid_index` and :meth:`Series.first_valid_index` raised for a row index having duplicate values (:issue:`21441`)
57-
-
57+
- Bug in :meth:`Timestamp.ceil` and :meth:`Timestamp.floor` when timestamp is a multiple of the rounding frequency (:issue:`21262`)
5858

5959
.. _whatsnew_0232.performance:
6060

pandas/_libs/tslibs/timestamps.pyx

+23-11
Original file line numberDiff line numberDiff line change
@@ -59,42 +59,51 @@ cdef inline object create_timestamp_from_ts(int64_t value,
5959

6060

6161
def round_ns(values, rounder, freq):
62+
6263
"""
6364
Applies rounding function at given frequency
6465
6566
Parameters
6667
----------
67-
values : int, :obj:`ndarray`
68-
rounder : function
68+
values : :obj:`ndarray`
69+
rounder : function, eg. 'ceil', 'floor', 'round'
6970
freq : str, obj
7071
7172
Returns
7273
-------
73-
int or :obj:`ndarray`
74+
:obj:`ndarray`
7475
"""
76+
7577
from pandas.tseries.frequencies import to_offset
7678
unit = to_offset(freq).nanos
79+
80+
# GH21262 If the Timestamp is multiple of the freq str
81+
# don't apply any rounding
82+
mask = values % unit == 0
83+
if mask.all():
84+
return values
85+
r = values.copy()
86+
7787
if unit < 1000:
7888
# for nano rounding, work with the last 6 digits separately
7989
# due to float precision
8090
buff = 1000000
81-
r = (buff * (values // buff) + unit *
82-
(rounder((values % buff) * (1 / float(unit)))).astype('i8'))
91+
r[~mask] = (buff * (values[~mask] // buff) +
92+
unit * (rounder((values[~mask] % buff) *
93+
(1 / float(unit)))).astype('i8'))
8394
else:
8495
if unit % 1000 != 0:
8596
msg = 'Precision will be lost using frequency: {}'
8697
warnings.warn(msg.format(freq))
87-
8898
# GH19206
8999
# to deal with round-off when unit is large
90100
if unit >= 1e9:
91101
divisor = 10 ** int(np.log10(unit / 1e7))
92102
else:
93103
divisor = 10
94-
95-
r = (unit * rounder((values * (divisor / float(unit))) / divisor)
96-
.astype('i8'))
97-
104+
r[~mask] = (unit * rounder((values[~mask] *
105+
(divisor / float(unit))) / divisor)
106+
.astype('i8'))
98107
return r
99108

100109

@@ -649,7 +658,10 @@ class Timestamp(_Timestamp):
649658
else:
650659
value = self.value
651660

652-
r = round_ns(value, rounder, freq)
661+
value = np.array([value], dtype=np.int64)
662+
663+
# Will only ever contain 1 element for timestamp
664+
r = round_ns(value, rounder, freq)[0]
653665
result = Timestamp(r, unit='ns')
654666
if self.tz is not None:
655667
result = result.tz_localize(self.tz)

pandas/tests/indexes/datetimes/test_scalar_compat.py

+19
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,21 @@ def test_round(self, tz):
134134
ts = '2016-10-17 12:00:00.001501031'
135135
DatetimeIndex([ts]).round('1010ns')
136136

137+
def test_no_rounding_occurs(self, tz):
138+
# GH 21262
139+
rng = date_range(start='2016-01-01', periods=5,
140+
freq='2Min', tz=tz)
141+
142+
expected_rng = DatetimeIndex([
143+
Timestamp('2016-01-01 00:00:00', tz=tz, freq='2T'),
144+
Timestamp('2016-01-01 00:02:00', tz=tz, freq='2T'),
145+
Timestamp('2016-01-01 00:04:00', tz=tz, freq='2T'),
146+
Timestamp('2016-01-01 00:06:00', tz=tz, freq='2T'),
147+
Timestamp('2016-01-01 00:08:00', tz=tz, freq='2T'),
148+
])
149+
150+
tm.assert_index_equal(rng.round(freq='2T'), expected_rng)
151+
137152
@pytest.mark.parametrize('test_input, rounder, freq, expected', [
138153
(['2117-01-01 00:00:45'], 'floor', '15s', ['2117-01-01 00:00:45']),
139154
(['2117-01-01 00:00:45'], 'ceil', '15s', ['2117-01-01 00:00:45']),
@@ -143,6 +158,10 @@ def test_round(self, tz):
143158
['1823-01-01 00:00:01.000000020']),
144159
(['1823-01-01 00:00:01'], 'floor', '1s', ['1823-01-01 00:00:01']),
145160
(['1823-01-01 00:00:01'], 'ceil', '1s', ['1823-01-01 00:00:01']),
161+
(['2018-01-01 00:15:00'], 'ceil', '15T', ['2018-01-01 00:15:00']),
162+
(['2018-01-01 00:15:00'], 'floor', '15T', ['2018-01-01 00:15:00']),
163+
(['1823-01-01 03:00:00'], 'ceil', '3H', ['1823-01-01 03:00:00']),
164+
(['1823-01-01 03:00:00'], 'floor', '3H', ['1823-01-01 03:00:00']),
146165
(('NaT', '1823-01-01 00:00:01'), 'floor', '1s',
147166
('NaT', '1823-01-01 00:00:01')),
148167
(('NaT', '1823-01-01 00:00:01'), 'ceil', '1s',

pandas/tests/scalar/timestamp/test_unary_ops.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,25 @@ def test_ceil_floor_edge(self, test_input, rounder, freq, expected):
118118
expected = Timestamp(expected)
119119
assert result == expected
120120

121+
@pytest.mark.parametrize('test_input, freq, expected', [
122+
('2018-01-01 00:02:06', '2s', '2018-01-01 00:02:06'),
123+
('2018-01-01 00:02:00', '2T', '2018-01-01 00:02:00'),
124+
('2018-01-01 00:04:00', '4T', '2018-01-01 00:04:00'),
125+
('2018-01-01 00:15:00', '15T', '2018-01-01 00:15:00'),
126+
('2018-01-01 00:20:00', '20T', '2018-01-01 00:20:00'),
127+
('2018-01-01 03:00:00', '3H', '2018-01-01 03:00:00'),
128+
])
129+
@pytest.mark.parametrize('rounder', ['ceil', 'floor', 'round'])
130+
def test_round_minute_freq(self, test_input, freq, expected, rounder):
131+
# Ensure timestamps that shouldnt round dont!
132+
# GH#21262
133+
134+
dt = Timestamp(test_input)
135+
expected = Timestamp(expected)
136+
func = getattr(dt, rounder)
137+
result = func(freq)
138+
assert result == expected
139+
121140
def test_ceil(self):
122141
dt = Timestamp('20130101 09:10:11')
123142
result = dt.ceil('D')
@@ -257,7 +276,6 @@ def test_timestamp(self):
257276
if PY3:
258277
# datetime.timestamp() converts in the local timezone
259278
with tm.set_timezone('UTC'):
260-
261279
# should agree with datetime.timestamp method
262280
dt = ts.to_pydatetime()
263281
assert dt.timestamp() == ts.timestamp()

0 commit comments

Comments
 (0)