@@ -22,6 +22,7 @@ cimport ccalendar
22
22
from conversion import tz_localize_to_utc, normalize_i8_timestamps
23
23
from conversion cimport (tz_convert_single, _TSObject,
24
24
convert_to_tsobject, convert_datetime_to_tsobject)
25
+ import enum
25
26
from fields import get_start_end_field, get_date_name_field
26
27
from nattype import NaT
27
28
from nattype cimport NPY_NAT
@@ -57,50 +58,114 @@ cdef inline object create_timestamp_from_ts(int64_t value,
57
58
return ts_base
58
59
59
60
60
- def round_ns (values , rounder , freq ):
61
+ @enum.unique
62
+ class RoundTo (enum .Enum ):
61
63
"""
62
- Applies rounding function at given frequency
64
+ enumeration defining the available rounding modes
65
+
66
+ Attributes
67
+ ----------
68
+ MINUS_INFTY
69
+ round towards -∞, or floor [2]_
70
+ PLUS_INFTY
71
+ round towards +∞, or ceil [3]_
72
+ NEAREST_HALF_EVEN
73
+ round to nearest, tie-break half to even [6]_
74
+ NEAREST_HALF_MINUS_INFTY
75
+ round to nearest, tie-break half to -∞ [5]_
76
+ NEAREST_HALF_PLUS_INFTY
77
+ round to nearest, tie-break half to +∞ [4]_
78
+
79
+
80
+ References
81
+ ----------
82
+ .. [1] "Rounding - Wikipedia"
83
+ https://en.wikipedia.org/wiki/Rounding
84
+ .. [2] "Rounding down"
85
+ https://en.wikipedia.org/wiki/Rounding#Rounding_down
86
+ .. [3] "Rounding up"
87
+ https://en.wikipedia.org/wiki/Rounding#Rounding_up
88
+ .. [4] "Round half up"
89
+ https://en.wikipedia.org/wiki/Rounding#Round_half_up
90
+ .. [5] "Round half down"
91
+ https://en.wikipedia.org/wiki/Rounding#Round_half_down
92
+ .. [6] "Round half to even"
93
+ https://en.wikipedia.org/wiki/Rounding#Round_half_to_even
94
+ """
95
+ MINUS_INFTY = 0
96
+ PLUS_INFTY = 1
97
+ NEAREST_HALF_EVEN = 2
98
+ NEAREST_HALF_PLUS_INFTY = 3
99
+ NEAREST_HALF_MINUS_INFTY = 4
100
+
101
+
102
+ cdef inline _npdivmod(x1, x2):
103
+ """ implement divmod for numpy < 1.13"""
104
+ return np.floor_divide(x1, x2), np.remainder(x1, x2)
105
+
106
+
107
+ try :
108
+ from numpy import divmod as npdivmod
109
+ except ImportError :
110
+ npdivmod = _npdivmod
111
+
112
+
113
+ cdef inline _floor_int64(values, unit):
114
+ return values - np.remainder(values, unit)
115
+
116
+ cdef inline _ceil_int64(values, unit):
117
+ return values + np.remainder(- values, unit)
118
+
119
+ cdef inline _rounddown_int64(values, unit):
120
+ return _ceil_int64(values - unit// 2 , unit)
121
+
122
+ cdef inline _roundup_int64(values, unit):
123
+ return _floor_int64(values + unit// 2 , unit)
124
+
125
+
126
+ def round_nsint64 (values , mode , freq ):
127
+ """
128
+ Applies rounding mode at given frequency
63
129
64
130
Parameters
65
131
----------
66
132
values : :obj:`ndarray`
67
- rounder : function, eg. 'ceil', 'floor', 'round'
133
+ mode : instance of `RoundTo` enumeration
68
134
freq : str, obj
69
135
70
136
Returns
71
137
-------
72
138
:obj:`ndarray`
73
139
"""
140
+
141
+ if not isinstance (mode, RoundTo):
142
+ raise ValueError (' mode should be a RoundTo member' )
143
+
74
144
unit = to_offset(freq).nanos
75
145
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
146
+ if mode is RoundTo.MINUS_INFTY:
147
+ return _floor_int64(values, unit)
148
+ elif mode is RoundTo.PLUS_INFTY:
149
+ return _ceil_int64(values, unit)
150
+ elif mode is RoundTo.NEAREST_HALF_MINUS_INFTY:
151
+ return _rounddown_int64(values, unit)
152
+ elif mode is RoundTo.NEAREST_HALF_PLUS_INFTY:
153
+ return _roundup_int64(values, unit)
154
+ elif mode is RoundTo.NEAREST_HALF_EVEN:
155
+ # for odd unit there is no need of a tie break
156
+ if unit % 2 :
157
+ return _rounddown_int64(values, unit)
158
+ quotient, remainder = npdivmod(values, unit)
159
+ mask = np.logical_or(
160
+ remainder > (unit // 2 ),
161
+ np.logical_and(remainder == (unit // 2 ), quotient % 2 )
162
+ )
163
+ quotient[mask] += 1
164
+ return quotient * unit
165
+
166
+ # if/elif above should catch all rounding modes defined in enum 'RoundTo':
167
+ # if flow of control arrives here, it is a bug
168
+ assert False , " round_nsint64 called with an unrecognized rounding mode"
104
169
105
170
106
171
# This is PITA. Because we inherit from datetime, which has very specific
@@ -656,7 +721,7 @@ class Timestamp(_Timestamp):
656
721
657
722
return create_timestamp_from_ts(ts.value, ts.dts, ts.tzinfo, freq)
658
723
659
- def _round (self , freq , rounder , ambiguous = ' raise' ):
724
+ def _round (self , freq , mode , ambiguous = ' raise' ):
660
725
if self .tz is not None :
661
726
value = self .tz_localize(None ).value
662
727
else :
@@ -665,7 +730,7 @@ class Timestamp(_Timestamp):
665
730
value = np.array([value], dtype = np.int64)
666
731
667
732
# Will only ever contain 1 element for timestamp
668
- r = round_ns (value, rounder , freq)[0 ]
733
+ r = round_nsint64 (value, mode , freq)[0 ]
669
734
result = Timestamp(r, unit = ' ns' )
670
735
if self .tz is not None :
671
736
result = result.tz_localize(self .tz, ambiguous = ambiguous)
@@ -694,7 +759,7 @@ class Timestamp(_Timestamp):
694
759
------
695
760
ValueError if the freq cannot be converted
696
761
"""
697
- return self ._round(freq, np.round , ambiguous)
762
+ return self ._round(freq, RoundTo.NEAREST_HALF_EVEN , ambiguous)
698
763
699
764
def floor (self , freq , ambiguous = ' raise' ):
700
765
"""
@@ -715,7 +780,7 @@ class Timestamp(_Timestamp):
715
780
------
716
781
ValueError if the freq cannot be converted
717
782
"""
718
- return self ._round(freq, np.floor , ambiguous)
783
+ return self ._round(freq, RoundTo.MINUS_INFTY , ambiguous)
719
784
720
785
def ceil (self , freq , ambiguous = ' raise' ):
721
786
"""
@@ -736,7 +801,7 @@ class Timestamp(_Timestamp):
736
801
------
737
802
ValueError if the freq cannot be converted
738
803
"""
739
- return self ._round(freq, np.ceil , ambiguous)
804
+ return self ._round(freq, RoundTo.PLUS_INFTY , ambiguous)
740
805
741
806
@property
742
807
def tz (self ):
0 commit comments