Skip to content

Commit 7cd2679

Browse files
jbrockmendeljreback
authored andcommitted
ENH: implement DatetimeLikeArray (#19902)
1 parent 8b2070a commit 7cd2679

File tree

9 files changed

+347
-229
lines changed

9 files changed

+347
-229
lines changed

pandas/core/arrays/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
from .base import (ExtensionArray, # noqa
22
ExtensionScalarOpsMixin)
33
from .categorical import Categorical # noqa
4+
from .datetimes import DatetimeArrayMixin # noqa
5+
from .period import PeriodArrayMixin # noqa
6+
from .timedelta import TimedeltaArrayMixin # noqa

pandas/core/arrays/datetimelike.py

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import numpy as np
4+
5+
from pandas._libs import iNaT
6+
from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds
7+
8+
from pandas.tseries import frequencies
9+
10+
import pandas.core.common as com
11+
from pandas.core.algorithms import checked_add_with_arr
12+
13+
14+
class DatetimeLikeArrayMixin(object):
15+
"""
16+
Shared Base/Mixin class for DatetimeArray, TimedeltaArray, PeriodArray
17+
18+
Assumes that __new__/__init__ defines:
19+
_data
20+
_freq
21+
22+
and that the inheriting class has methods:
23+
_validate_frequency
24+
"""
25+
26+
@property
27+
def _box_func(self):
28+
"""
29+
box function to get object from internal representation
30+
"""
31+
raise com.AbstractMethodError(self)
32+
33+
def __iter__(self):
34+
return (self._box_func(v) for v in self.asi8)
35+
36+
@property
37+
def values(self):
38+
""" return the underlying data as an ndarray """
39+
return self._data.view(np.ndarray)
40+
41+
@property
42+
def asi8(self):
43+
# do not cache or you'll create a memory leak
44+
return self.values.view('i8')
45+
46+
# ------------------------------------------------------------------
47+
# Null Handling
48+
49+
@property # NB: override with cache_readonly in immutable subclasses
50+
def _isnan(self):
51+
""" return if each value is nan"""
52+
return (self.asi8 == iNaT)
53+
54+
@property # NB: override with cache_readonly in immutable subclasses
55+
def hasnans(self):
56+
""" return if I have any nans; enables various perf speedups """
57+
return self._isnan.any()
58+
59+
def _maybe_mask_results(self, result, fill_value=None, convert=None):
60+
"""
61+
Parameters
62+
----------
63+
result : a ndarray
64+
convert : string/dtype or None
65+
66+
Returns
67+
-------
68+
result : ndarray with values replace by the fill_value
69+
70+
mask the result if needed, convert to the provided dtype if its not
71+
None
72+
73+
This is an internal routine
74+
"""
75+
76+
if self.hasnans:
77+
if convert:
78+
result = result.astype(convert)
79+
if fill_value is None:
80+
fill_value = np.nan
81+
result[self._isnan] = fill_value
82+
return result
83+
84+
# ------------------------------------------------------------------
85+
# Frequency Properties/Methods
86+
87+
@property
88+
def freq(self):
89+
"""Return the frequency object if it is set, otherwise None"""
90+
return self._freq
91+
92+
@freq.setter
93+
def freq(self, value):
94+
if value is not None:
95+
value = frequencies.to_offset(value)
96+
self._validate_frequency(self, value)
97+
98+
self._freq = value
99+
100+
@property
101+
def freqstr(self):
102+
"""
103+
Return the frequency object as a string if its set, otherwise None
104+
"""
105+
if self.freq is None:
106+
return None
107+
return self.freq.freqstr
108+
109+
@property # NB: override with cache_readonly in immutable subclasses
110+
def inferred_freq(self):
111+
"""
112+
Tryies to return a string representing a frequency guess,
113+
generated by infer_freq. Returns None if it can't autodetect the
114+
frequency.
115+
"""
116+
try:
117+
return frequencies.infer_freq(self)
118+
except ValueError:
119+
return None
120+
121+
# ------------------------------------------------------------------
122+
# Arithmetic Methods
123+
124+
def _add_datelike(self, other):
125+
raise TypeError("cannot add {cls} and {typ}"
126+
.format(cls=type(self).__name__,
127+
typ=type(other).__name__))
128+
129+
def _sub_datelike(self, other):
130+
raise com.AbstractMethodError(self)
131+
132+
def _sub_period(self, other):
133+
return NotImplemented
134+
135+
def _add_offset(self, offset):
136+
raise com.AbstractMethodError(self)
137+
138+
def _add_delta(self, other):
139+
return NotImplemented
140+
141+
def _add_delta_td(self, other):
142+
"""
143+
Add a delta of a timedeltalike
144+
return the i8 result view
145+
"""
146+
inc = delta_to_nanoseconds(other)
147+
new_values = checked_add_with_arr(self.asi8, inc,
148+
arr_mask=self._isnan).view('i8')
149+
if self.hasnans:
150+
new_values[self._isnan] = iNaT
151+
return new_values.view('i8')
152+
153+
def _add_delta_tdi(self, other):
154+
"""
155+
Add a delta of a TimedeltaIndex
156+
return the i8 result view
157+
"""
158+
if not len(self) == len(other):
159+
raise ValueError("cannot add indices of unequal length")
160+
161+
self_i8 = self.asi8
162+
other_i8 = other.asi8
163+
new_values = checked_add_with_arr(self_i8, other_i8,
164+
arr_mask=self._isnan,
165+
b_mask=other._isnan)
166+
if self.hasnans or other.hasnans:
167+
mask = (self._isnan) | (other._isnan)
168+
new_values[mask] = iNaT
169+
return new_values.view('i8')

pandas/core/arrays/datetimes.py

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# -*- coding: utf-8 -*-
2+
import warnings
3+
4+
import numpy as np
5+
6+
from pandas._libs.tslib import Timestamp, NaT, iNaT
7+
from pandas._libs.tslibs import timezones
8+
9+
from pandas.util._decorators import cache_readonly
10+
11+
from pandas.core.dtypes.common import _NS_DTYPE, is_datetime64tz_dtype
12+
from pandas.core.dtypes.dtypes import DatetimeTZDtype
13+
14+
from .datetimelike import DatetimeLikeArrayMixin
15+
16+
17+
class DatetimeArrayMixin(DatetimeLikeArrayMixin):
18+
"""
19+
Assumes that subclass __new__/__init__ defines:
20+
tz
21+
_freq
22+
_data
23+
"""
24+
25+
# -----------------------------------------------------------------
26+
# Descriptive Properties
27+
28+
@property
29+
def _box_func(self):
30+
return lambda x: Timestamp(x, freq=self.freq, tz=self.tz)
31+
32+
@cache_readonly
33+
def dtype(self):
34+
if self.tz is None:
35+
return _NS_DTYPE
36+
return DatetimeTZDtype('ns', self.tz)
37+
38+
@property
39+
def tzinfo(self):
40+
"""
41+
Alias for tz attribute
42+
"""
43+
return self.tz
44+
45+
@property # NB: override with cache_readonly in immutable subclasses
46+
def _timezone(self):
47+
""" Comparable timezone both for pytz / dateutil"""
48+
return timezones.get_timezone(self.tzinfo)
49+
50+
@property
51+
def offset(self):
52+
"""get/set the frequency of the instance"""
53+
msg = ('DatetimeIndex.offset has been deprecated and will be removed '
54+
'in a future version; use DatetimeIndex.freq instead.')
55+
warnings.warn(msg, FutureWarning, stacklevel=2)
56+
return self.freq
57+
58+
@offset.setter
59+
def offset(self, value):
60+
"""get/set the frequency of the instance"""
61+
msg = ('DatetimeIndex.offset has been deprecated and will be removed '
62+
'in a future version; use DatetimeIndex.freq instead.')
63+
warnings.warn(msg, FutureWarning, stacklevel=2)
64+
self.freq = value
65+
66+
# -----------------------------------------------------------------
67+
# Comparison Methods
68+
69+
def _has_same_tz(self, other):
70+
zzone = self._timezone
71+
72+
# vzone sholdn't be None if value is non-datetime like
73+
if isinstance(other, np.datetime64):
74+
# convert to Timestamp as np.datetime64 doesn't have tz attr
75+
other = Timestamp(other)
76+
vzone = timezones.get_timezone(getattr(other, 'tzinfo', '__no_tz__'))
77+
return zzone == vzone
78+
79+
def _assert_tzawareness_compat(self, other):
80+
# adapted from _Timestamp._assert_tzawareness_compat
81+
other_tz = getattr(other, 'tzinfo', None)
82+
if is_datetime64tz_dtype(other):
83+
# Get tzinfo from Series dtype
84+
other_tz = other.dtype.tz
85+
if other is NaT:
86+
# pd.NaT quacks both aware and naive
87+
pass
88+
elif self.tz is None:
89+
if other_tz is not None:
90+
raise TypeError('Cannot compare tz-naive and tz-aware '
91+
'datetime-like objects.')
92+
elif other_tz is None:
93+
raise TypeError('Cannot compare tz-naive and tz-aware '
94+
'datetime-like objects')
95+
96+
# -----------------------------------------------------------------
97+
# Arithmetic Methods
98+
99+
def _sub_datelike_dti(self, other):
100+
"""subtraction of two DatetimeIndexes"""
101+
if not len(self) == len(other):
102+
raise ValueError("cannot add indices of unequal length")
103+
104+
self_i8 = self.asi8
105+
other_i8 = other.asi8
106+
new_values = self_i8 - other_i8
107+
if self.hasnans or other.hasnans:
108+
mask = (self._isnan) | (other._isnan)
109+
new_values[mask] = iNaT
110+
return new_values.view('timedelta64[ns]')

pandas/core/arrays/period.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from pandas._libs.tslibs.period import Period
4+
5+
from pandas.util._decorators import cache_readonly
6+
7+
from pandas.core.dtypes.dtypes import PeriodDtype
8+
9+
from .datetimelike import DatetimeLikeArrayMixin
10+
11+
12+
class PeriodArrayMixin(DatetimeLikeArrayMixin):
13+
@property
14+
def _box_func(self):
15+
return lambda x: Period._from_ordinal(ordinal=x, freq=self.freq)
16+
17+
@cache_readonly
18+
def dtype(self):
19+
return PeriodDtype.construct_from_string(self.freq)
20+
21+
@property
22+
def _ndarray_values(self):
23+
# Ordinals
24+
return self._data
25+
26+
@property
27+
def asi8(self):
28+
return self._ndarray_values.view('i8')

pandas/core/arrays/timedelta.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from pandas._libs.tslib import Timedelta
4+
5+
from pandas.core.dtypes.common import _TD_DTYPE
6+
7+
from .datetimelike import DatetimeLikeArrayMixin
8+
9+
10+
class TimedeltaArrayMixin(DatetimeLikeArrayMixin):
11+
@property
12+
def _box_func(self):
13+
return lambda x: Timedelta(x, unit='ns')
14+
15+
@property
16+
def dtype(self):
17+
return _TD_DTYPE

0 commit comments

Comments
 (0)