diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 83ee335aa54657..a6f254c79fb518 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -10,11 +10,12 @@ from pandas._libs.tslibs.period import ( DIFFERENT_FREQ_INDEX, IncompatibleFrequency, Period) from pandas._libs.tslibs.timedeltas import Timedelta, delta_to_nanoseconds -from pandas._libs.tslibs.timestamps import maybe_integer_op_deprecated +from pandas._libs.tslibs.timestamps import ( + RoundTo, maybe_integer_op_deprecated, round_nsint64) import pandas.compat as compat from pandas.errors import ( AbstractMethodError, NullFrequencyError, PerformanceWarning) -from pandas.util._decorators import deprecate_kwarg +from pandas.util._decorators import Appender, deprecate_kwarg from pandas.core.dtypes.common import ( is_bool_dtype, is_datetime64_any_dtype, is_datetime64_dtype, @@ -80,6 +81,189 @@ def _get_attributes_dict(self): return {k: getattr(self, k, None) for k in self._attributes} +class DatelikeOps(object): + """ + Common ops for DatetimeIndex/PeriodIndex, but not TimedeltaIndex. + """ + + def strftime(self, date_format): + from pandas import Index + return Index(self.format(date_format=date_format), + dtype=compat.text_type) + strftime.__doc__ = """ + Convert to Index using specified date_format. + + Return an Index of formatted strings specified by date_format, which + supports the same string format as the python standard library. Details + of the string format can be found in `python string format doc <{0}>`__ + + Parameters + ---------- + date_format : str + Date format string (e.g. "%Y-%m-%d"). + + Returns + ------- + Index + Index of formatted strings + + See Also + -------- + to_datetime : Convert the given argument to datetime. + DatetimeIndex.normalize : Return DatetimeIndex with times to midnight. + DatetimeIndex.round : Round the DatetimeIndex to the specified freq. + DatetimeIndex.floor : Floor the DatetimeIndex to the specified freq. + + Examples + -------- + >>> rng = pd.date_range(pd.Timestamp("2018-03-10 09:00"), + ... periods=3, freq='s') + >>> rng.strftime('%B %d, %Y, %r') + Index(['March 10, 2018, 09:00:00 AM', 'March 10, 2018, 09:00:01 AM', + 'March 10, 2018, 09:00:02 AM'], + dtype='object') + """.format("https://docs.python.org/3/library/datetime.html" + "#strftime-and-strptime-behavior") + + +class TimelikeOps(object): + """ + Common ops for TimedeltaIndex/DatetimeIndex, but not PeriodIndex. + """ + + _round_doc = ( + """ + Perform {op} operation on the data to the specified `freq`. + + Parameters + ---------- + freq : str or Offset + The frequency level to {op} the index to. Must be a fixed + frequency like 'S' (second) not 'ME' (month end). See + :ref:`frequency aliases ` for + a list of possible `freq` values. + ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise' + Only relevant for DatetimeIndex: + + - 'infer' will attempt to infer fall dst-transition hours based on + order + - bool-ndarray where True signifies a DST time, False designates + a non-DST time (note that this flag is only applicable for + ambiguous times) + - 'NaT' will return NaT where there are ambiguous times + - 'raise' will raise an AmbiguousTimeError if there are ambiguous + times + + .. versionadded:: 0.24.0 + nonexistent : 'shift', 'NaT', default 'raise' + A nonexistent time does not exist in a particular timezone + where clocks moved forward due to DST. + + - 'shift' will shift the nonexistent time forward to the closest + existing time + - 'NaT' will return NaT where there are nonexistent times + - 'raise' will raise an NonExistentTimeError if there are + nonexistent times + + .. versionadded:: 0.24.0 + + Returns + ------- + DatetimeIndex, TimedeltaIndex, or Series + Index of the same type for a DatetimeIndex or TimedeltaIndex, + or a Series with the same index for a Series. + + Raises + ------ + ValueError if the `freq` cannot be converted. + + Examples + -------- + **DatetimeIndex** + + >>> rng = pd.date_range('1/1/2018 11:59:00', periods=3, freq='min') + >>> rng + DatetimeIndex(['2018-01-01 11:59:00', '2018-01-01 12:00:00', + '2018-01-01 12:01:00'], + dtype='datetime64[ns]', freq='T') + """) + + _round_example = ( + """>>> rng.round('H') + DatetimeIndex(['2018-01-01 12:00:00', '2018-01-01 12:00:00', + '2018-01-01 12:00:00'], + dtype='datetime64[ns]', freq=None) + + **Series** + + >>> pd.Series(rng).dt.round("H") + 0 2018-01-01 12:00:00 + 1 2018-01-01 12:00:00 + 2 2018-01-01 12:00:00 + dtype: datetime64[ns] + """) + + _floor_example = ( + """>>> rng.floor('H') + DatetimeIndex(['2018-01-01 11:00:00', '2018-01-01 12:00:00', + '2018-01-01 12:00:00'], + dtype='datetime64[ns]', freq=None) + + **Series** + + >>> pd.Series(rng).dt.floor("H") + 0 2018-01-01 11:00:00 + 1 2018-01-01 12:00:00 + 2 2018-01-01 12:00:00 + dtype: datetime64[ns] + """ + ) + + _ceil_example = ( + """>>> rng.ceil('H') + DatetimeIndex(['2018-01-01 12:00:00', '2018-01-01 12:00:00', + '2018-01-01 13:00:00'], + dtype='datetime64[ns]', freq=None) + + **Series** + + >>> pd.Series(rng).dt.ceil("H") + 0 2018-01-01 12:00:00 + 1 2018-01-01 12:00:00 + 2 2018-01-01 13:00:00 + dtype: datetime64[ns] + """ + ) + + def _round(self, freq, mode, ambiguous, nonexistent): + # round the local times + values = _ensure_datetimelike_to_i8(self) + result = round_nsint64(values, mode, freq) + result = self._maybe_mask_results(result, fill_value=NaT) + + attribs = self._get_attributes_dict() + attribs['freq'] = None + if 'tz' in attribs: + attribs['tz'] = None + return self._ensure_localized( + self._shallow_copy(result, **attribs), ambiguous, nonexistent + ) + + @Appender((_round_doc + _round_example).format(op="round")) + def round(self, freq, ambiguous='raise', nonexistent='raise'): + return self._round( + freq, RoundTo.NEAREST_HALF_EVEN, ambiguous, nonexistent + ) + + @Appender((_round_doc + _floor_example).format(op="floor")) + def floor(self, freq, ambiguous='raise', nonexistent='raise'): + return self._round(freq, RoundTo.MINUS_INFTY, ambiguous, nonexistent) + + @Appender((_round_doc + _ceil_example).format(op="ceil")) + def ceil(self, freq, ambiguous='raise', nonexistent='raise'): + return self._round(freq, RoundTo.PLUS_INFTY, ambiguous, nonexistent) + + class DatetimeLikeArrayMixin(ExtensionOpsMixin, AttributesMixin): """ Shared Base/Mixin class for DatetimeArray, TimedeltaArray, PeriodArray @@ -1023,3 +1207,39 @@ def validate_dtype_freq(dtype, freq): raise IncompatibleFrequency('specified freq and dtype ' 'are different') return freq + + +def _ensure_datetimelike_to_i8(other, to_utc=False): + """ + Helper for coercing an input scalar or array to i8. + + Parameters + ---------- + other : 1d array + to_utc : bool, default False + If True, convert the values to UTC before extracting the i8 values + If False, extract the i8 values directly. + + Returns + ------- + i8 1d array + """ + from pandas import Index + from pandas.core.arrays import PeriodArray + + if lib.is_scalar(other) and isna(other): + return iNaT + elif isinstance(other, (PeriodArray, ABCIndexClass)): + # convert tz if needed + if getattr(other, 'tz', None) is not None: + if to_utc: + other = other.tz_convert('UTC') + else: + other = other.tz_localize(None) + else: + try: + return np.array(other, copy=False).view('i8') + except TypeError: + # period array cannot be coerced to int + other = Index(other) + return other.asi8 diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 4d3caaacca1c16..050442c530314a 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -156,7 +156,9 @@ def wrapper(self, other): return compat.set_function_name(wrapper, opname, cls) -class DatetimeArrayMixin(dtl.DatetimeLikeArrayMixin): +class DatetimeArrayMixin(dtl.DatetimeLikeArrayMixin, + dtl.TimelikeOps, + dtl.DatelikeOps): """ Assumes that subclass __new__/__init__ defines: tz diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 856a01e41ce13e..6a7ce7033efa0f 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -129,7 +129,7 @@ def method(self, other): return method -class TimedeltaArrayMixin(dtl.DatetimeLikeArrayMixin): +class TimedeltaArrayMixin(dtl.DatetimeLikeArrayMixin, dtl.TimelikeOps): _typ = "timedeltaarray" __array_priority__ = 1000 diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 5e25efe77d8b95..0dedd8fe1cf4b3 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -7,8 +7,6 @@ import numpy as np from pandas._libs import NaT, iNaT, lib -from pandas._libs.tslibs.timestamps import RoundTo, round_nsint64 -import pandas.compat as compat from pandas.compat.numpy import function as nv from pandas.errors import AbstractMethodError from pandas.util._decorators import Appender, cache_readonly @@ -19,11 +17,10 @@ is_integer, is_integer_dtype, is_list_like, is_object_dtype, is_period_dtype, is_scalar, is_string_dtype) from pandas.core.dtypes.generic import ABCIndex, ABCIndexClass, ABCSeries -from pandas.core.dtypes.missing import isna from pandas.core import algorithms, ops -from pandas.core.arrays import PeriodArray -from pandas.core.arrays.datetimelike import DatetimeLikeArrayMixin +from pandas.core.arrays.datetimelike import ( + DatetimeLikeArrayMixin, _ensure_datetimelike_to_i8) import pandas.core.indexes.base as ibase from pandas.core.indexes.base import Index, _index_shared_docs from pandas.core.tools.timedeltas import to_timedelta @@ -33,188 +30,6 @@ _index_doc_kwargs = dict(ibase._index_doc_kwargs) -class DatelikeOps(object): - """ - Common ops for DatetimeIndex/PeriodIndex, but not TimedeltaIndex. - """ - - def strftime(self, date_format): - return Index(self.format(date_format=date_format), - dtype=compat.text_type) - strftime.__doc__ = """ - Convert to Index using specified date_format. - - Return an Index of formatted strings specified by date_format, which - supports the same string format as the python standard library. Details - of the string format can be found in `python string format doc <{0}>`__ - - Parameters - ---------- - date_format : str - Date format string (e.g. "%Y-%m-%d"). - - Returns - ------- - Index - Index of formatted strings - - See Also - -------- - to_datetime : Convert the given argument to datetime. - DatetimeIndex.normalize : Return DatetimeIndex with times to midnight. - DatetimeIndex.round : Round the DatetimeIndex to the specified freq. - DatetimeIndex.floor : Floor the DatetimeIndex to the specified freq. - - Examples - -------- - >>> rng = pd.date_range(pd.Timestamp("2018-03-10 09:00"), - ... periods=3, freq='s') - >>> rng.strftime('%B %d, %Y, %r') - Index(['March 10, 2018, 09:00:00 AM', 'March 10, 2018, 09:00:01 AM', - 'March 10, 2018, 09:00:02 AM'], - dtype='object') - """.format("https://docs.python.org/3/library/datetime.html" - "#strftime-and-strptime-behavior") - - -class TimelikeOps(object): - """ - Common ops for TimedeltaIndex/DatetimeIndex, but not PeriodIndex. - """ - - _round_doc = ( - """ - Perform {op} operation on the data to the specified `freq`. - - Parameters - ---------- - freq : str or Offset - The frequency level to {op} the index to. Must be a fixed - frequency like 'S' (second) not 'ME' (month end). See - :ref:`frequency aliases ` for - a list of possible `freq` values. - ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise' - Only relevant for DatetimeIndex: - - - 'infer' will attempt to infer fall dst-transition hours based on - order - - bool-ndarray where True signifies a DST time, False designates - a non-DST time (note that this flag is only applicable for - ambiguous times) - - 'NaT' will return NaT where there are ambiguous times - - 'raise' will raise an AmbiguousTimeError if there are ambiguous - times - - .. versionadded:: 0.24.0 - nonexistent : 'shift', 'NaT', default 'raise' - A nonexistent time does not exist in a particular timezone - where clocks moved forward due to DST. - - - 'shift' will shift the nonexistent time forward to the closest - existing time - - 'NaT' will return NaT where there are nonexistent times - - 'raise' will raise an NonExistentTimeError if there are - nonexistent times - - .. versionadded:: 0.24.0 - - Returns - ------- - DatetimeIndex, TimedeltaIndex, or Series - Index of the same type for a DatetimeIndex or TimedeltaIndex, - or a Series with the same index for a Series. - - Raises - ------ - ValueError if the `freq` cannot be converted. - - Examples - -------- - **DatetimeIndex** - - >>> rng = pd.date_range('1/1/2018 11:59:00', periods=3, freq='min') - >>> rng - DatetimeIndex(['2018-01-01 11:59:00', '2018-01-01 12:00:00', - '2018-01-01 12:01:00'], - dtype='datetime64[ns]', freq='T') - """) - - _round_example = ( - """>>> rng.round('H') - DatetimeIndex(['2018-01-01 12:00:00', '2018-01-01 12:00:00', - '2018-01-01 12:00:00'], - dtype='datetime64[ns]', freq=None) - - **Series** - - >>> pd.Series(rng).dt.round("H") - 0 2018-01-01 12:00:00 - 1 2018-01-01 12:00:00 - 2 2018-01-01 12:00:00 - dtype: datetime64[ns] - """) - - _floor_example = ( - """>>> rng.floor('H') - DatetimeIndex(['2018-01-01 11:00:00', '2018-01-01 12:00:00', - '2018-01-01 12:00:00'], - dtype='datetime64[ns]', freq=None) - - **Series** - - >>> pd.Series(rng).dt.floor("H") - 0 2018-01-01 11:00:00 - 1 2018-01-01 12:00:00 - 2 2018-01-01 12:00:00 - dtype: datetime64[ns] - """ - ) - - _ceil_example = ( - """>>> rng.ceil('H') - DatetimeIndex(['2018-01-01 12:00:00', '2018-01-01 12:00:00', - '2018-01-01 13:00:00'], - dtype='datetime64[ns]', freq=None) - - **Series** - - >>> pd.Series(rng).dt.ceil("H") - 0 2018-01-01 12:00:00 - 1 2018-01-01 12:00:00 - 2 2018-01-01 13:00:00 - dtype: datetime64[ns] - """ - ) - - def _round(self, freq, mode, ambiguous, nonexistent): - # round the local times - values = _ensure_datetimelike_to_i8(self) - result = round_nsint64(values, mode, freq) - result = self._maybe_mask_results(result, fill_value=NaT) - - attribs = self._get_attributes_dict() - attribs['freq'] = None - if 'tz' in attribs: - attribs['tz'] = None - return self._ensure_localized( - self._shallow_copy(result, **attribs), ambiguous, nonexistent - ) - - @Appender((_round_doc + _round_example).format(op="round")) - def round(self, freq, ambiguous='raise', nonexistent='raise'): - return self._round( - freq, RoundTo.NEAREST_HALF_EVEN, ambiguous, nonexistent - ) - - @Appender((_round_doc + _floor_example).format(op="floor")) - def floor(self, freq, ambiguous='raise', nonexistent='raise'): - return self._round(freq, RoundTo.MINUS_INFTY, ambiguous, nonexistent) - - @Appender((_round_doc + _ceil_example).format(op="ceil")) - def ceil(self, freq, ambiguous='raise', nonexistent='raise'): - return self._round(freq, RoundTo.PLUS_INFTY, ambiguous, nonexistent) - - class DatetimeIndexOpsMixin(DatetimeLikeArrayMixin): """ common ops mixin to support a unified interface datetimelike Index """ @@ -754,39 +569,6 @@ def _time_shift(self, periods, freq=None): return result -def _ensure_datetimelike_to_i8(other, to_utc=False): - """ - Helper for coercing an input scalar or array to i8. - - Parameters - ---------- - other : 1d array - to_utc : bool, default False - If True, convert the values to UTC before extracting the i8 values - If False, extract the i8 values directly. - - Returns - ------- - i8 1d array - """ - if is_scalar(other) and isna(other): - return iNaT - elif isinstance(other, (PeriodArray, ABCIndexClass)): - # convert tz if needed - if getattr(other, 'tz', None) is not None: - if to_utc: - other = other.tz_convert('UTC') - else: - other = other.tz_localize(None) - else: - try: - return np.array(other, copy=False).view('i8') - except TypeError: - # period array cannot be coerced to int - other = Index(other) - return other.asi8 - - def wrap_arithmetic_op(self, other, result): if result is NotImplemented: return NotImplemented diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 01901d022da322..8f36096d128c29 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -31,8 +31,7 @@ import pandas.core.common as com from pandas.core.indexes.base import Index, _index_shared_docs from pandas.core.indexes.datetimelike import ( - DatelikeOps, DatetimeIndexOpsMixin, TimelikeOps, wrap_array_method, - wrap_field_accessor) + DatetimeIndexOpsMixin, wrap_array_method, wrap_field_accessor) from pandas.core.indexes.numeric import Int64Index from pandas.core.ops import get_op_result_name import pandas.core.tools.datetimes as tools @@ -62,8 +61,7 @@ def _new_DatetimeIndex(cls, d): return result -class DatetimeIndex(DatetimeArray, DatelikeOps, TimelikeOps, - DatetimeIndexOpsMixin, Int64Index): +class DatetimeIndex(DatetimeArray, DatetimeIndexOpsMixin, Int64Index): """ Immutable ndarray of datetime64 data, represented internally as int64, and which can be boxed to Timestamp objects that are subclasses of datetime and diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 56df454bddf1c1..3cdefb02ef8b3a 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -26,7 +26,7 @@ import pandas.core.indexes.base as ibase from pandas.core.indexes.base import _index_shared_docs, ensure_index from pandas.core.indexes.datetimelike import ( - DatelikeOps, DatetimeIndexOpsMixin, wrap_arithmetic_op) + DatetimeIndexOpsMixin, wrap_arithmetic_op) from pandas.core.indexes.datetimes import DatetimeIndex, Index, Int64Index from pandas.core.missing import isna from pandas.core.ops import get_op_result_name @@ -83,7 +83,7 @@ def _delegate_method(self, name, *args, **kwargs): if x not in {"asfreq", "to_timestamp"}], typ="method", overwrite=True) -class PeriodIndex(DatelikeOps, DatetimeIndexOpsMixin, +class PeriodIndex(DatetimeIndexOpsMixin, Int64Index, PeriodDelegateMixin): """ Immutable ndarray holding ordinal values indicating regular periods in diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index ed4e43df8f41a3..e33d61d29d302e 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -22,7 +22,7 @@ import pandas.core.common as com from pandas.core.indexes.base import Index, _index_shared_docs from pandas.core.indexes.datetimelike import ( - DatetimeIndexOpsMixin, TimelikeOps, wrap_arithmetic_op, wrap_array_method, + DatetimeIndexOpsMixin, wrap_arithmetic_op, wrap_array_method, wrap_field_accessor) from pandas.core.indexes.numeric import Int64Index from pandas.core.ops import get_op_result_name @@ -31,8 +31,7 @@ from pandas.tseries.frequencies import to_offset -class TimedeltaIndex(TimedeltaArray, DatetimeIndexOpsMixin, - TimelikeOps, Int64Index): +class TimedeltaIndex(TimedeltaArray, DatetimeIndexOpsMixin, Int64Index): """ Immutable ndarray of timedelta64 data, represented internally as int64, and which can be boxed to timedelta objects