From 65d2edb3377aaf3976bf28f106958f0debfae76f Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Sep 2018 16:07:49 -0700 Subject: [PATCH 01/39] Adjust timeseries.rst --- doc/source/timeseries.rst | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/doc/source/timeseries.rst b/doc/source/timeseries.rst index 85b0abe421eb2..0a319d4fb0bbd 100644 --- a/doc/source/timeseries.rst +++ b/doc/source/timeseries.rst @@ -414,7 +414,7 @@ In practice this becomes very cumbersome because we often need a very long index with a large number of timestamps. If we need timestamps on a regular frequency, we can use the :func:`date_range` and :func:`bdate_range` functions to create a ``DatetimeIndex``. The default frequency for ``date_range`` is a -**day** while the default for ``bdate_range`` is a **business day**: +**calendar day** while the default for ``bdate_range`` is a **business day**: .. ipython:: python @@ -937,26 +937,6 @@ time information included in the operation. hour.apply(ts).normalize() hour.apply(pd.Timestamp("2014-01-01 23:30")).normalize() -.. _timeseries.dayvscalendarday: - -Day vs. CalendarDay -~~~~~~~~~~~~~~~~~~~ - -:class:`Day` (``'D'``) is a timedelta-like offset that respects absolute time -arithmetic and is an alias for 24 :class:`Hour`. This offset is the default -argument to many pandas time related function like :func:`date_range` and :func:`timedelta_range`. - -:class:`CalendarDay` (``'CD'``) is a relativedelta-like offset that respects -calendar time arithmetic. :class:`CalendarDay` is useful preserving calendar day -semantics with date times with have day light savings transitions, i.e. :class:`CalendarDay` -will preserve the hour before the day light savings transition. - -.. ipython:: python - - ts = pd.Timestamp('2016-10-30 00:00:00', tz='Europe/Helsinki') - ts + pd.offsets.Day(1) - ts + pd.offsets.CalendarDay(1) - Parametric Offsets ~~~~~~~~~~~~~~~~~~ @@ -1248,8 +1228,7 @@ frequencies. We will refer to these aliases as *offset aliases*. "B", "business day frequency" "C", "custom business day frequency" - "D", "day frequency" - "CD", "calendar day frequency" + "D", "calendar day frequency" "W", "weekly frequency" "M", "month end frequency" "SM", "semi-month end frequency (15th and end of month)" From 29a8a76e3edd14886bff6c0aeb9c8e9bbd3f77ad Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Sep 2018 16:14:30 -0700 Subject: [PATCH 02/39] Replace CalendarDay with Day; remove Day Tick --- pandas/tseries/offsets.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 0a9931c46bbd5..9406c4392c531 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -41,7 +41,7 @@ 'LastWeekOfMonth', 'FY5253Quarter', 'FY5253', 'Week', 'WeekOfMonth', 'Easter', 'Hour', 'Minute', 'Second', 'Milli', 'Micro', 'Nano', - 'DateOffset', 'CalendarDay'] + 'DateOffset'] # convert to/from datetime/timestamp to allow invalid Timestamp ranges to # pass thru @@ -2124,14 +2124,15 @@ def onOffset(self, dt): return date(dt.year, dt.month, dt.day) == easter(dt.year) -class CalendarDay(SingleConstructorOffset): +class Day(SingleConstructorOffset): """ - Calendar day offset. Respects calendar arithmetic as opposed to Day which - respects absolute time. + Day offset representing a calendar day. Respects calendar day arithmetic + in the midst of daylight savings time transitions; therefore, Day does not + necessarily equate to 24 hours. """ _adjust_dst = True _inc = Timedelta(days=1) - _prefix = 'CD' + _prefix = 'D' _attributes = frozenset(['n', 'normalize']) def __init__(self, n=1, normalize=False): @@ -2140,11 +2141,11 @@ def __init__(self, n=1, normalize=False): @apply_wraps def apply(self, other): """ - Apply scalar arithmetic with CalendarDay offset. Incoming datetime + Apply scalar arithmetic with Day offset. Incoming datetime objects can be tz-aware or naive. """ if type(other) == type(self): - # Add other CalendarDays + # Add other Days return type(self)(self.n + other.n, normalize=self.normalize) tzinfo = getattr(other, 'tzinfo', None) if tzinfo is not None: @@ -2160,12 +2161,12 @@ def apply(self, other): return as_timestamp(other) except TypeError: raise TypeError("Cannot perform arithmetic between {other} and " - "CalendarDay".format(other=type(other))) + "Day".format(other=type(other))) @apply_index_wraps def apply_index(self, i): """ - Apply the CalendarDay offset to a DatetimeIndex. Incoming DatetimeIndex + Apply the Day offset to a DatetimeIndex. Incoming DatetimeIndex objects are assumed to be tz_naive """ return i + self.n * self._inc @@ -2302,11 +2303,6 @@ def _delta_to_tick(delta): return Nano(nanos) -class Day(Tick): - _inc = Timedelta(days=1) - _prefix = 'D' - - class Hour(Tick): _inc = Timedelta(hours=1) _prefix = 'H' @@ -2455,5 +2451,4 @@ def generate_range(start=None, end=None, periods=None, WeekOfMonth, # 'WOM' FY5253, FY5253Quarter, - CalendarDay # 'CD' ]} From 16de58de1fa114f1d868b7c6bf88daa228f9584a Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Sep 2018 16:16:25 -0700 Subject: [PATCH 03/39] Revert whatsnew entry --- doc/source/whatsnew/v0.24.0.txt | 40 --------------------------------- 1 file changed, 40 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 481c31d2410a9..04d024ed9f9b2 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -293,46 +293,6 @@ that the dates have been converted to UTC pd.to_datetime(["2015-11-18 15:30:00+05:30", "2015-11-18 16:30:00+06:30"], utc=True) -.. _whatsnew_0240.api_breaking.calendarday: - -CalendarDay Offset -^^^^^^^^^^^^^^^^^^ - -:class:`Day` and associated frequency alias ``'D'`` were documented to represent -a calendar day; however, arithmetic and operations with :class:`Day` sometimes -respected absolute time instead (i.e. ``Day(n)`` and acted identically to ``Timedelta(days=n)``). - -*Previous Behavior*: - -.. code-block:: ipython - - - In [2]: ts = pd.Timestamp('2016-10-30 00:00:00', tz='Europe/Helsinki') - - # Respects calendar arithmetic - In [3]: pd.date_range(start=ts, freq='D', periods=3) - Out[3]: - DatetimeIndex(['2016-10-30 00:00:00+03:00', '2016-10-31 00:00:00+02:00', - '2016-11-01 00:00:00+02:00'], - dtype='datetime64[ns, Europe/Helsinki]', freq='D') - - # Respects absolute arithmetic - In [4]: ts + pd.tseries.frequencies.to_offset('D') - Out[4]: Timestamp('2016-10-30 23:00:00+0200', tz='Europe/Helsinki') - -:class:`CalendarDay` and associated frequency alias ``'CD'`` are now available -and respect calendar day arithmetic while :class:`Day` and frequency alias ``'D'`` -will now respect absolute time (:issue:`22274`, :issue:`20596`, :issue:`16980`, :issue:`8774`) -See the :ref:`documentation here ` for more information. - -Addition with :class:`CalendarDay` across a daylight savings time transition: - -.. ipython:: python - - ts = pd.Timestamp('2016-10-30 00:00:00', tz='Europe/Helsinki') - ts + pd.offsets.Day(1) - ts + pd.offsets.CalendarDay(1) - .. _whatsnew_0240.api_breaking.period_end_time: Time values in ``dt.end_time`` and ``to_timestamp(how='end')`` From 2314e9bd0ac106669b14f593d22680c1cad41920 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Sep 2018 16:19:48 -0700 Subject: [PATCH 04/39] Undo some docstrings --- pandas/core/indexes/datetimes.py | 2 +- pandas/core/indexes/interval.py | 2 +- pandas/core/indexes/period.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 9b00f21668bf5..8488880fe0e45 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -1589,7 +1589,7 @@ def date_range(start=None, end=None, periods=None, freq=None, tz=None, Right bound for generating dates. periods : integer, optional Number of periods to generate. - freq : str or DateOffset, default 'D' + freq : str or DateOffset, default 'D' (calendar daily) Frequency strings can have multiples, e.g. '5H'. See :ref:`here ` for a list of frequency aliases. diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 4b125580bd7e0..364eea8fb8a3a 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -1052,7 +1052,7 @@ def interval_range(start=None, end=None, periods=None, freq=None, freq : numeric, string, or DateOffset, default None The length of each interval. Must be consistent with the type of start and end, e.g. 2 for numeric, or '5H' for datetime-like. Default is 1 - for numeric and 'D' for datetime-like. + for numeric and 'D' (calendar daily) for datetime-like. name : string, default None Name of the resulting IntervalIndex closed : {'left', 'right', 'both', 'neither'}, default 'right' diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 0f86e18103e3c..f75970256a642 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -838,7 +838,7 @@ def period_range(start=None, end=None, periods=None, freq='D', name=None): Right bound for generating periods periods : integer, default None Number of periods to generate - freq : string or DateOffset, default 'D' + freq : string or DateOffset, default 'D' (calendar daily) Frequency alias name : string, default None Name of the resulting PeriodIndex From edebf1a3b4daf066644fae9f0d62804378c76cb2 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Sep 2018 16:41:02 -0700 Subject: [PATCH 05/39] Add a nanos property to allow Day to interact with timedeltas during depreciation --- pandas/tseries/offsets.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 9406c4392c531..06093acd0eeaa 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2171,6 +2171,19 @@ def apply_index(self, i): """ return i + self.n * self._inc + @property + def nanos(self): + """ + Return the number of nanoseconds of this offset. + + Note: This is a patch to allow Timedeltas to interact with Day offsets + during the depreciation cycle. Remove this method after full + deprecation. + + .. deprecated:: 0.24.0 + """ + return delta_to_nanoseconds(self.n * self._inc) + # --------------------------------------------------------------------- # Ticks From ee409cec537ffc9d901480b727c639d1439f5f32 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Sep 2018 16:45:42 -0700 Subject: [PATCH 06/39] Fix some date_range tests --- pandas/tests/indexes/datetimes/test_date_range.py | 13 ++++++------- pandas/tseries/offsets.py | 1 + 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pandas/tests/indexes/datetimes/test_date_range.py b/pandas/tests/indexes/datetimes/test_date_range.py index e0caf671fc390..f5f2a3ea7eb01 100644 --- a/pandas/tests/indexes/datetimes/test_date_range.py +++ b/pandas/tests/indexes/datetimes/test_date_range.py @@ -343,18 +343,18 @@ def test_range_tz_pytz(self): Timestamp(datetime(2013, 11, 6), tz='US/Eastern')] ]) def test_range_tz_dst_straddle_pytz(self, start, end): - dr = date_range(start, end, freq='CD') + dr = date_range(start, end, freq='D') assert dr[0] == start assert dr[-1] == end assert np.all(dr.hour == 0) - dr = date_range(start, end, freq='CD', tz='US/Eastern') + dr = date_range(start, end, freq='D', tz='US/Eastern') assert dr[0] == start assert dr[-1] == end assert np.all(dr.hour == 0) dr = date_range(start.replace(tzinfo=None), end.replace( - tzinfo=None), freq='CD', tz='US/Eastern') + tzinfo=None), freq='D', tz='US/Eastern') assert dr[0] == start assert dr[-1] == end assert np.all(dr.hour == 0) @@ -578,9 +578,9 @@ def test_mismatching_tz_raises_err(self, start, end): with pytest.raises(TypeError): pd.DatetimeIndex(start, end, freq=BDay()) - def test_CalendarDay_range_with_dst_crossing(self): + def test_date_range_with_dst_crossing(self): # GH 20596 - result = date_range('2018-10-23', '2018-11-06', freq='7CD', + result = date_range('2018-10-23', '2018-11-06', freq='7D', tz='Europe/Paris') expected = date_range('2018-10-23', '2018-11-06', freq=pd.DateOffset(days=7), tz='Europe/Paris') @@ -780,8 +780,7 @@ def test_cdaterange_weekmask_and_holidays(self): holidays=['2013-05-01']) @pytest.mark.parametrize('freq', [freq for freq in prefix_mapping - if freq.startswith('C') - and freq != 'CD']) # CalendarDay + if freq.startswith('C')]) def test_all_custom_freq(self, freq): # should not raise bdate_range(START, END, freq=freq, weekmask='Mon Wed Fri', diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 06093acd0eeaa..847fafdfb9cd4 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2182,6 +2182,7 @@ def nanos(self): .. deprecated:: 0.24.0 """ + # TODO: Maybe add the deprecation warning here? return delta_to_nanoseconds(self.n * self._inc) From 3d1dd5bc3d654fdd9f3b71354e044ae2881dda2d Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Sep 2018 16:48:32 -0700 Subject: [PATCH 07/39] Fix some timezone tests --- pandas/tests/indexes/datetimes/test_timezones.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/indexes/datetimes/test_timezones.py b/pandas/tests/indexes/datetimes/test_timezones.py index dc01f7ccbd496..05a73fb6cbf95 100644 --- a/pandas/tests/indexes/datetimes/test_timezones.py +++ b/pandas/tests/indexes/datetimes/test_timezones.py @@ -431,7 +431,7 @@ def test_dti_tz_localize_utc_conversion(self, tz): @pytest.mark.parametrize('idx', [ date_range(start='2014-01-01', end='2014-12-31', freq='M'), - date_range(start='2014-01-01', end='2014-12-31', freq='CD'), + date_range(start='2014-01-01', end='2014-12-31', freq='D'), date_range(start='2014-01-01', end='2014-03-01', freq='H'), date_range(start='2014-08-01', end='2014-10-31', freq='T') ]) @@ -1034,7 +1034,7 @@ def test_date_range_span_dst_transition(self, tzstr): dr = date_range('2012-11-02', periods=10, tz=tzstr) result = dr.hour - expected = Index([0, 0, 0, 23, 23, 23, 23, 23, 23, 23]) + expected = Index([0] * 10) tm.assert_index_equal(result, expected) @pytest.mark.parametrize('tzstr', ['US/Eastern', 'dateutil/US/Eastern']) From 3ab973bcb35f0bea4b9f354709fad2bc9a353416 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Sep 2018 18:12:28 -0700 Subject: [PATCH 08/39] Revert "Revert whatsnew entry" This reverts commit 16de58de1fa114f1d868b7c6bf88daa228f9584a. --- doc/source/whatsnew/v0.24.0.txt | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 04d024ed9f9b2..481c31d2410a9 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -293,6 +293,46 @@ that the dates have been converted to UTC pd.to_datetime(["2015-11-18 15:30:00+05:30", "2015-11-18 16:30:00+06:30"], utc=True) +.. _whatsnew_0240.api_breaking.calendarday: + +CalendarDay Offset +^^^^^^^^^^^^^^^^^^ + +:class:`Day` and associated frequency alias ``'D'`` were documented to represent +a calendar day; however, arithmetic and operations with :class:`Day` sometimes +respected absolute time instead (i.e. ``Day(n)`` and acted identically to ``Timedelta(days=n)``). + +*Previous Behavior*: + +.. code-block:: ipython + + + In [2]: ts = pd.Timestamp('2016-10-30 00:00:00', tz='Europe/Helsinki') + + # Respects calendar arithmetic + In [3]: pd.date_range(start=ts, freq='D', periods=3) + Out[3]: + DatetimeIndex(['2016-10-30 00:00:00+03:00', '2016-10-31 00:00:00+02:00', + '2016-11-01 00:00:00+02:00'], + dtype='datetime64[ns, Europe/Helsinki]', freq='D') + + # Respects absolute arithmetic + In [4]: ts + pd.tseries.frequencies.to_offset('D') + Out[4]: Timestamp('2016-10-30 23:00:00+0200', tz='Europe/Helsinki') + +:class:`CalendarDay` and associated frequency alias ``'CD'`` are now available +and respect calendar day arithmetic while :class:`Day` and frequency alias ``'D'`` +will now respect absolute time (:issue:`22274`, :issue:`20596`, :issue:`16980`, :issue:`8774`) +See the :ref:`documentation here ` for more information. + +Addition with :class:`CalendarDay` across a daylight savings time transition: + +.. ipython:: python + + ts = pd.Timestamp('2016-10-30 00:00:00', tz='Europe/Helsinki') + ts + pd.offsets.Day(1) + ts + pd.offsets.CalendarDay(1) + .. _whatsnew_0240.api_breaking.period_end_time: Time values in ``dt.end_time`` and ``to_timestamp(how='end')`` From 2cd8d4bae31b908091ca6492d5ac3bc8bbcff6a5 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Sep 2018 18:31:38 -0700 Subject: [PATCH 09/39] Adjust language in whatsnew --- doc/source/whatsnew/v0.24.0.txt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 481c31d2410a9..ac46c93594eec 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -295,8 +295,8 @@ that the dates have been converted to UTC .. _whatsnew_0240.api_breaking.calendarday: -CalendarDay Offset -^^^^^^^^^^^^^^^^^^ +Frequency alisas ``'D'`` and Day offset representing calendar day +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :class:`Day` and associated frequency alias ``'D'`` were documented to represent a calendar day; however, arithmetic and operations with :class:`Day` sometimes @@ -320,10 +320,9 @@ respected absolute time instead (i.e. ``Day(n)`` and acted identically to ``Time In [4]: ts + pd.tseries.frequencies.to_offset('D') Out[4]: Timestamp('2016-10-30 23:00:00+0200', tz='Europe/Helsinki') -:class:`CalendarDay` and associated frequency alias ``'CD'`` are now available -and respect calendar day arithmetic while :class:`Day` and frequency alias ``'D'`` -will now respect absolute time (:issue:`22274`, :issue:`20596`, :issue:`16980`, :issue:`8774`) -See the :ref:`documentation here ` for more information. +:class:`Day` and associated frequency alias ``'D'`` will always respect +calendar day arithmetic +(:issue:`22864`, :issue:`20596`, :issue:`16980`, :issue:`8774`) Addition with :class:`CalendarDay` across a daylight savings time transition: @@ -331,7 +330,6 @@ Addition with :class:`CalendarDay` across a daylight savings time transition: ts = pd.Timestamp('2016-10-30 00:00:00', tz='Europe/Helsinki') ts + pd.offsets.Day(1) - ts + pd.offsets.CalendarDay(1) .. _whatsnew_0240.api_breaking.period_end_time: From 58f1944bdfa783bcd0b37b00f7e8bb5420da22c1 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Sep 2018 18:36:21 -0700 Subject: [PATCH 10/39] fix some initial timedelta_tests --- pandas/tests/indexes/timedeltas/test_timedelta_range.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pandas/tests/indexes/timedeltas/test_timedelta_range.py b/pandas/tests/indexes/timedeltas/test_timedelta_range.py index 1d10e63363cc8..3256238c62d4f 100644 --- a/pandas/tests/indexes/timedeltas/test_timedelta_range.py +++ b/pandas/tests/indexes/timedeltas/test_timedelta_range.py @@ -18,7 +18,8 @@ def test_timedelta_range(self): result = timedelta_range('0 days', '10 days', freq='D') tm.assert_index_equal(result, expected) - expected = to_timedelta(np.arange(5), unit='D') + Second(2) + Day() + expected = to_timedelta(np.arange(5), unit='D') + expected = expected + Second(2) + pd.Timedelta(days=1) result = timedelta_range('1 days, 00:00:02', '5 days, 00:00:02', freq='D') tm.assert_index_equal(result, expected) @@ -48,9 +49,7 @@ def test_timedelta_range(self): result = df.loc['0s':, :] tm.assert_frame_equal(expected, result) - with pytest.raises(ValueError): - # GH 22274: CalendarDay is a relative time measurement - timedelta_range('1day', freq='CD', periods=2) + timedelta_range('1day', freq='D', periods=2) @pytest.mark.parametrize('periods, freq', [ (3, '2D'), (5, 'D'), (6, '19H12T'), (7, '16H'), (9, '12H')]) From 2ec6010866b0830fba7a87dcde808437a99830ec Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Sep 2018 18:53:04 -0700 Subject: [PATCH 11/39] Remove CalendarDay from some tests --- pandas/tests/series/test_timezones.py | 2 +- pandas/tests/test_resample.py | 8 +++--- pandas/tests/tseries/offsets/test_offsets.py | 29 +++++++++----------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/pandas/tests/series/test_timezones.py b/pandas/tests/series/test_timezones.py index 472b2c5644fa5..d59e7fd445f17 100644 --- a/pandas/tests/series/test_timezones.py +++ b/pandas/tests/series/test_timezones.py @@ -302,7 +302,7 @@ def test_getitem_pydatetime_tz(self, tzstr): def test_series_truncate_datetimeindex_tz(self): # GH 9243 - idx = date_range('4/1/2005', '4/30/2005', freq='CD', tz='US/Pacific') + idx = date_range('4/1/2005', '4/30/2005', freq='D', tz='US/Pacific') s = Series(range(len(idx)), index=idx) result = s.truncate(datetime(2005, 4, 2), datetime(2005, 4, 4)) expected = Series([1, 2, 3], index=idx[1:4]) diff --git a/pandas/tests/test_resample.py b/pandas/tests/test_resample.py index ccd2461d1512e..6644ccaeb8191 100644 --- a/pandas/tests/test_resample.py +++ b/pandas/tests/test_resample.py @@ -2040,7 +2040,7 @@ def test_resample_dst_anchor(self): # 5172 dti = DatetimeIndex([datetime(2012, 11, 4, 23)], tz='US/Eastern') df = DataFrame([5], index=dti) - assert_frame_equal(df.resample(rule='CD').sum(), + assert_frame_equal(df.resample(rule='D').sum(), DataFrame([5], index=df.index.normalize())) df.resample(rule='MS').sum() assert_frame_equal( @@ -2094,14 +2094,14 @@ def test_resample_dst_anchor(self): df_daily = df['10/26/2013':'10/29/2013'] assert_frame_equal( - df_daily.resample("CD").agg({"a": "min", "b": "max", "c": "count"}) + df_daily.resample("D").agg({"a": "min", "b": "max", "c": "count"}) [["a", "b", "c"]], DataFrame({"a": [1248, 1296, 1346, 1394], "b": [1295, 1345, 1393, 1441], "c": [48, 50, 48, 48]}, index=date_range('10/26/2013', '10/29/2013', - freq='CD', tz='Europe/Paris')), - 'CD Frequency') + freq='D', tz='Europe/Paris')), + 'D Frequency') def test_downsample_across_dst(self): # GH 8531 diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index b8fabbf52159d..e0006f9060969 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -30,7 +30,7 @@ YearEnd, Day, QuarterEnd, BusinessMonthEnd, FY5253, Nano, Easter, FY5253Quarter, - LastWeekOfMonth, Tick, CalendarDay) + LastWeekOfMonth, Tick) import pandas.tseries.offsets as offsets from pandas.io.pickle import read_pickle from pandas._libs.tslibs import timezones @@ -195,7 +195,6 @@ class TestCommon(Base): # are applied to 2011/01/01 09:00 (Saturday) # used for .apply and .rollforward expecteds = {'Day': Timestamp('2011-01-02 09:00:00'), - 'CalendarDay': Timestamp('2011-01-02 09:00:00'), 'DateOffset': Timestamp('2011-01-02 09:00:00'), 'BusinessDay': Timestamp('2011-01-03 09:00:00'), 'CustomBusinessDay': Timestamp('2011-01-03 09:00:00'), @@ -364,7 +363,7 @@ def test_rollforward(self, offset_types): # result will not be changed if the target is on the offset no_changes = ['Day', 'MonthBegin', 'SemiMonthBegin', 'YearBegin', 'Week', 'Hour', 'Minute', 'Second', 'Milli', 'Micro', - 'Nano', 'DateOffset', 'CalendarDay'] + 'Nano', 'DateOffset'] for n in no_changes: expecteds[n] = Timestamp('2011/01/01 09:00') @@ -377,7 +376,6 @@ def test_rollforward(self, offset_types): norm_expected[k] = Timestamp(norm_expected[k].date()) normalized = {'Day': Timestamp('2011-01-02 00:00:00'), - 'CalendarDay': Timestamp('2011-01-02 00:00:00'), 'DateOffset': Timestamp('2011-01-02 00:00:00'), 'MonthBegin': Timestamp('2011-02-01 00:00:00'), 'SemiMonthBegin': Timestamp('2011-01-15 00:00:00'), @@ -430,7 +428,7 @@ def test_rollback(self, offset_types): # result will not be changed if the target is on the offset for n in ['Day', 'MonthBegin', 'SemiMonthBegin', 'YearBegin', 'Week', 'Hour', 'Minute', 'Second', 'Milli', 'Micro', 'Nano', - 'DateOffset', 'CalendarDay']: + 'DateOffset']: expecteds[n] = Timestamp('2011/01/01 09:00') # but be changed when normalize=True @@ -439,7 +437,6 @@ def test_rollback(self, offset_types): norm_expected[k] = Timestamp(norm_expected[k].date()) normalized = {'Day': Timestamp('2010-12-31 00:00:00'), - 'CalendarDay': Timestamp('2010-12-31 00:00:00'), 'DateOffset': Timestamp('2010-12-31 00:00:00'), 'MonthBegin': Timestamp('2010-12-01 00:00:00'), 'SemiMonthBegin': Timestamp('2010-12-15 00:00:00'), @@ -3185,16 +3182,16 @@ def test_last_week_of_month_on_offset(): assert fast == slow -class TestCalendarDay(object): +class TestDay(object): def test_add_across_dst_scalar(self): # GH 22274 ts = Timestamp('2016-10-30 00:00:00+0300', tz='Europe/Helsinki') expected = Timestamp('2016-10-31 00:00:00+0200', tz='Europe/Helsinki') - result = ts + CalendarDay(1) + result = ts + Day(1) assert result == expected - result = result - CalendarDay(1) + result = result - Day(1) assert result == ts @pytest.mark.parametrize('box', [DatetimeIndex, Series]) @@ -3204,10 +3201,10 @@ def test_add_across_dst_array(self, box): expected = Timestamp('2016-10-31 00:00:00+0200', tz='Europe/Helsinki') arr = box([ts]) expected = box([expected]) - result = arr + CalendarDay(1) + result = arr + Day(1) tm.assert_equal(result, expected) - result = result - CalendarDay(1) + result = result - Day(1) tm.assert_equal(arr, result) @pytest.mark.parametrize('arg', [ @@ -3217,7 +3214,7 @@ def test_add_across_dst_array(self, box): def test_raises_AmbiguousTimeError(self, arg): # GH 22274 with pytest.raises(pytz.AmbiguousTimeError): - arg + CalendarDay(1) + arg + Day(1) @pytest.mark.parametrize('arg', [ Timestamp("2019-03-09 02:00:00", tz='US/Pacific'), @@ -3226,7 +3223,7 @@ def test_raises_AmbiguousTimeError(self, arg): def test_raises_NonExistentTimeError(self, arg): # GH 22274 with pytest.raises(pytz.NonExistentTimeError): - arg + CalendarDay(1) + arg + Day(1) @pytest.mark.parametrize('arg, exp', [ [1, 2], @@ -3235,8 +3232,8 @@ def test_raises_NonExistentTimeError(self, arg): ]) def test_arithmetic(self, arg, exp): # GH 22274 - result = CalendarDay(1) + CalendarDay(arg) - expected = CalendarDay(exp) + result = Day(1) + Day(arg) + expected = Day(exp) assert result == expected @pytest.mark.parametrize('arg', [ @@ -3250,4 +3247,4 @@ def test_invalid_arithmetic(self, arg): # CalendarDay (relative time) cannot be added to Timedelta-like objects # (absolute time) with pytest.raises(TypeError): - CalendarDay(1) + arg + Day(1) + arg From 6fecd5788c720a32d48d4fcffdd2824fafc144f8 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Sep 2018 18:58:02 -0700 Subject: [PATCH 12/39] fix some offset tests --- pandas/tests/tseries/offsets/test_offsets.py | 3 +-- pandas/tests/tseries/offsets/test_ticks.py | 10 +--------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index e0006f9060969..dcedcbd584a8d 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -3053,7 +3053,7 @@ def test_springforward_singular(self): QuarterEnd: ['11/2/2012', '12/31/2012'], BQuarterBegin: ['11/2/2012', '12/3/2012'], BQuarterEnd: ['11/2/2012', '12/31/2012'], - Day: ['11/4/2012', '11/4/2012 23:00']}.items() + Day: ['11/4/2012', '11/5/2012']}.items() @pytest.mark.parametrize('tup', offset_classes) def test_all_offset_classes(self, tup): @@ -3238,7 +3238,6 @@ def test_arithmetic(self, arg, exp): @pytest.mark.parametrize('arg', [ timedelta(1), - Day(1), Timedelta(1), TimedeltaIndex([timedelta(1)]) ]) diff --git a/pandas/tests/tseries/offsets/test_ticks.py b/pandas/tests/tseries/offsets/test_ticks.py index 369c0971f1e9a..914d61a18ee11 100644 --- a/pandas/tests/tseries/offsets/test_ticks.py +++ b/pandas/tests/tseries/offsets/test_ticks.py @@ -10,8 +10,7 @@ from pandas import Timedelta, Timestamp from pandas.tseries import offsets -from pandas.tseries.offsets import (Day, Hour, Minute, Second, Milli, Micro, - Nano) +from pandas.tseries.offsets import Hour, Minute, Second, Milli, Micro, Nano from .common import assert_offset_equal @@ -212,13 +211,6 @@ def test_Nanosecond(): assert Micro(5) + Nano(1) == Nano(5001) -def test_Day_equals_24_Hours(): - ts = Timestamp('2016-10-30 00:00:00+0300', tz='Europe/Helsinki') - result = ts + Day(1) - expected = ts + Hour(24) - assert result == expected - - @pytest.mark.parametrize('kls, expected', [(Hour, Timedelta(hours=5)), (Minute, Timedelta(hours=2, minutes=3)), From 2e31635063095f946113750a79210227eddf5678 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Tue, 2 Oct 2018 22:19:02 -0700 Subject: [PATCH 13/39] Add back Day and make CalendarDay _Day --- pandas/tests/tseries/offsets/test_offsets.py | 22 ++-- pandas/tseries/offsets.py | 114 ++++++++++++++++--- 2 files changed, 109 insertions(+), 27 deletions(-) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index dcedcbd584a8d..a21e1d6a51032 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -30,7 +30,7 @@ YearEnd, Day, QuarterEnd, BusinessMonthEnd, FY5253, Nano, Easter, FY5253Quarter, - LastWeekOfMonth, Tick) + LastWeekOfMonth, Tick, _Day) import pandas.tseries.offsets as offsets from pandas.io.pickle import read_pickle from pandas._libs.tslibs import timezones @@ -3053,7 +3053,7 @@ def test_springforward_singular(self): QuarterEnd: ['11/2/2012', '12/31/2012'], BQuarterBegin: ['11/2/2012', '12/3/2012'], BQuarterEnd: ['11/2/2012', '12/31/2012'], - Day: ['11/4/2012', '11/5/2012']}.items() + Day: ['11/4/2012', '11/4/2012 23:00']}.items() @pytest.mark.parametrize('tup', offset_classes) def test_all_offset_classes(self, tup): @@ -3188,10 +3188,10 @@ def test_add_across_dst_scalar(self): # GH 22274 ts = Timestamp('2016-10-30 00:00:00+0300', tz='Europe/Helsinki') expected = Timestamp('2016-10-31 00:00:00+0200', tz='Europe/Helsinki') - result = ts + Day(1) + result = ts + _Day(1) assert result == expected - result = result - Day(1) + result = result - _Day(1) assert result == ts @pytest.mark.parametrize('box', [DatetimeIndex, Series]) @@ -3201,10 +3201,10 @@ def test_add_across_dst_array(self, box): expected = Timestamp('2016-10-31 00:00:00+0200', tz='Europe/Helsinki') arr = box([ts]) expected = box([expected]) - result = arr + Day(1) + result = arr + _Day(1) tm.assert_equal(result, expected) - result = result - Day(1) + result = result - _Day(1) tm.assert_equal(arr, result) @pytest.mark.parametrize('arg', [ @@ -3214,7 +3214,7 @@ def test_add_across_dst_array(self, box): def test_raises_AmbiguousTimeError(self, arg): # GH 22274 with pytest.raises(pytz.AmbiguousTimeError): - arg + Day(1) + arg + _Day(1) @pytest.mark.parametrize('arg', [ Timestamp("2019-03-09 02:00:00", tz='US/Pacific'), @@ -3223,7 +3223,7 @@ def test_raises_AmbiguousTimeError(self, arg): def test_raises_NonExistentTimeError(self, arg): # GH 22274 with pytest.raises(pytz.NonExistentTimeError): - arg + Day(1) + arg + _Day(1) @pytest.mark.parametrize('arg, exp', [ [1, 2], @@ -3232,8 +3232,8 @@ def test_raises_NonExistentTimeError(self, arg): ]) def test_arithmetic(self, arg, exp): # GH 22274 - result = Day(1) + Day(arg) - expected = Day(exp) + result = _Day(1) + _Day(arg) + expected = _Day(exp) assert result == expected @pytest.mark.parametrize('arg', [ @@ -3246,4 +3246,4 @@ def test_invalid_arithmetic(self, arg): # CalendarDay (relative time) cannot be added to Timedelta-like objects # (absolute time) with pytest.raises(TypeError): - Day(1) + arg + _Day(1) + arg diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 847fafdfb9cd4..d8bc4434a5585 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2,6 +2,7 @@ from datetime import date, datetime, timedelta import functools import operator +import warnings from pandas.compat import range from pandas import compat @@ -2124,15 +2125,18 @@ def onOffset(self, dt): return date(dt.year, dt.month, dt.day) == easter(dt.year) -class Day(SingleConstructorOffset): +class _Day(SingleConstructorOffset): """ Day offset representing a calendar day. Respects calendar day arithmetic in the midst of daylight savings time transitions; therefore, Day does not necessarily equate to 24 hours. + + Note: _Day is meant to replace Day once Day's mixed calendar and absolute + time behavior is fully depreciated. """ _adjust_dst = True _inc = Timedelta(days=1) - _prefix = 'D' + _prefix = '_D' _attributes = frozenset(['n', 'normalize']) def __init__(self, n=1, normalize=False): @@ -2171,20 +2175,6 @@ def apply_index(self, i): """ return i + self.n * self._inc - @property - def nanos(self): - """ - Return the number of nanoseconds of this offset. - - Note: This is a patch to allow Timedeltas to interact with Day offsets - during the depreciation cycle. Remove this method after full - deprecation. - - .. deprecated:: 0.24.0 - """ - # TODO: Maybe add the deprecation warning here? - return delta_to_nanoseconds(self.n * self._inc) - # --------------------------------------------------------------------- # Ticks @@ -2316,6 +2306,98 @@ def _delta_to_tick(delta): else: # pragma: no cover return Nano(nanos) +# This entire class can be removed once Day completely functions as calendar +# day (i.e. remove Day and replace _Day with Day) +class Day(Tick): + _inc = Timedelta(days=1) + _prefix = 'D' + + def __add__(self, other): + if isinstance(other, Tick): + warnings.warn("Arithmetic between {} and Day is deprecated. Day " + "will become a non-fixed offset".format(type(self))) + if type(self) == type(other): + return type(self)(self.n + other.n) + else: + return _delta_to_tick(self.delta + other.delta) + elif isinstance(other, ABCPeriod): + return other + self + try: + return self.apply(other) + except ApplyTypeError: + return NotImplemented + except OverflowError: + raise OverflowError("the add operation between {self} and {other} " + "will overflow".format(self=self, other=other)) + + def __eq__(self, other): + if isinstance(other, compat.string_types): + from pandas.tseries.frequencies import to_offset + + other = to_offset(other) + + if isinstance(other, Tick): + return self.delta == other.delta + else: + return False + + # This is identical to DateOffset.__hash__, but has to be redefined here + # for Python 3, because we've redefined __eq__. + def __hash__(self): + return hash(self._params) + + def __ne__(self, other): + if isinstance(other, compat.string_types): + from pandas.tseries.frequencies import to_offset + + other = to_offset(other) + + if isinstance(other, Tick): + return self.delta != other.delta + else: + return True + + @property + def delta(self): + return self.n * self._inc + + @property + def nanos(self): + # This is what Timedelta like operations call when 'D' is passed + warnings.warn("'D' will refer to calendar day in a future release. " + "Use '24H' instead.") + return delta_to_nanoseconds(self.delta) + + # TODO: Should Tick have its own apply_index? + def apply(self, other): + # Timestamp can handle tz and nano sec, thus no need to use apply_wraps + if isinstance(other, Timestamp): + + # GH 15126 + # in order to avoid a recursive + # call of __add__ and __radd__ if there is + # an exception, when we call using the + operator, + # we directly call the known method + result = other.__add__(self) + if result == NotImplemented: + raise OverflowError + if other.tz is not None: + warnings.warn("Day arithmetic will respect calendar day in a" + "future release") + return result + elif isinstance(other, (datetime, np.datetime64, date)): + return as_timestamp(other) + self + + if isinstance(other, timedelta): + warnings.warn("Day arithmetic with timedelta is deprecated. Use" + "Hour(24) instead.") + return other + self.delta + elif isinstance(other, type(self)): + return type(self)(self.n + other.n) + + raise ApplyTypeError('Unhandled type: {type_str}' + .format(type_str=type(other).__name__)) + class Hour(Tick): _inc = Timedelta(hours=1) From 92170c84b54aa73d803ea37a9824f0d8624a4842 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Wed, 3 Oct 2018 15:06:34 -0700 Subject: [PATCH 14/39] Undo resample semantics --- pandas/core/arrays/datetimes.py | 7 ++++--- pandas/tests/indexes/timedeltas/test_timedelta_range.py | 2 +- pandas/tseries/offsets.py | 5 +++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index a0a9b57712249..deaa92d078ddd 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -32,7 +32,7 @@ from pandas.core import ops from pandas.tseries.frequencies import to_offset -from pandas.tseries.offsets import Tick, generate_range +from pandas.tseries.offsets import Tick, Day, generate_range from pandas.core.arrays import datetimelike as dtl @@ -1347,9 +1347,10 @@ def _maybe_localize_point(ts, is_none, is_not_none, freq, tz): ts : Timestamp """ # Make sure start and end are timezone localized if: - # 1) freq = a Timedelta-like frequency (Tick) + # 1) freq = a Timedelta-like frequency (Tick) and freq != Day + # TODO: remove freq != Day condition once Day fully acts as calendar day # 2) freq = None i.e. generating a linspaced range - if isinstance(freq, Tick) or freq is None: + if (isinstance(freq, Tick) and not isinstance(freq, Day)) or freq is None: localize_args = {'tz': tz, 'ambiguous': False} else: localize_args = {'tz': None} diff --git a/pandas/tests/indexes/timedeltas/test_timedelta_range.py b/pandas/tests/indexes/timedeltas/test_timedelta_range.py index 3256238c62d4f..d03f057a04a84 100644 --- a/pandas/tests/indexes/timedeltas/test_timedelta_range.py +++ b/pandas/tests/indexes/timedeltas/test_timedelta_range.py @@ -2,7 +2,7 @@ import numpy as np import pandas as pd import pandas.util.testing as tm -from pandas.tseries.offsets import Day, Second +from pandas.tseries.offsets import Second from pandas import to_timedelta, timedelta_range diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index d8bc4434a5585..27678b051ebb6 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2306,9 +2306,10 @@ def _delta_to_tick(delta): else: # pragma: no cover return Nano(nanos) -# This entire class can be removed once Day completely functions as calendar -# day (i.e. remove Day and replace _Day with Day) + class Day(Tick): + # This entire class can be removed once Day completely functions as + # calendar day (i.e. remove Day and replace _Day with Day) _inc = Timedelta(days=1) _prefix = 'D' From 2940c82b9176de9e1c6704496c94b38dd66a536a Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Sat, 6 Oct 2018 00:36:38 -0700 Subject: [PATCH 15/39] Fix additional tests --- pandas/core/resample.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 70a8deb33b7f2..55bc162bec2e2 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -1522,7 +1522,8 @@ def _take_new_index(obj, indexer, new_index, axis=0): def _get_range_edges(first, last, offset, closed='left', base=0): - if isinstance(offset, Tick): + + if isinstance(offset, Tick) and not isinstance(offset, Day): is_day = isinstance(offset, Day) day_nanos = delta_to_nanoseconds(timedelta(1)) From 5772a9d9b92f8a820c1d4e1be7c5f2b53f9b3a40 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Sun, 7 Oct 2018 00:20:08 -0700 Subject: [PATCH 16/39] Supress warnings for now --- pandas/tseries/offsets.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 27678b051ebb6..edf5881fef555 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2315,8 +2315,8 @@ class Day(Tick): def __add__(self, other): if isinstance(other, Tick): - warnings.warn("Arithmetic between {} and Day is deprecated. Day " - "will become a non-fixed offset".format(type(self))) + # warnings.warn("Arithmetic between {} and Day is deprecated. Day " + # "will become a non-fixed offset".format(type(self))) if type(self) == type(other): return type(self)(self.n + other.n) else: @@ -2365,8 +2365,8 @@ def delta(self): @property def nanos(self): # This is what Timedelta like operations call when 'D' is passed - warnings.warn("'D' will refer to calendar day in a future release. " - "Use '24H' instead.") + #warnings.warn("'D' will refer to calendar day in a future release. " + # "Use '24H' instead.") return delta_to_nanoseconds(self.delta) # TODO: Should Tick have its own apply_index? @@ -2382,9 +2382,9 @@ def apply(self, other): result = other.__add__(self) if result == NotImplemented: raise OverflowError - if other.tz is not None: - warnings.warn("Day arithmetic will respect calendar day in a" - "future release") + #if other.tz is not None: + # warnings.warn("Day arithmetic will respect calendar day in a" + # "future release") return result elif isinstance(other, (datetime, np.datetime64, date)): return as_timestamp(other) + self From 89f5b44194b1cdae68505662b9fe1d11c8266ce0 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Sun, 7 Oct 2018 00:31:52 -0700 Subject: [PATCH 17/39] Comment out warnings and try another patch --- pandas/core/arrays/datetimes.py | 11 ++++++++--- pandas/core/resample.py | 2 +- pandas/tseries/offsets.py | 20 +++++++++++++------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index deaa92d078ddd..c93f1f3d9c252 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -32,7 +32,7 @@ from pandas.core import ops from pandas.tseries.frequencies import to_offset -from pandas.tseries.offsets import Tick, Day, generate_range +from pandas.tseries.offsets import Tick, Day, generate_range, _Day from pandas.core.arrays import datetimelike as dtl @@ -222,6 +222,9 @@ def _generate_range(cls, start, end, periods, freq, tz=None, raise ValueError('Of the four parameters: start, end, periods, ' 'and freq, exactly three must be specified') freq = to_offset(freq) + # TODO: Remove once Day offsets fully becomes a calendar offset + if isinstance(freq, Day): + freq = _Day(freq.n) if start is not None: start = Timestamp(start) @@ -291,6 +294,9 @@ def _generate_range(cls, start, end, periods, freq, tz=None, if not right_closed and len(index) and index[-1] == end: index = index[:-1] + # TODO: Remove once Day offsets fully becomes a calendar offset + if isinstance(freq, _Day): + freq = Day(freq.n) return cls._simple_new(index.values, freq=freq, tz=tz) @classmethod @@ -1348,9 +1354,8 @@ def _maybe_localize_point(ts, is_none, is_not_none, freq, tz): """ # Make sure start and end are timezone localized if: # 1) freq = a Timedelta-like frequency (Tick) and freq != Day - # TODO: remove freq != Day condition once Day fully acts as calendar day # 2) freq = None i.e. generating a linspaced range - if (isinstance(freq, Tick) and not isinstance(freq, Day)) or freq is None: + if isinstance(freq, Tick) or freq is None: localize_args = {'tz': tz, 'ambiguous': False} else: localize_args = {'tz': None} diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 55bc162bec2e2..6bf783239f4af 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -1523,7 +1523,7 @@ def _take_new_index(obj, indexer, new_index, axis=0): def _get_range_edges(first, last, offset, closed='left', base=0): - if isinstance(offset, Tick) and not isinstance(offset, Day): + if isinstance(offset, Tick): is_day = isinstance(offset, Day) day_nanos = delta_to_nanoseconds(timedelta(1)) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index edf5881fef555..0a14ba0e081bf 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2315,8 +2315,10 @@ class Day(Tick): def __add__(self, other): if isinstance(other, Tick): - # warnings.warn("Arithmetic between {} and Day is deprecated. Day " - # "will become a non-fixed offset".format(type(self))) + """ + warnings.warn("Arithmetic between {} and Day is deprecated. Day " + "will become a non-fixed offset".format(type(self))) + """ if type(self) == type(other): return type(self)(self.n + other.n) else: @@ -2365,8 +2367,10 @@ def delta(self): @property def nanos(self): # This is what Timedelta like operations call when 'D' is passed - #warnings.warn("'D' will refer to calendar day in a future release. " - # "Use '24H' instead.") + """ + warnings.warn("'D' will refer to calendar day in a future release. " + "Use '24H' instead.") + """ return delta_to_nanoseconds(self.delta) # TODO: Should Tick have its own apply_index? @@ -2382,9 +2386,11 @@ def apply(self, other): result = other.__add__(self) if result == NotImplemented: raise OverflowError - #if other.tz is not None: - # warnings.warn("Day arithmetic will respect calendar day in a" - # "future release") + """ + if other.tz is not None: + warnings.warn("Day arithmetic will respect calendar day in a" + "future release") + """ return result elif isinstance(other, (datetime, np.datetime64, date)): return as_timestamp(other) + self From 16c76c705e94203c2e14270aef1d4f9f64ec28cf Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Mon, 8 Oct 2018 13:45:17 -0700 Subject: [PATCH 18/39] fix docstring --- pandas/core/arrays/datetimes.py | 2 +- pandas/core/resample.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index c93f1f3d9c252..50da5c1bb1098 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -1353,7 +1353,7 @@ def _maybe_localize_point(ts, is_none, is_not_none, freq, tz): ts : Timestamp """ # Make sure start and end are timezone localized if: - # 1) freq = a Timedelta-like frequency (Tick) and freq != Day + # 1) freq = a Timedelta-like frequency (Tick) # 2) freq = None i.e. generating a linspaced range if isinstance(freq, Tick) or freq is None: localize_args = {'tz': tz, 'ambiguous': False} diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 6bf783239f4af..b76cee6d5c1a2 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -17,7 +17,7 @@ from pandas.core.indexes.datetimes import DatetimeIndex, date_range from pandas.core.indexes.timedeltas import TimedeltaIndex from pandas.tseries.offsets import (DateOffset, Tick, Day, - delta_to_nanoseconds, Nano) + delta_to_nanoseconds, Nano, _Day) from pandas.core.indexes.period import PeriodIndex from pandas.errors import AbstractMethodError import pandas.core.algorithms as algos From 9ab0be9b4d98fcc6c91459a668884cfc32d3ea68 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Mon, 8 Oct 2018 16:04:59 -0700 Subject: [PATCH 19/39] Missing import --- pandas/core/arrays/datetimes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 781ca89f72031..6ddf29f8c5461 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -32,7 +32,7 @@ from pandas.core import ops from pandas.tseries.frequencies import to_offset, get_period_alias -from pandas.tseries.offsets import Tick, generate_range, _Day +from pandas.tseries.offsets import Tick, generate_range, Day, _Day from pandas.core.arrays import datetimelike as dtl From 93132d0deb449ee332e78e75fb6664cd28478683 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Sun, 21 Oct 2018 19:53:39 -0700 Subject: [PATCH 20/39] paramerize test --- .../tests/indexes/datetimes/test_indexing.py | 71 +++++++++---------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/pandas/tests/indexes/datetimes/test_indexing.py b/pandas/tests/indexes/datetimes/test_indexing.py index 601a7b13e370a..6cc75d24b013c 100644 --- a/pandas/tests/indexes/datetimes/test_indexing.py +++ b/pandas/tests/indexes/datetimes/test_indexing.py @@ -131,47 +131,46 @@ def test_where_tz(self): class TestTake(object): - def test_take(self): - # GH#10295 - idx1 = pd.date_range('2011-01-01', '2011-01-31', freq='D', name='idx') - idx2 = pd.date_range('2011-01-01', '2011-01-31', freq='D', - tz='Asia/Tokyo', name='idx') - - for idx in [idx1, idx2]: - result = idx.take([0]) - assert result == Timestamp('2011-01-01', tz=idx.tz) - result = idx.take([0, 1, 2]) - expected = pd.date_range('2011-01-01', '2011-01-03', freq='D', - tz=idx.tz, name='idx') - tm.assert_index_equal(result, expected) - assert result.freq == expected.freq + @pytest.mark.parametrize('tz', [None, 'Asia/Tokyo']) + def test_take(self, tz): + # GH#10295 + idx = pd.date_range('2011-01-01', '2011-01-31', freq='D', name='idx', + tz=tz) + result = idx.take([0]) + assert result == Timestamp('2011-01-01', tz=idx.tz) + + result = idx.take([0, 1, 2]) + expected = pd.date_range('2011-01-01', '2011-01-03', freq='D', + tz=idx.tz, name='idx') + tm.assert_index_equal(result, expected) + assert result.freq == expected.freq - result = idx.take([0, 2, 4]) - expected = pd.date_range('2011-01-01', '2011-01-05', freq='2D', - tz=idx.tz, name='idx') - tm.assert_index_equal(result, expected) - assert result.freq == expected.freq + result = idx.take([0, 2, 4]) + expected = pd.date_range('2011-01-01', '2011-01-05', freq='2D', + tz=idx.tz, name='idx') + tm.assert_index_equal(result, expected) + assert result.freq == expected.freq - result = idx.take([7, 4, 1]) - expected = pd.date_range('2011-01-08', '2011-01-02', freq='-3D', - tz=idx.tz, name='idx') - tm.assert_index_equal(result, expected) - assert result.freq == expected.freq + result = idx.take([7, 4, 1]) + expected = pd.date_range('2011-01-08', '2011-01-02', freq='-3D', + tz=idx.tz, name='idx') + tm.assert_index_equal(result, expected) + assert result.freq == expected.freq - result = idx.take([3, 2, 5]) - expected = DatetimeIndex(['2011-01-04', '2011-01-03', - '2011-01-06'], - freq=None, tz=idx.tz, name='idx') - tm.assert_index_equal(result, expected) - assert result.freq is None + result = idx.take([3, 2, 5]) + expected = DatetimeIndex(['2011-01-04', '2011-01-03', + '2011-01-06'], + freq=None, tz=idx.tz, name='idx') + tm.assert_index_equal(result, expected) + assert result.freq is None - result = idx.take([-3, 2, 5]) - expected = DatetimeIndex(['2011-01-29', '2011-01-03', - '2011-01-06'], - freq=None, tz=idx.tz, name='idx') - tm.assert_index_equal(result, expected) - assert result.freq is None + result = idx.take([-3, 2, 5]) + expected = DatetimeIndex(['2011-01-29', '2011-01-03', + '2011-01-06'], + freq=None, tz=idx.tz, name='idx') + tm.assert_index_equal(result, expected) + assert result.freq is None def test_take_invalid_kwargs(self): idx = pd.date_range('2011-01-01', '2011-01-31', freq='D', name='idx') From a69478e41ece6fdd7362e335753fd86839e04f89 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Sun, 21 Oct 2018 22:16:47 -0700 Subject: [PATCH 21/39] Patch up dst edge case --- pandas/core/resample.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pandas/core/resample.py b/pandas/core/resample.py index b76cee6d5c1a2..24d7527c10a6c 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -1522,15 +1522,23 @@ def _take_new_index(obj, indexer, new_index, axis=0): def _get_range_edges(first, last, offset, closed='left', base=0): - if isinstance(offset, Tick): is_day = isinstance(offset, Day) day_nanos = delta_to_nanoseconds(timedelta(1)) # #1165 if (is_day and day_nanos % offset.nanos == 0) or not is_day: - return _adjust_dates_anchored(first, last, offset, - closed=closed, base=base) + first, last = _adjust_dates_anchored(first, last, offset, + closed=closed, base=base) + if offset == 'D' and first.tz is not None: + # TODO: Remove this statement when 'D' fully means calendar day + # and just return first and last. + # We need to make Tick 'D' flexible to DST (23H, 24H, or 25H) + # _adjust_dates_anchored assumes 'D' means 24H, so ensure + # first and last snap to midnight. + first = first.normalize() + last = last.normalize() + return first, last else: first = first.normalize() From 88aa09509c2b4ae0da7378b6294b68d755cf18ef Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Mon, 22 Oct 2018 16:13:13 -0700 Subject: [PATCH 22/39] Add PR number --- doc/source/whatsnew/v0.24.0.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 3225b8d70cd79..bc150d0f009d1 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -449,7 +449,7 @@ respected absolute time instead (i.e. ``Day(n)`` and acted identically to ``Time :class:`Day` and associated frequency alias ``'D'`` will always respect calendar day arithmetic -(:issue:`22864`, :issue:`20596`, :issue:`16980`, :issue:`8774`) +(:issue:`22867`, :issue:`22864`, :issue:`20596`, :issue:`16980`, :issue:`8774`) Addition with :class:`CalendarDay` across a daylight savings time transition: From 6c73025071658130573eae5e623e4831ced1e81f Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Tue, 23 Oct 2018 18:12:08 -0700 Subject: [PATCH 23/39] Lint --- pandas/core/resample.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 24d7527c10a6c..02eae675f8a2a 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -17,7 +17,7 @@ from pandas.core.indexes.datetimes import DatetimeIndex, date_range from pandas.core.indexes.timedeltas import TimedeltaIndex from pandas.tseries.offsets import (DateOffset, Tick, Day, - delta_to_nanoseconds, Nano, _Day) + delta_to_nanoseconds, Nano) from pandas.core.indexes.period import PeriodIndex from pandas.errors import AbstractMethodError import pandas.core.algorithms as algos From 78ad1b5782f535ccb651bb8ee37cfc96af467b91 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Tue, 23 Oct 2018 18:30:53 -0700 Subject: [PATCH 24/39] uncomment warnings --- pandas/tseries/offsets.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index acd5535cdf80a..0a2003c3a5663 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2313,10 +2313,8 @@ class Day(Tick): def __add__(self, other): if isinstance(other, Tick): - """ warnings.warn("Arithmetic between {} and Day is deprecated. Day " "will become a non-fixed offset".format(type(self))) - """ if type(self) == type(other): return type(self)(self.n + other.n) else: @@ -2365,10 +2363,8 @@ def delta(self): @property def nanos(self): # This is what Timedelta like operations call when 'D' is passed - """ warnings.warn("'D' will refer to calendar day in a future release. " "Use '24H' instead.") - """ return delta_to_nanoseconds(self.delta) # TODO: Should Tick have its own apply_index? @@ -2384,18 +2380,16 @@ def apply(self, other): result = other.__add__(self) if result == NotImplemented: raise OverflowError - """ if other.tz is not None: warnings.warn("Day arithmetic will respect calendar day in a" - "future release") - """ + " future release") return result elif isinstance(other, (datetime, np.datetime64, date)): return as_timestamp(other) + self if isinstance(other, timedelta): warnings.warn("Day arithmetic with timedelta is deprecated. Use" - "Hour(24) instead.") + " Hour(24) instead.") return other + self.delta elif isinstance(other, type(self)): return type(self)(self.n + other.n) From ca8f5c6570391566fd15f877f9a9bad6e3f304d8 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Wed, 24 Oct 2018 11:03:58 -0700 Subject: [PATCH 25/39] Dont duplicate code, add statement to check for warning --- pandas/core/arrays/datetimes.py | 4 +- pandas/core/resample.py | 3 +- pandas/tseries/offsets.py | 112 +++++++------------------------- 3 files changed, 25 insertions(+), 94 deletions(-) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 9651b911e6374..eae6b264c0103 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -232,7 +232,7 @@ def _generate_range(cls, start, end, periods, freq, tz=None, raise ValueError('Of the four parameters: start, end, periods, ' 'and freq, exactly three must be specified') freq = to_offset(freq) - # TODO: Remove once Day offsets fully becomes a calendar offset + # TODO: Remove when _Day replaces Day if isinstance(freq, Day): freq = _Day(freq.n) @@ -299,7 +299,7 @@ def _generate_range(cls, start, end, periods, freq, tz=None, if not right_closed and len(index) and index[-1] == end: index = index[:-1] - # TODO: Remove once Day offsets fully becomes a calendar offset + # TODO: Remove when _Day replaces Day if isinstance(freq, _Day): freq = Day(freq.n) return cls._simple_new(index.values, freq=freq, tz=tz) diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 02eae675f8a2a..0d0645f91774b 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -1530,9 +1530,8 @@ def _get_range_edges(first, last, offset, closed='left', base=0): if (is_day and day_nanos % offset.nanos == 0) or not is_day: first, last = _adjust_dates_anchored(first, last, offset, closed=closed, base=base) + # TODO: Remove when _Day replaces Day and just return first, last if offset == 'D' and first.tz is not None: - # TODO: Remove this statement when 'D' fully means calendar day - # and just return first and last. # We need to make Tick 'D' flexible to DST (23H, 24H, or 25H) # _adjust_dates_anchored assumes 'D' means 24H, so ensure # first and last snap to midnight. diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 0a2003c3a5663..c83276e5d57b4 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2129,11 +2129,11 @@ class _Day(SingleConstructorOffset): in the midst of daylight savings time transitions; therefore, Day does not necessarily equate to 24 hours. - Note: _Day is meant to replace Day once Day's mixed calendar and absolute - time behavior is fully depreciated. + TODO: Replace _Day with Day """ _adjust_dst = True _inc = Timedelta(days=1) + # TODO: Replace '_D' with 'D' when _Day replaces Day _prefix = '_D' _attributes = frozenset(['n', 'normalize']) @@ -2205,6 +2205,12 @@ def __init__(self, n=1, normalize=False): def __add__(self, other): if isinstance(other, Tick): + # TODO: Remove when _Day replaces Day + if self._prefix == 'D': + warnings.warn( + "Arithmetic between {} and Day is deprecated. Day " + "will become a non-fixed offset".format(type(self)) + ) if type(self) == type(other): return type(self)(self.n + other.n) else: @@ -2252,6 +2258,11 @@ def delta(self): @property def nanos(self): + # TODO: Remove when _Day replaces Day + # This is what Timedelta like operations call when 'D' is passed + if self._prefix == 'D': + warnings.warn("'D' will refer to calendar day in a future release." + " Use '24H' instead.") return delta_to_nanoseconds(self.delta) # TODO: Should Tick have its own apply_index? @@ -2267,11 +2278,19 @@ def apply(self, other): result = other.__add__(self) if result == NotImplemented: raise OverflowError + # TODO: Remove when _Day replaces Day + if other.tz is not None and self._prefix == 'D': + warnings.warn("Day arithmetic will respect calendar day in a" + " future release") return result elif isinstance(other, (datetime, np.datetime64, date)): return as_timestamp(other) + self if isinstance(other, timedelta): + # TODO: Remove when _Day replaces Day + if self._prefix == 'D': + warnings.warn("Day arithmetic with timedelta is deprecated. " + "Use Hour(24) instead.") return other + self.delta elif isinstance(other, type(self)): return type(self)(self.n + other.n) @@ -2306,97 +2325,10 @@ def _delta_to_tick(delta): class Day(Tick): - # This entire class can be removed once Day completely functions as - # calendar day (i.e. remove Day and replace _Day with Day) + # TODO: Remove when _Day replaces Day _inc = Timedelta(days=1) _prefix = 'D' - def __add__(self, other): - if isinstance(other, Tick): - warnings.warn("Arithmetic between {} and Day is deprecated. Day " - "will become a non-fixed offset".format(type(self))) - if type(self) == type(other): - return type(self)(self.n + other.n) - else: - return _delta_to_tick(self.delta + other.delta) - elif isinstance(other, ABCPeriod): - return other + self - try: - return self.apply(other) - except ApplyTypeError: - return NotImplemented - except OverflowError: - raise OverflowError("the add operation between {self} and {other} " - "will overflow".format(self=self, other=other)) - - def __eq__(self, other): - if isinstance(other, compat.string_types): - from pandas.tseries.frequencies import to_offset - - other = to_offset(other) - - if isinstance(other, Tick): - return self.delta == other.delta - else: - return False - - # This is identical to DateOffset.__hash__, but has to be redefined here - # for Python 3, because we've redefined __eq__. - def __hash__(self): - return hash(self._params) - - def __ne__(self, other): - if isinstance(other, compat.string_types): - from pandas.tseries.frequencies import to_offset - - other = to_offset(other) - - if isinstance(other, Tick): - return self.delta != other.delta - else: - return True - - @property - def delta(self): - return self.n * self._inc - - @property - def nanos(self): - # This is what Timedelta like operations call when 'D' is passed - warnings.warn("'D' will refer to calendar day in a future release. " - "Use '24H' instead.") - return delta_to_nanoseconds(self.delta) - - # TODO: Should Tick have its own apply_index? - def apply(self, other): - # Timestamp can handle tz and nano sec, thus no need to use apply_wraps - if isinstance(other, Timestamp): - - # GH 15126 - # in order to avoid a recursive - # call of __add__ and __radd__ if there is - # an exception, when we call using the + operator, - # we directly call the known method - result = other.__add__(self) - if result == NotImplemented: - raise OverflowError - if other.tz is not None: - warnings.warn("Day arithmetic will respect calendar day in a" - " future release") - return result - elif isinstance(other, (datetime, np.datetime64, date)): - return as_timestamp(other) + self - - if isinstance(other, timedelta): - warnings.warn("Day arithmetic with timedelta is deprecated. Use" - " Hour(24) instead.") - return other + self.delta - elif isinstance(other, type(self)): - return type(self)(self.n + other.n) - - raise ApplyTypeError('Unhandled type: {type_str}' - .format(type_str=type(other).__name__)) - class Hour(Tick): _inc = Timedelta(hours=1) From 17e0794f8d8374644051c9cbbc61231015590ab0 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Wed, 24 Oct 2018 11:24:52 -0700 Subject: [PATCH 26/39] Fix whatsnew --- doc/source/whatsnew/v0.24.0.txt | 39 +-------------------------------- 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index e11124dd2ffef..63c1a36b14d06 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -421,44 +421,6 @@ that the dates have been converted to UTC pd.to_datetime(["2015-11-18 15:30:00+05:30", "2015-11-18 16:30:00+06:30"], utc=True) -.. _whatsnew_0240.api_breaking.calendarday: - -Frequency alisas ``'D'`` and Day offset representing calendar day -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:class:`Day` and associated frequency alias ``'D'`` were documented to represent -a calendar day; however, arithmetic and operations with :class:`Day` sometimes -respected absolute time instead (i.e. ``Day(n)`` and acted identically to ``Timedelta(days=n)``). - -*Previous Behavior*: - -.. code-block:: ipython - - - In [2]: ts = pd.Timestamp('2016-10-30 00:00:00', tz='Europe/Helsinki') - - # Respects calendar arithmetic - In [3]: pd.date_range(start=ts, freq='D', periods=3) - Out[3]: - DatetimeIndex(['2016-10-30 00:00:00+03:00', '2016-10-31 00:00:00+02:00', - '2016-11-01 00:00:00+02:00'], - dtype='datetime64[ns, Europe/Helsinki]', freq='D') - - # Respects absolute arithmetic - In [4]: ts + pd.tseries.frequencies.to_offset('D') - Out[4]: Timestamp('2016-10-30 23:00:00+0200', tz='Europe/Helsinki') - -:class:`Day` and associated frequency alias ``'D'`` will always respect -calendar day arithmetic -(:issue:`22867`, :issue:`22864`, :issue:`20596`, :issue:`16980`, :issue:`8774`) - -Addition with :class:`CalendarDay` across a daylight savings time transition: - -.. ipython:: python - - ts = pd.Timestamp('2016-10-30 00:00:00', tz='Europe/Helsinki') - ts + pd.offsets.Day(1) - .. _whatsnew_0240.api_breaking.period_end_time: Time values in ``dt.end_time`` and ``to_timestamp(how='end')`` @@ -908,6 +870,7 @@ Deprecations - :meth:`FrozenNDArray.searchsorted` has deprecated the ``v`` parameter in favor of ``value`` (:issue:`14645`) - :func:`DatetimeIndex.shift` and :func:`PeriodIndex.shift` now accept ``periods`` argument instead of ``n`` for consistency with :func:`Index.shift` and :func:`Series.shift`. Using ``n`` throws a deprecation warning (:issue:`22458`, :issue:`22912`) - The ``fastpath`` keyword of the different Index constructors is deprecated (:issue:`23110`). +- Operations where the offset alias ``'D'`` or :class:`Day` acts as a fixed, absolute duration of 24 hours are deprecated. This includes :class:`Timestamp` arithmetic and all operations with :class:`Timedelta`. :class:`Day` will always represent a calendar day in a future version (:issue:`22864`) .. _whatsnew_0240.prior_deprecations: From 90cf660ac4d4b97bc7c25b774ff903e79ef54ba3 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 8 Nov 2018 15:24:38 -0800 Subject: [PATCH 27/39] remove all warnings so far: --- pandas/tseries/offsets.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 3a8cb35711e84..a58d48bd6556d 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2,7 +2,6 @@ from datetime import date, datetime, timedelta import functools import operator -import warnings from pandas.compat import range from pandas import compat @@ -2230,12 +2229,6 @@ def __init__(self, n=1, normalize=False): def __add__(self, other): if isinstance(other, Tick): - # TODO: Remove when _Day replaces Day - if self._prefix == 'D': - warnings.warn( - "Arithmetic between {} and Day is deprecated. Day " - "will become a non-fixed offset".format(type(self)) - ) if type(self) == type(other): return type(self)(self.n + other.n) else: @@ -2283,11 +2276,6 @@ def delta(self): @property def nanos(self): - # TODO: Remove when _Day replaces Day - # This is what Timedelta like operations call when 'D' is passed - if self._prefix == 'D': - warnings.warn("'D' will refer to calendar day in a future release." - " Use '24H' instead.") return delta_to_nanoseconds(self.delta) # TODO: Should Tick have its own apply_index? @@ -2303,19 +2291,11 @@ def apply(self, other): result = other.__add__(self) if result == NotImplemented: raise OverflowError - # TODO: Remove when _Day replaces Day - if other.tz is not None and self._prefix == 'D': - warnings.warn("Day arithmetic will respect calendar day in a" - " future release") return result elif isinstance(other, (datetime, np.datetime64, date)): return as_timestamp(other) + self if isinstance(other, timedelta): - # TODO: Remove when _Day replaces Day - if self._prefix == 'D': - warnings.warn("Day arithmetic with timedelta is deprecated. " - "Use Hour(24) instead.") return other + self.delta elif isinstance(other, type(self)): return type(self)(self.n + other.n) From 394efba0ae021cc715de34fb24d7292278ae33b2 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Sat, 10 Nov 2018 14:07:30 -0800 Subject: [PATCH 28/39] Move freq replacement to when needed --- pandas/core/arrays/datetimes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 6055ed3a38852..745616cb3b40f 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -254,9 +254,6 @@ def _generate_range(cls, start, end, periods, freq, tz=None, raise ValueError('Of the four parameters: start, end, periods, ' 'and freq, exactly three must be specified') freq = to_offset(freq) - # TODO: Remove when _Day replaces Day - if isinstance(freq, Day): - freq = _Day(freq.n) if start is not None: start = Timestamp(start) @@ -293,6 +290,9 @@ def _generate_range(cls, start, end, periods, freq, tz=None, end, end.tz, start.tz, freq, tz ) if freq is not None: + # TODO: Remove when _Day replaces Day + if isinstance(freq, Day) and tz is not None: + freq = _Day(freq.n) # TODO: consider re-implementing _cached_range; GH#17914 index = _generate_regular_range(cls, start, end, periods, freq) From 12063c317989876a3b488b6516efc96293ad92d9 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Wed, 21 Nov 2018 12:05:07 -0800 Subject: [PATCH 29/39] Undo bad merge --- pandas/core/arrays/datetimes.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 0a60abdb69357..3d6a51da2959a 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -22,19 +22,12 @@ from pandas.core.dtypes.missing import isna from pandas.core import ops -<<<<<<< HEAD - -from pandas.tseries.frequencies import to_offset, get_period_alias -from pandas.tseries.offsets import Tick, generate_range, Day, _Day - -======= from pandas.core.algorithms import checked_add_with_arr ->>>>>>> upstream/master from pandas.core.arrays import datetimelike as dtl import pandas.core.common as com from pandas.tseries.frequencies import get_period_alias, to_offset -from pandas.tseries.offsets import Tick, generate_range +from pandas.tseries.offsets import _Day, Day, Tick, generate_range _midnight = time(0, 0) From cf07aed6a9bfc5c3e593c604c267f4c036f75e52 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Wed, 21 Nov 2018 19:30:48 -0800 Subject: [PATCH 30/39] fix test --- pandas/_libs/tslibs/conversion.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/conversion.pyx b/pandas/_libs/tslibs/conversion.pyx index 28b68b9ee1925..81670dd213127 100644 --- a/pandas/_libs/tslibs/conversion.pyx +++ b/pandas/_libs/tslibs/conversion.pyx @@ -602,7 +602,7 @@ cpdef inline datetime localize_pydatetime(datetime dt, object tz): return dt elif not PyDateTime_CheckExact(dt): # i.e. is a Timestamp - return dt.tz_localize(tz) + return dt.tz_localize(tz, nonexistent='shift') elif tz == 'UTC' or tz is UTC: return UTC.localize(dt) try: From c9dc4b4a854c93a1a24bcdfc42cf8601a48c18bf Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 22 Nov 2018 10:07:12 -0800 Subject: [PATCH 31/39] isort and revert fix --- pandas/_libs/tslibs/conversion.pyx | 2 +- pandas/core/arrays/datetimes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/_libs/tslibs/conversion.pyx b/pandas/_libs/tslibs/conversion.pyx index 81670dd213127..28b68b9ee1925 100644 --- a/pandas/_libs/tslibs/conversion.pyx +++ b/pandas/_libs/tslibs/conversion.pyx @@ -602,7 +602,7 @@ cpdef inline datetime localize_pydatetime(datetime dt, object tz): return dt elif not PyDateTime_CheckExact(dt): # i.e. is a Timestamp - return dt.tz_localize(tz, nonexistent='shift') + return dt.tz_localize(tz) elif tz == 'UTC' or tz is UTC: return UTC.localize(dt) try: diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 3d6a51da2959a..27b65211af9c4 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -27,7 +27,7 @@ import pandas.core.common as com from pandas.tseries.frequencies import get_period_alias, to_offset -from pandas.tseries.offsets import _Day, Day, Tick, generate_range +from pandas.tseries.offsets import Day, Tick, _Day, generate_range _midnight = time(0, 0) From 30caab294275c9f7efa991624f80dfe0d7c0cb1a Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Fri, 23 Nov 2018 11:24:30 -0800 Subject: [PATCH 32/39] xfail resample nonexistent test --- pandas/tests/test_resample.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pandas/tests/test_resample.py b/pandas/tests/test_resample.py index 4137f403f4e5f..2e3ac90ed5c16 100644 --- a/pandas/tests/test_resample.py +++ b/pandas/tests/test_resample.py @@ -2507,6 +2507,8 @@ def test_with_local_timezone_dateutil(self): expected = Series(1, index=expected_index) assert_series_equal(result, expected) + @pytest.mark.xfail(reason='Day as calendar day will raise on ' + 'NonExistentTimeError') def test_resample_nonexistent_time_bin_edge(self): # GH 19375 index = date_range('2017-03-12', '2017-03-12 1:45:00', freq='15T') From 953fabf47299053d70d6a2920030f457928174df Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Sun, 2 Dec 2018 19:03:15 -0800 Subject: [PATCH 33/39] xfail resample test --- pandas/tests/resample/test_period_index.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pandas/tests/resample/test_period_index.py b/pandas/tests/resample/test_period_index.py index 0b3e67ca0525a..6f900ddf39f0e 100644 --- a/pandas/tests/resample/test_period_index.py +++ b/pandas/tests/resample/test_period_index.py @@ -314,6 +314,8 @@ def test_with_local_timezone_dateutil(self): expected = Series(1, index=expected_index) assert_series_equal(result, expected) + @pytest.mark.xfail(reason='Day as calendar day will raise on ' + 'NonExistentTimeError') def test_resample_nonexistent_time_bin_edge(self): # GH 19375 index = date_range('2017-03-12', '2017-03-12 1:45:00', freq='15T') From 0289c2e4896d2fa437cf788d2463bd2a6dfdfd82 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Sun, 2 Dec 2018 19:52:00 -0800 Subject: [PATCH 34/39] remove 'CD' inplace of 'D' --- pandas/tests/resample/test_datetime_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/resample/test_datetime_index.py b/pandas/tests/resample/test_datetime_index.py index b287eb468cd94..dd9d52abc3bac 100644 --- a/pandas/tests/resample/test_datetime_index.py +++ b/pandas/tests/resample/test_datetime_index.py @@ -1286,7 +1286,7 @@ def test_resample_dst_anchor(self): # 5172 dti = DatetimeIndex([datetime(2012, 11, 4, 23)], tz='US/Eastern') df = DataFrame([5], index=dti) - assert_frame_equal(df.resample(rule='CD').sum(), + assert_frame_equal(df.resample(rule='D').sum(), DataFrame([5], index=df.index.normalize())) df.resample(rule='MS').sum() assert_frame_equal( From 386d633164861a069a934bce4ac26875150b9df0 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 6 Dec 2018 23:29:13 -0800 Subject: [PATCH 35/39] Remove CD in test --- pandas/tests/resample/test_datetime_index.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/tests/resample/test_datetime_index.py b/pandas/tests/resample/test_datetime_index.py index dd9d52abc3bac..1eaf45b63e60a 100644 --- a/pandas/tests/resample/test_datetime_index.py +++ b/pandas/tests/resample/test_datetime_index.py @@ -1340,14 +1340,14 @@ def test_resample_dst_anchor(self): df_daily = df['10/26/2013':'10/29/2013'] assert_frame_equal( - df_daily.resample("CD").agg({"a": "min", "b": "max", "c": "count"}) + df_daily.resample("D").agg({"a": "min", "b": "max", "c": "count"}) [["a", "b", "c"]], DataFrame({"a": [1248, 1296, 1346, 1394], "b": [1295, 1345, 1393, 1441], "c": [48, 50, 48, 48]}, index=date_range('10/26/2013', '10/29/2013', - freq='CD', tz='Europe/Paris')), - 'CD Frequency') + freq='D', tz='Europe/Paris')), + 'D Frequency') def test_downsample_across_dst(self): # GH 8531 From c5885d8c3c86b9108fb0e5b5f7370e348bf904d5 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Fri, 7 Dec 2018 11:02:50 -0800 Subject: [PATCH 36/39] isort --- pandas/tests/tseries/offsets/test_offsets.py | 9 +++++---- pandas/tests/tseries/offsets/test_ticks.py | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 75b9e1808a797..55dde6310eb22 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -24,11 +24,12 @@ from pandas.tseries.holiday import USFederalHolidayCalendar import pandas.tseries.offsets as offsets from pandas.tseries.offsets import ( - _Day, FY5253, BDay, BMonthBegin, BMonthEnd, BQuarterBegin, BQuarterEnd, - BusinessHour, BYearBegin, BYearEnd, CBMonthBegin, CBMonthEnd, - CDay, CustomBusinessHour, DateOffset, Day, Easter, FY5253Quarter, + FY5253, BDay, BMonthBegin, BMonthEnd, BQuarterBegin, BQuarterEnd, + BusinessHour, BYearBegin, BYearEnd, CBMonthBegin, CBMonthEnd, CDay, + CustomBusinessHour, DateOffset, Day, Easter, FY5253Quarter, LastWeekOfMonth, MonthBegin, MonthEnd, Nano, QuarterBegin, QuarterEnd, - SemiMonthBegin, SemiMonthEnd, Tick, Week, WeekOfMonth, YearBegin, YearEnd) + SemiMonthBegin, SemiMonthEnd, Tick, Week, WeekOfMonth, YearBegin, YearEnd, + _Day) from .common import assert_offset_equal, assert_onOffset diff --git a/pandas/tests/tseries/offsets/test_ticks.py b/pandas/tests/tseries/offsets/test_ticks.py index d9bd8347dc0da..a1940241b4c56 100644 --- a/pandas/tests/tseries/offsets/test_ticks.py +++ b/pandas/tests/tseries/offsets/test_ticks.py @@ -11,8 +11,7 @@ from pandas import Timedelta, Timestamp from pandas.tseries import offsets -from pandas.tseries.offsets import ( - Hour, Micro, Milli, Minute, Nano, Second) +from pandas.tseries.offsets import Hour, Micro, Milli, Minute, Nano, Second from .common import assert_offset_equal From 6a387979dd8a457a5336b975d94e257a13cdd752 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Wed, 12 Dec 2018 22:38:58 -0800 Subject: [PATCH 37/39] Deprecate Tick arithmetic with Day --- pandas/tseries/offsets.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index a1b02aa925075..c33ff0e544ff1 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2,6 +2,7 @@ from datetime import date, datetime, timedelta import functools import operator +import warnings from dateutil.easter import easter import numpy as np @@ -2306,6 +2307,9 @@ def __init__(self, n=1, normalize=False): def __add__(self, other): if isinstance(other, Tick): + if self._prefix == 'D' or other._prefix == 'D': + warnings.warn("Arithmetic with Day is deprecated.", + DeprecationWarning, stacklevel=2) if type(self) == type(other): return type(self)(self.n + other.n) else: From a7013429666935f8c9aa6ce550046fd42c75865e Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 13 Dec 2018 15:21:31 -0800 Subject: [PATCH 38/39] Add and fix test for tick airthmetic deprecation --- pandas/tests/scalar/timedelta/test_arithmetic.py | 2 +- pandas/tests/tseries/offsets/test_offsets.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pandas/tests/scalar/timedelta/test_arithmetic.py b/pandas/tests/scalar/timedelta/test_arithmetic.py index b6ad251d598ab..d3c92614c7962 100644 --- a/pandas/tests/scalar/timedelta/test_arithmetic.py +++ b/pandas/tests/scalar/timedelta/test_arithmetic.py @@ -44,7 +44,7 @@ def test_td_add_sub_ten_seconds(self, ten_seconds): Timedelta('1 days, 00:00:10'), timedelta(days=1, seconds=10), np.timedelta64(1, 'D') + np.timedelta64(10, 's'), - pd.offsets.Day() + pd.offsets.Second(10)]) + pd.offsets.Hour(24) + pd.offsets.Second(10)]) def test_td_add_sub_one_day_ten_seconds(self, one_day_ten_secs): # GH#6808 base = Timestamp('20130102 09:01:12.123456') diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 55dde6310eb22..e7f9549d9cf37 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -3217,3 +3217,8 @@ def test_invalid_arithmetic(self, arg): # (absolute time) with pytest.raises(TypeError): _Day(1) + arg + + def test_day_tick_arithmetic_deprecation(self): + with tm.assert_produces_warning(DeprecationWarning): + Day(1) + Tick(24) + Tick(24) + Day(1) From 78369f521615db3488983948adc778aeffe16e20 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 13 Dec 2018 15:43:00 -0800 Subject: [PATCH 39/39] Deprecate Day arithmetic with Timestamptz --- pandas/_libs/tslibs/timestamps.pyx | 3 +++ pandas/tests/tseries/offsets/test_offsets.py | 5 +++++ pandas/tseries/offsets.py | 7 ++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 472ac0ee6d45c..82fdb5b0d6dbd 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -360,6 +360,9 @@ cdef class _Timestamp(datetime): elif PyDelta_Check(other) or hasattr(other, 'delta'): # delta --> offsets.Tick + if self.tz is not None and getattr(other, '_prefix') == 'D': + warnings.warn("Day arithmetic will respect calendar day in a" + "future release", DeprecationWarning) nanos = delta_to_nanoseconds(other) result = Timestamp(self.value + nanos, tz=self.tzinfo, freq=self.freq) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index e7f9549d9cf37..c40af49078f82 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -3222,3 +3222,8 @@ def test_day_tick_arithmetic_deprecation(self): with tm.assert_produces_warning(DeprecationWarning): Day(1) + Tick(24) Tick(24) + Day(1) + + def test_day_timestamptz_arithmetic_deprecation(self): + with tm.assert_produces_warning(DeprecationWarning): + Timestamp("2012-10-28", tz='Europe/Brussels') + Day() + Day() + Timestamp("2012-10-28", tz='Europe/Brussels') \ No newline at end of file diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index c33ff0e544ff1..f3473b917a5ab 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2308,7 +2308,8 @@ def __init__(self, n=1, normalize=False): def __add__(self, other): if isinstance(other, Tick): if self._prefix == 'D' or other._prefix == 'D': - warnings.warn("Arithmetic with Day is deprecated.", + warnings.warn("Arithmetic with Day is deprecated. Day will " + "become a non-fixed offset.", DeprecationWarning, stacklevel=2) if type(self) == type(other): return type(self)(self.n + other.n) @@ -2382,6 +2383,10 @@ def apply(self, other): result = other.__add__(self) if result == NotImplemented: raise OverflowError + if other.tz is not None and self._prefix == 'D': + warnings.warn("Day arithmetic will respect calendar day in a " + "future release", DeprecationWarning, + stacklevel=3) return result elif isinstance(other, (datetime, np.datetime64, date)): return as_timestamp(other) + self