Skip to content

Commit 1adfd41

Browse files
committed
implement int64 rounding function round_nsint64
The old `round_ns` is replaced by `round_nsint64`; `round_nsint64` is based on integer arithmetic while `round_ns` was based on floating point numbers. Rounding mode is explicitly defined by RoundTo enum class: - RoundTo.MINUS_INFTY rounds to -∞ (floor) - RountTo.PLUS_INFTY rounds to +∞ (ceil) - RoundTo.NEAREST_HALF_MINUS_INFTY rounds to nearest multiple, and breaks tie to -∞ Other rounding modes are not yet implemented.
1 parent 0976e12 commit 1adfd41

File tree

2 files changed

+35
-42
lines changed

2 files changed

+35
-42
lines changed

pandas/_libs/tslibs/timestamps.pyx

+29-36
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ cimport ccalendar
2222
from conversion import tz_localize_to_utc, normalize_i8_timestamps
2323
from conversion cimport (tz_convert_single, _TSObject,
2424
convert_to_tsobject, convert_datetime_to_tsobject)
25+
import enum
2526
from fields import get_start_end_field, get_date_name_field
2627
from nattype import NaT
2728
from nattype cimport NPY_NAT
@@ -57,50 +58,42 @@ cdef inline object create_timestamp_from_ts(int64_t value,
5758
return ts_base
5859

5960

60-
def round_ns(values, rounder, freq):
61+
class RoundTo(enum.Enum):
62+
MINUS_INFTY = enum.auto()
63+
PLUS_INFTY = enum.auto()
64+
NEAREST_HALF_EVEN = enum.auto()
65+
NEAREST_HALF_PLUS_INFTY = enum.auto()
66+
NEAREST_HALF_MINUS_INFTY = enum.auto()
67+
68+
69+
def round_nsint64(values, mode, freq):
6170
"""
62-
Applies rounding function at given frequency
71+
Applies rounding mode at given frequency
6372
6473
Parameters
6574
----------
6675
values : :obj:`ndarray`
67-
rounder : function, eg. 'ceil', 'floor', 'round'
76+
mode : instance of `RoundTo` enumeration
6877
freq : str, obj
6978
7079
Returns
7180
-------
7281
:obj:`ndarray`
7382
"""
83+
84+
if not isinstance(mode, RoundTo):
85+
raise ValueError('mode should be a RoundTo member')
86+
7487
unit = to_offset(freq).nanos
7588

76-
# GH21262 If the Timestamp is multiple of the freq str
77-
# don't apply any rounding
78-
mask = values % unit == 0
79-
if mask.all():
80-
return values
81-
r = values.copy()
82-
83-
if unit < 1000:
84-
# for nano rounding, work with the last 6 digits separately
85-
# due to float precision
86-
buff = 1000000
87-
r[~mask] = (buff * (values[~mask] // buff) +
88-
unit * (rounder((values[~mask] % buff) *
89-
(1 / float(unit)))).astype('i8'))
90-
else:
91-
if unit % 1000 != 0:
92-
msg = 'Precision will be lost using frequency: {}'
93-
warnings.warn(msg.format(freq))
94-
# GH19206
95-
# to deal with round-off when unit is large
96-
if unit >= 1e9:
97-
divisor = 10 ** int(np.log10(unit / 1e7))
98-
else:
99-
divisor = 10
100-
r[~mask] = (unit * rounder((values[~mask] *
101-
(divisor / float(unit))) / divisor)
102-
.astype('i8'))
103-
return r
89+
if mode is RoundTo.MINUS_INFTY:
90+
return values - (values % unit)
91+
elif mode is RoundTo.PLUS_INFTY:
92+
return values + (-values % unit)
93+
elif mode is RoundTo.NEAREST_HALF_MINUS_INFTY:
94+
return round_nsint64(values - unit//2, RoundTo.PLUS_INFTY, freq)
95+
96+
raise NotImplementedError(mode)
10497

10598

10699
# This is PITA. Because we inherit from datetime, which has very specific
@@ -656,7 +649,7 @@ class Timestamp(_Timestamp):
656649

657650
return create_timestamp_from_ts(ts.value, ts.dts, ts.tzinfo, freq)
658651

659-
def _round(self, freq, rounder):
652+
def _round(self, freq, mode):
660653
if self.tz is not None:
661654
value = self.tz_localize(None).value
662655
else:
@@ -665,7 +658,7 @@ class Timestamp(_Timestamp):
665658
value = np.array([value], dtype=np.int64)
666659

667660
# Will only ever contain 1 element for timestamp
668-
r = round_ns(value, rounder, freq)[0]
661+
r = round_nsint64(value, mode, freq)[0]
669662
result = Timestamp(r, unit='ns')
670663
if self.tz is not None:
671664
result = result.tz_localize(self.tz)
@@ -687,7 +680,7 @@ class Timestamp(_Timestamp):
687680
------
688681
ValueError if the freq cannot be converted
689682
"""
690-
return self._round(freq, np.round)
683+
return self._round(freq, RoundTo.NEAREST_HALF_MINUS_INFTY)
691684

692685
def floor(self, freq):
693686
"""
@@ -697,7 +690,7 @@ class Timestamp(_Timestamp):
697690
----------
698691
freq : a freq string indicating the flooring resolution
699692
"""
700-
return self._round(freq, np.floor)
693+
return self._round(freq, RoundTo.MINUS_INFTY)
701694

702695
def ceil(self, freq):
703696
"""
@@ -707,7 +700,7 @@ class Timestamp(_Timestamp):
707700
----------
708701
freq : a freq string indicating the ceiling resolution
709702
"""
710-
return self._round(freq, np.ceil)
703+
return self._round(freq, RoundTo.PLUS_INFTY)
711704

712705
@property
713706
def tz(self):

pandas/core/indexes/datetimelike.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import numpy as np
1212

1313
from pandas._libs import lib, iNaT, NaT
14-
from pandas._libs.tslibs.timestamps import round_ns
14+
from pandas._libs.tslibs.timestamps import round_nsint64, RoundTo
1515

1616
from pandas.core.dtypes.common import (
1717
ensure_int64,
@@ -168,10 +168,10 @@ class TimelikeOps(object):
168168
"""
169169
)
170170

171-
def _round(self, freq, rounder):
171+
def _round(self, freq, mode):
172172
# round the local times
173173
values = _ensure_datetimelike_to_i8(self)
174-
result = round_ns(values, rounder, freq)
174+
result = round_nsint64(values, mode, freq)
175175
result = self._maybe_mask_results(result, fill_value=NaT)
176176

177177
attribs = self._get_attributes_dict()
@@ -184,15 +184,15 @@ def _round(self, freq, rounder):
184184

185185
@Appender((_round_doc + _round_example).format(op="round"))
186186
def round(self, freq, *args, **kwargs):
187-
return self._round(freq, np.round)
187+
return self._round(freq, RoundTo.NEAREST_HALF_MINUS_INFTY)
188188

189189
@Appender((_round_doc + _floor_example).format(op="floor"))
190190
def floor(self, freq):
191-
return self._round(freq, np.floor)
191+
return self._round(freq, RoundTo.MINUS_INFTY)
192192

193193
@Appender((_round_doc + _ceil_example).format(op="ceil"))
194194
def ceil(self, freq):
195-
return self._round(freq, np.ceil)
195+
return self._round(freq, RoundTo.PLUS_INFTY)
196196

197197

198198
class DatetimeIndexOpsMixin(DatetimeLikeArrayMixin):

0 commit comments

Comments
 (0)