From 62918ce37db07250bf12428d1d6db435aa2a8fa0 Mon Sep 17 00:00:00 2001 From: isVoid Date: Thu, 16 Sep 2021 04:20:15 +0000 Subject: [PATCH 01/21] initial --- pandas/_libs/tslibs/offsets.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 4c9b681452c0a..5c934cf7f7a7e 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -300,7 +300,7 @@ cdef _validate_business_time(t_input): _relativedelta_kwds = {"years", "months", "weeks", "days", "year", "month", "day", "weekday", "hour", "minute", "second", "microsecond", "nanosecond", "nanoseconds", "hours", - "minutes", "seconds", "microseconds"} + "minutes", "seconds", "milliseconds", "microseconds"} cdef _determine_offset(kwds): @@ -315,7 +315,7 @@ cdef _determine_offset(kwds): _kwds_use_relativedelta = ('years', 'months', 'weeks', 'days', 'year', 'month', 'week', 'day', 'weekday', - 'hour', 'minute', 'second', 'microsecond') + 'hour', 'minute', 'second', 'millisecond', 'microsecond') use_relativedelta = False if len(kwds_no_nanos) > 0: From a04bf6432d0d2154b20c23fb737d7fb2c2f04f22 Mon Sep 17 00:00:00 2001 From: isVoid Date: Thu, 16 Sep 2021 04:23:26 +0000 Subject: [PATCH 02/21] docstrings --- pandas/_libs/tslibs/offsets.pyx | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 5c934cf7f7a7e..9727301737cff 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -1211,6 +1211,7 @@ class DateOffset(RelativeDeltaOffset, metaclass=OffsetMeta): - hours - minutes - seconds + - milliseconds - microseconds - nanoseconds From b61c14db1cd3600868e952f7f20d2eba0b26d8d0 Mon Sep 17 00:00:00 2001 From: isVoid Date: Thu, 16 Sep 2021 04:31:14 +0000 Subject: [PATCH 03/21] Remove unwanted change --- pandas/_libs/tslibs/offsets.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 9727301737cff..2f0bb19b3dd75 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -315,7 +315,7 @@ cdef _determine_offset(kwds): _kwds_use_relativedelta = ('years', 'months', 'weeks', 'days', 'year', 'month', 'week', 'day', 'weekday', - 'hour', 'minute', 'second', 'millisecond', 'microsecond') + 'hour', 'minute', 'second', 'microsecond') use_relativedelta = False if len(kwds_no_nanos) > 0: From c067d2405a37b1b9e3f32a016a4596f3952d23f7 Mon Sep 17 00:00:00 2001 From: isVoid Date: Tue, 28 Sep 2021 21:30:18 +0000 Subject: [PATCH 04/21] Add tests --- pandas/tests/tseries/offsets/test_offsets.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 0c79c0b64f4cd..6637735efd685 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -528,6 +528,7 @@ def test_offsets_hashable(self, offset_types): class TestDateOffset(Base): def setup_method(self, method): self.d = Timestamp(datetime(2008, 1, 2)) + self.d2 = Timestamp("2021-10-01 08:00:00.000000000") _offset_map.clear() def test_repr(self): @@ -540,11 +541,20 @@ def test_mul(self): assert DateOffset(2) == 2 * DateOffset(1) assert DateOffset(2) == DateOffset(1) * 2 + assert DateOffset(milliseconds=3) * 2 == DateOffset(milliseconds=3, n=2) + def test_constructor(self): assert (self.d + DateOffset(months=2)) == datetime(2008, 3, 2) assert (self.d - DateOffset(months=2)) == datetime(2007, 11, 2) + assert self.d2 + DateOffset(milliseconds=7) == Timestamp( + "2021-10-01 08:00:00.007000000" + ) + assert self.d2 - DateOffset(milliseconds=7) == Timestamp( + "2021-10-01 07:59:59.993000000" + ) + assert (self.d + DateOffset(2)) == datetime(2008, 1, 4) assert not DateOffset(2).is_anchored() @@ -555,6 +565,7 @@ def test_constructor(self): def test_copy(self): assert DateOffset(months=2).copy() == DateOffset(months=2) + assert DateOffset(milliseconds=1).copy() == DateOffset(milliseconds=1) def test_eq(self): offset1 = DateOffset(days=1) @@ -562,6 +573,8 @@ def test_eq(self): assert offset1 != offset2 + assert DateOffset(milliseconds=3) != DateOffset(milliseconds=7) + class TestOffsetNames: def test_get_offset_name(self): From d0814a8101f0d552aa1e60f391603bf5d5384cae Mon Sep 17 00:00:00 2001 From: isVoid Date: Tue, 28 Sep 2021 22:28:15 +0000 Subject: [PATCH 05/21] add license --- pandas/_libs/tslibs/offsets.pyx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 2f0bb19b3dd75..dfc3a3a63cce1 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -1,3 +1,5 @@ +# Copyright (c) 2021, NVIDIA CORPORATION. + import operator import re import time From 8e96489b200aff1bf7d9e8e3bc9c4682de6618c2 Mon Sep 17 00:00:00 2001 From: isVoid Date: Tue, 28 Sep 2021 22:33:41 +0000 Subject: [PATCH 06/21] Add to whats new section --- doc/source/whatsnew/v1.4.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index eb1bc270a7d1b..6c7e339ae4a6c 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -124,6 +124,7 @@ Other enhancements - Add :meth:`Series.str.removeprefix` and :meth:`Series.str.removesuffix` introduced in Python 3.9 to remove pre-/suffixes from string-type :class:`Series` (:issue:`36944`) - Attempting to write into a file in missing parent directory with :meth:`DataFrame.to_csv`, :meth:`DataFrame.to_html`, :meth:`DataFrame.to_excel`, :meth:`DataFrame.to_feather`, :meth:`DataFrame.to_parquet`, :meth:`DataFrame.to_stata`, :meth:`DataFrame.to_json`, :meth:`DataFrame.to_pickle`, and :meth:`DataFrame.to_xml` now explicitly mentions missing parent directory, the same is true for :class:`Series` counterparts (:issue:`24306`) - Added support for nullable boolean and integer types in :meth:`DataFrame.to_stata`, :class:`~pandas.io.stata.StataWriter`, :class:`~pandas.io.stata.StataWriter117`, and :class:`~pandas.io.stata.StataWriterUTF8` (:issue:`40855`) +- Add ``milliseconds`` field support for :class:`~pandas.DateOffset` - .. --------------------------------------------------------------------------- From a02be79bcee49a5dca27a27c50267f8e5100fb1a Mon Sep 17 00:00:00 2001 From: isVoid Date: Tue, 5 Oct 2021 01:00:23 +0000 Subject: [PATCH 07/21] restructure TestDateOffset and extend with more coverage over parameters --- pandas/tests/tseries/offsets/test_offsets.py | 142 +++++++++++++++++-- 1 file changed, 129 insertions(+), 13 deletions(-) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 6637735efd685..7c6806b3ae655 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -62,6 +62,18 @@ _ApplyCases = List[Tuple[BaseOffset, Dict[datetime, datetime]]] +_ARITHMATIC_DATE_OFFSET = [ + "years", + "months", + "weeks", + "days", + "hours", + "minutes", + "seconds", + "milliseconds", + "microseconds", +] + class TestCommon(Base): # executed value created by Base._get_offset @@ -528,7 +540,6 @@ def test_offsets_hashable(self, offset_types): class TestDateOffset(Base): def setup_method(self, method): self.d = Timestamp(datetime(2008, 1, 2)) - self.d2 = Timestamp("2021-10-01 08:00:00.000000000") _offset_map.clear() def test_repr(self): @@ -543,30 +554,135 @@ def test_mul(self): assert DateOffset(milliseconds=3) * 2 == DateOffset(milliseconds=3, n=2) - def test_constructor(self): + @pytest.mark.parametrize("offset_type", list(liboffsets._relativedelta_kwds)) + def test_constructor(self, offset_type): + # assert (self.d + DateOffset(months=2)) == datetime(2008, 3, 2) + # assert (self.d - DateOffset(months=2)) == datetime(2007, 11, 2) - assert (self.d + DateOffset(months=2)) == datetime(2008, 3, 2) - assert (self.d - DateOffset(months=2)) == datetime(2007, 11, 2) + # assert self.d + DateOffset(milliseconds=7) == Timestamp( + # "2008-01-02 00:00:00.007000000" + # ) + # assert self.d - DateOffset(milliseconds=7) == Timestamp( + # "2008-01-01 23:59:59.993000000" + # ) - assert self.d2 + DateOffset(milliseconds=7) == Timestamp( - "2021-10-01 08:00:00.007000000" - ) - assert self.d2 - DateOffset(milliseconds=7) == Timestamp( - "2021-10-01 07:59:59.993000000" - ) + DateOffset(**{offset_type: 2}) + def test_default_constructor(self): assert (self.d + DateOffset(2)) == datetime(2008, 1, 4) + def test_is_anchored(self): assert not DateOffset(2).is_anchored() assert DateOffset(1).is_anchored() - d = datetime(2008, 1, 31) - assert (d + DateOffset(months=1)) == datetime(2008, 2, 29) - def test_copy(self): assert DateOffset(months=2).copy() == DateOffset(months=2) assert DateOffset(milliseconds=1).copy() == DateOffset(milliseconds=1) + @pytest.mark.parametrize( + "arithmatic_offset_type, expected", + zip( + _ARITHMATIC_DATE_OFFSET, + [ + "2009-01-02", + "2008-02-02", + "2008-01-09", + "2008-01-03", + "2008-01-02 01:00:00", + "2008-01-02 00:01:00", + "2008-01-02 00:00:01", + "2008-01-02 00:00:00.001000000", + "2008-01-02 00:00:00.000001000", + ], + ), + ) + def test_add(self, arithmatic_offset_type, expected): + assert DateOffset(**{arithmatic_offset_type: 1}).__add__(self.d) == Timestamp( + expected + ) + assert DateOffset(**{arithmatic_offset_type: 1}).__radd__(self.d) == Timestamp( + expected + ) + + @pytest.mark.parametrize( + "arithmatic_offset_type, expected", + zip( + _ARITHMATIC_DATE_OFFSET, + [ + "2007-01-02", + "2007-12-02", + "2007-12-26", + "2008-01-01", + "2008-01-01 23:00:00", + "2008-01-01 23:59:00", + "2008-01-01 23:59:59", + "2008-01-01 23:59:59.999000000", + "2008-01-01 23:59:59.999999000", + ], + ), + ) + def test_sub(self, arithmatic_offset_type, expected): + assert DateOffset(**{arithmatic_offset_type: 1}).__rsub__(self.d) == Timestamp( + expected + ) + with pytest.raises(TypeError, match="Cannot subtract datetime from offset"): + DateOffset(**{arithmatic_offset_type: 1}).__sub__(self.d) == Timestamp( + expected + ) + + @pytest.mark.parametrize( + "arithmatic_offset_type, n, expected", + zip( + _ARITHMATIC_DATE_OFFSET, + range(1, 10), + [ + "2009-01-02", + "2008-03-02", + "2008-01-23", + "2008-01-06", + "2008-01-02 05:00:00", + "2008-01-02 00:06:00", + "2008-01-02 00:00:07", + "2008-01-02 00:00:00.008000000", + "2008-01-02 00:00:00.000009000", + ], + ), + ) + def test_mul_add(self, arithmatic_offset_type, n, expected): + assert DateOffset(**{arithmatic_offset_type: 1}).__mul__(n).__add__( + self.d + ) == Timestamp(expected) + assert DateOffset(**{arithmatic_offset_type: 1}).__mul__(n).__radd__( + self.d + ) == Timestamp(expected) + + @pytest.mark.parametrize( + "arithmatic_offset_type, n, expected", + zip( + _ARITHMATIC_DATE_OFFSET, + range(1, 10), + [ + "2007-01-02", + "2007-11-02", + "2007-12-12", + "2007-12-29", + "2008-01-01 19:00:00", + "2008-01-01 23:54:00", + "2008-01-01 23:59:53", + "2008-01-01 23:59:59.992000000", + "2008-01-01 23:59:59.999991000", + ], + ), + ) + def test_mul_sub(self, arithmatic_offset_type, n, expected): + assert DateOffset(**{arithmatic_offset_type: 1}).__mul__(n).__rsub__( + self.d + ) == Timestamp(expected) + + def test_leap_year(self): + d = datetime(2008, 1, 31) + assert (d + DateOffset(months=1)) == datetime(2008, 2, 29) + def test_eq(self): offset1 = DateOffset(days=1) offset2 = DateOffset(days=365) From 255fe23a6125a86acd73102df8dc963987534116 Mon Sep 17 00:00:00 2001 From: isVoid Date: Tue, 5 Oct 2021 01:01:12 +0000 Subject: [PATCH 08/21] remove license --- pandas/_libs/tslibs/offsets.pyx | 2 -- 1 file changed, 2 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index dfc3a3a63cce1..2f0bb19b3dd75 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -1,5 +1,3 @@ -# Copyright (c) 2021, NVIDIA CORPORATION. - import operator import re import time From b5a42e662445eb028eae5f8993ede2327263a802 Mon Sep 17 00:00:00 2001 From: isVoid Date: Tue, 5 Oct 2021 01:02:57 +0000 Subject: [PATCH 09/21] remove stale comments --- pandas/tests/tseries/offsets/test_offsets.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 7c6806b3ae655..07f6406356512 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -556,16 +556,6 @@ def test_mul(self): @pytest.mark.parametrize("offset_type", list(liboffsets._relativedelta_kwds)) def test_constructor(self, offset_type): - # assert (self.d + DateOffset(months=2)) == datetime(2008, 3, 2) - # assert (self.d - DateOffset(months=2)) == datetime(2007, 11, 2) - - # assert self.d + DateOffset(milliseconds=7) == Timestamp( - # "2008-01-02 00:00:00.007000000" - # ) - # assert self.d - DateOffset(milliseconds=7) == Timestamp( - # "2008-01-01 23:59:59.993000000" - # ) - DateOffset(**{offset_type: 2}) def test_default_constructor(self): From 4801de33ac2c4baa05cad636e5d5e41ba99e7b15 Mon Sep 17 00:00:00 2001 From: isVoid Date: Wed, 20 Oct 2021 03:06:52 +0000 Subject: [PATCH 10/21] Removing existing mul tests --- pandas/tests/tseries/offsets/test_offsets.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 07f6406356512..421fe68bf893f 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -552,8 +552,6 @@ def test_mul(self): assert DateOffset(2) == 2 * DateOffset(1) assert DateOffset(2) == DateOffset(1) * 2 - assert DateOffset(milliseconds=3) * 2 == DateOffset(milliseconds=3, n=2) - @pytest.mark.parametrize("offset_type", list(liboffsets._relativedelta_kwds)) def test_constructor(self, offset_type): DateOffset(**{offset_type: 2}) From dbadf9f764d6475ffed3eac5a0860f9a28cdc8f0 Mon Sep 17 00:00:00 2001 From: isVoid Date: Wed, 20 Oct 2021 03:15:51 +0000 Subject: [PATCH 11/21] change test parameter name --- pandas/tests/tseries/offsets/test_offsets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 421fe68bf893f..a8546b85dc63d 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -552,9 +552,9 @@ def test_mul(self): assert DateOffset(2) == 2 * DateOffset(1) assert DateOffset(2) == DateOffset(1) * 2 - @pytest.mark.parametrize("offset_type", list(liboffsets._relativedelta_kwds)) - def test_constructor(self, offset_type): - DateOffset(**{offset_type: 2}) + @pytest.mark.parametrize("relativedelta_kwd", list(liboffsets._relativedelta_kwds)) + def test_constructor(self, relativedelta_kwd): + DateOffset(**{relativedelta_kwd: 2}) def test_default_constructor(self): assert (self.d + DateOffset(2)) == datetime(2008, 1, 4) From 30d3403ee3e11716db35505e6b6e5caf105b9dfc Mon Sep 17 00:00:00 2001 From: isVoid Date: Wed, 20 Oct 2021 04:02:47 +0000 Subject: [PATCH 12/21] Add some docs for replace component usage --- doc/source/user_guide/timeseries.rst | 5 +++++ pandas/_libs/tslibs/offsets.pyx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/source/user_guide/timeseries.rst b/doc/source/user_guide/timeseries.rst index a112c632ceb25..a5f8a96cd13e3 100644 --- a/doc/source/user_guide/timeseries.rst +++ b/doc/source/user_guide/timeseries.rst @@ -853,6 +853,9 @@ savings time. However, all :class:`DateOffset` subclasses that are an hour or sm The basic :class:`DateOffset` acts similar to ``dateutil.relativedelta`` (`relativedelta documentation`_) that shifts a date time by the corresponding calendar duration specified. The arithmetic operator (``+``) or the ``apply`` method can be used to perform the shift. +Multiplication (*) and subtraction are also supported. Besides arithmetic operations, +basic :class:`DateOffset` can also be specified to replace certain component of the +timestamp. .. ipython:: python @@ -869,6 +872,8 @@ arithmetic operator (``+``) or the ``apply`` method can be used to perform the s two_business_days.apply(friday) friday + two_business_days (friday + two_business_days).day_name() + # Replaces "hour" component on ts + pd.DateOffset(hour=8).apply(ts) Most ``DateOffsets`` have associated frequencies strings, or offset aliases, that can be passed into ``freq`` keyword arguments. The available date offsets and associated frequency strings can be found below: diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index c7aab02d35a88..8a01221ba5809 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -1241,8 +1241,8 @@ class DateOffset(RelativeDeltaOffset, metaclass=OffsetMeta): Timestamp('2017-04-01 09:10:11') >>> ts = pd.Timestamp('2017-01-01 09:10:11') - >>> ts + DateOffset(months=2) - Timestamp('2017-03-01 09:10:11') + >>> DateOffset(day=31).apply(ts) + Timestamp('2017-01-31 09:10:11') """ def __setattr__(self, name, value): raise AttributeError("DateOffset objects are immutable.") From bafceb6a996d6331e15d0fd0dafdb4ae85ee6ba9 Mon Sep 17 00:00:00 2001 From: isVoid Date: Fri, 22 Oct 2021 04:07:40 +0000 Subject: [PATCH 13/21] docstring and test fixes --- doc/source/user_guide/timeseries.rst | 2 +- pandas/_libs/tslibs/offsets.pyx | 4 +- pandas/tests/tseries/offsets/test_offsets.py | 43 ++++++++++---------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/doc/source/user_guide/timeseries.rst b/doc/source/user_guide/timeseries.rst index a5f8a96cd13e3..10b3f593a6360 100644 --- a/doc/source/user_guide/timeseries.rst +++ b/doc/source/user_guide/timeseries.rst @@ -873,7 +873,7 @@ timestamp. friday + two_business_days (friday + two_business_days).day_name() # Replaces "hour" component on ts - pd.DateOffset(hour=8).apply(ts) + ts + pd.DateOffset(hour=8) Most ``DateOffsets`` have associated frequencies strings, or offset aliases, that can be passed into ``freq`` keyword arguments. The available date offsets and associated frequency strings can be found below: diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 8a01221ba5809..209510ce3bff1 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -1241,7 +1241,9 @@ class DateOffset(RelativeDeltaOffset, metaclass=OffsetMeta): Timestamp('2017-04-01 09:10:11') >>> ts = pd.Timestamp('2017-01-01 09:10:11') - >>> DateOffset(day=31).apply(ts) + >>> ts + DateOffset(months=2) + Timestamp('2017-03-01 09:10:11') + >>> ts + DateOffset(day=31) Timestamp('2017-01-31 09:10:11') """ def __setattr__(self, name, value): diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index a8546b85dc63d..14e6b6f55f1e9 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -585,12 +585,8 @@ def test_copy(self): ), ) def test_add(self, arithmatic_offset_type, expected): - assert DateOffset(**{arithmatic_offset_type: 1}).__add__(self.d) == Timestamp( - expected - ) - assert DateOffset(**{arithmatic_offset_type: 1}).__radd__(self.d) == Timestamp( - expected - ) + assert DateOffset(**{arithmatic_offset_type: 1}) + self.d == Timestamp(expected) + assert self.d + DateOffset(**{arithmatic_offset_type: 1}) == Timestamp(expected) @pytest.mark.parametrize( "arithmatic_offset_type, expected", @@ -610,13 +606,9 @@ def test_add(self, arithmatic_offset_type, expected): ), ) def test_sub(self, arithmatic_offset_type, expected): - assert DateOffset(**{arithmatic_offset_type: 1}).__rsub__(self.d) == Timestamp( - expected - ) + assert self.d - DateOffset(**{arithmatic_offset_type: 1}) == Timestamp(expected) with pytest.raises(TypeError, match="Cannot subtract datetime from offset"): - DateOffset(**{arithmatic_offset_type: 1}).__sub__(self.d) == Timestamp( - expected - ) + DateOffset(**{arithmatic_offset_type: 1}) - self.d @pytest.mark.parametrize( "arithmatic_offset_type, n, expected", @@ -637,12 +629,18 @@ def test_sub(self, arithmatic_offset_type, expected): ), ) def test_mul_add(self, arithmatic_offset_type, n, expected): - assert DateOffset(**{arithmatic_offset_type: 1}).__mul__(n).__add__( - self.d - ) == Timestamp(expected) - assert DateOffset(**{arithmatic_offset_type: 1}).__mul__(n).__radd__( - self.d - ) == Timestamp(expected) + assert DateOffset(**{arithmatic_offset_type: 1}) * n + self.d == Timestamp( + expected + ) + assert n * DateOffset(**{arithmatic_offset_type: 1}) + self.d == Timestamp( + expected + ) + assert self.d + DateOffset(**{arithmatic_offset_type: 1}) * n == Timestamp( + expected + ) + assert self.d + n * DateOffset(**{arithmatic_offset_type: 1}) == Timestamp( + expected + ) @pytest.mark.parametrize( "arithmatic_offset_type, n, expected", @@ -663,9 +661,12 @@ def test_mul_add(self, arithmatic_offset_type, n, expected): ), ) def test_mul_sub(self, arithmatic_offset_type, n, expected): - assert DateOffset(**{arithmatic_offset_type: 1}).__mul__(n).__rsub__( - self.d - ) == Timestamp(expected) + assert self.d - DateOffset(**{arithmatic_offset_type: 1}) * n == Timestamp( + expected + ) + assert self.d - n * DateOffset(**{arithmatic_offset_type: 1}) == Timestamp( + expected + ) def test_leap_year(self): d = datetime(2008, 1, 31) From 3bd154c87a47d68b743c0cbd1f9500f6fe53eae5 Mon Sep 17 00:00:00 2001 From: isVoid Date: Fri, 29 Oct 2021 18:45:10 +0000 Subject: [PATCH 14/21] add assertion to constructed dateoffset --- pandas/tests/tseries/offsets/test_offsets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 14e6b6f55f1e9..7b66ee7f6c364 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -554,7 +554,9 @@ def test_mul(self): @pytest.mark.parametrize("relativedelta_kwd", list(liboffsets._relativedelta_kwds)) def test_constructor(self, relativedelta_kwd): - DateOffset(**{relativedelta_kwd: 2}) + offset = DateOffset(**{relativedelta_kwd: 2}) + assert offset.kwds == {relativedelta_kwd: 2} + assert getattr(offset, relativedelta_kwd) == 2 def test_default_constructor(self): assert (self.d + DateOffset(2)) == datetime(2008, 1, 4) From 41e537f373fe1835dd3b34c1d5bf5a358e47c757 Mon Sep 17 00:00:00 2001 From: isVoid Date: Tue, 2 Nov 2021 04:13:04 +0000 Subject: [PATCH 15/21] raise when millisecond (replacement form) is specified --- pandas/_libs/tslibs/offsets.pyx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index d1d1633213177..203db3cd01156 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -299,8 +299,9 @@ cdef _validate_business_time(t_input): _relativedelta_kwds = {"years", "months", "weeks", "days", "year", "month", "day", "weekday", "hour", "minute", "second", - "microsecond", "nanosecond", "nanoseconds", "hours", - "minutes", "seconds", "milliseconds", "microseconds"} + "microsecond", "millisecond", "nanosecond", + "nanoseconds", "hours", "minutes", "seconds", + "milliseconds", "microseconds"} cdef _determine_offset(kwds): @@ -315,11 +316,17 @@ cdef _determine_offset(kwds): _kwds_use_relativedelta = ('years', 'months', 'weeks', 'days', 'year', 'month', 'week', 'day', 'weekday', - 'hour', 'minute', 'second', 'microsecond') + 'hour', 'minute', 'second', 'microsecond', + 'millisecond') use_relativedelta = False if len(kwds_no_nanos) > 0: if any(k in _kwds_use_relativedelta for k in kwds_no_nanos): + if 'millisecond' in kwds_no_nanos: + raise NotImplementedError( + "Use DateOffset to replace `millisecond` component in " + "datetime object is unsupported." + ) offset = relativedelta(**kwds_no_nanos) use_relativedelta = True else: From fcfce37eccb411c25377bb60467541c72e871d02 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 15 Dec 2021 04:19:08 +0000 Subject: [PATCH 16/21] apply review comments --- pandas/_libs/tslibs/offsets.pyx | 6 +++--- pandas/tests/tseries/offsets/test_offsets.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 09e36c9b9ac66..5da15f2f45d90 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -329,10 +329,10 @@ cdef _determine_offset(kwds): use_relativedelta = False if len(kwds_no_nanos) > 0: if any(k in _kwds_use_relativedelta for k in kwds_no_nanos): - if 'millisecond' in kwds_no_nanos: + if "millisecond" in kwds_no_nanos: raise NotImplementedError( - "Use DateOffset to replace `millisecond` component in " - "datetime object is unsupported." + "Using DateOffset to replace `millisecond` component in " + "datetime object is not supported." ) offset = relativedelta(**kwds_no_nanos) use_relativedelta = True diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 4a8130261f34e..da7993402f10f 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -62,7 +62,7 @@ _ApplyCases = List[Tuple[BaseOffset, Dict[datetime, datetime]]] -_ARITHMATIC_DATE_OFFSET = [ +_ARITHMETIC_DATE_OFFSET = [ "years", "months", "weeks", @@ -584,7 +584,7 @@ def test_copy(self): @pytest.mark.parametrize( "arithmatic_offset_type, expected", zip( - _ARITHMATIC_DATE_OFFSET, + _ARITHMETIC_DATE_OFFSET, [ "2009-01-02", "2008-02-02", @@ -605,7 +605,7 @@ def test_add(self, arithmatic_offset_type, expected): @pytest.mark.parametrize( "arithmatic_offset_type, expected", zip( - _ARITHMATIC_DATE_OFFSET, + _ARITHMETIC_DATE_OFFSET, [ "2007-01-02", "2007-12-02", @@ -627,7 +627,7 @@ def test_sub(self, arithmatic_offset_type, expected): @pytest.mark.parametrize( "arithmatic_offset_type, n, expected", zip( - _ARITHMATIC_DATE_OFFSET, + _ARITHMETIC_DATE_OFFSET, range(1, 10), [ "2009-01-02", @@ -659,7 +659,7 @@ def test_mul_add(self, arithmatic_offset_type, n, expected): @pytest.mark.parametrize( "arithmatic_offset_type, n, expected", zip( - _ARITHMATIC_DATE_OFFSET, + _ARITHMETIC_DATE_OFFSET, range(1, 10), [ "2007-01-02", From 1c7d7d769539af877c466fa16bda5cac6fb284ce Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 15 Dec 2021 04:20:16 +0000 Subject: [PATCH 17/21] fix fail tests --- pandas/tests/tseries/offsets/test_offsets.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index da7993402f10f..6f12868859544 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -566,6 +566,11 @@ def test_mul(self): @pytest.mark.parametrize("relativedelta_kwd", list(liboffsets._relativedelta_kwds)) def test_constructor(self, relativedelta_kwd): + if relativedelta_kwd == "millisecond": + pytest.skip( + "Constructing DateOffset object with `millisecond` is not yet " + "supported." + ) offset = DateOffset(**{relativedelta_kwd: 2}) assert offset.kwds == {relativedelta_kwd: 2} assert getattr(offset, relativedelta_kwd) == 2 @@ -871,6 +876,10 @@ def test_month_offset_name(month_classes): @pytest.mark.parametrize("kwd", sorted(liboffsets._relativedelta_kwds)) def test_valid_relativedelta_kwargs(kwd): + if kwd == "millisecond": + pytest.skip( + "Constructing DateOffset object with `millisecond` is not yet supported." + ) # Check that all the arguments specified in liboffsets._relativedelta_kwds # are in fact valid relativedelta keyword args DateOffset(**{kwd: 1}) From 99574eaa3408084521464eaaa35d3cf0e71489f3 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 29 Dec 2021 20:18:55 +0000 Subject: [PATCH 18/21] xfail instead of skip --- pandas/tests/tseries/offsets/test_offsets.py | 21 +++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 6f12868859544..0861ea32465a8 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -565,11 +565,14 @@ def test_mul(self): assert DateOffset(2) == DateOffset(1) * 2 @pytest.mark.parametrize("relativedelta_kwd", list(liboffsets._relativedelta_kwds)) - def test_constructor(self, relativedelta_kwd): + def test_constructor(self, relativedelta_kwd, request): if relativedelta_kwd == "millisecond": - pytest.skip( - "Constructing DateOffset object with `millisecond` is not yet " - "supported." + request.node.add_marker( + pytest.mark.xfail( + raises=NotImplementedError, + reason="Constructing DateOffset object with `millisecond` is not " + "yet supported.", + ) ) offset = DateOffset(**{relativedelta_kwd: 2}) assert offset.kwds == {relativedelta_kwd: 2} @@ -875,10 +878,14 @@ def test_month_offset_name(month_classes): @pytest.mark.parametrize("kwd", sorted(liboffsets._relativedelta_kwds)) -def test_valid_relativedelta_kwargs(kwd): +def test_valid_relativedelta_kwargs(kwd, request): if kwd == "millisecond": - pytest.skip( - "Constructing DateOffset object with `millisecond` is not yet supported." + request.node.add_marker( + pytest.mark.xfail( + raises=NotImplementedError, + reason="Constructing DateOffset object with `millisecond` is not " + "yet supported.", + ) ) # Check that all the arguments specified in liboffsets._relativedelta_kwds # are in fact valid relativedelta keyword args From e27146179b58fcb2a0b09b403045d97cd7faf66a Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Mon, 17 Jan 2022 19:52:48 +0000 Subject: [PATCH 19/21] Move notes to 1.5 --- doc/source/whatsnew/v1.4.0.rst | 1 - doc/source/whatsnew/v1.5.0.rst | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index 65947cea6b60d..47427620c8ece 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -203,7 +203,6 @@ Other enhancements - :meth:`IntegerArray.all` , :meth:`IntegerArray.any`, :meth:`FloatingArray.any`, and :meth:`FloatingArray.all` use Kleene logic (:issue:`41967`) - Added support for nullable boolean and integer types in :meth:`DataFrame.to_stata`, :class:`~pandas.io.stata.StataWriter`, :class:`~pandas.io.stata.StataWriter117`, and :class:`~pandas.io.stata.StataWriterUTF8` (:issue:`40855`) - :meth:`DataFrame.__pos__`, :meth:`DataFrame.__neg__` now retain ``ExtensionDtype`` dtypes (:issue:`43883`) -- Add ``milliseconds`` field support for :class:`~pandas.DateOffset` (:issue:`43371`) - The error raised when an optional dependency can't be imported now includes the original exception, for easier investigation (:issue:`43882`) - Added :meth:`.ExponentialMovingWindow.sum` (:issue:`13297`) - :meth:`Series.str.split` now supports a ``regex`` argument that explicitly specifies whether the pattern is a regular expression. Default is ``None`` (:issue:`43563`, :issue:`32835`, :issue:`25549`) diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index b259f182a1197..b393e33f37d6d 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -34,7 +34,7 @@ Other enhancements - :class:`StringArray` now accepts array-likes containing nan-likes (``None``, ``np.nan``) for the ``values`` parameter in its constructor in addition to strings and :attr:`pandas.NA`. (:issue:`40839`) - Improved the rendering of ``categories`` in :class:`CategoricalIndex` (:issue:`45218`) - :meth:`to_numeric` now preserves float64 arrays when downcasting would generate values not representable in float32 (:issue:`43693`) -- +- Add ``milliseconds`` field support for :class:`~pandas.DateOffset` (:issue:`43371`) .. --------------------------------------------------------------------------- .. _whatsnew_150.notable_bug_fixes: From b334f69685b60aa219be18e64242104d58d6d27f Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Mon, 31 Jan 2022 18:03:30 +0000 Subject: [PATCH 20/21] Add alternative to message --- pandas/_libs/tslibs/offsets.pyx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 1b23839689fd8..03d58865281b2 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -333,7 +333,9 @@ cdef _determine_offset(kwds): if "millisecond" in kwds_no_nanos: raise NotImplementedError( "Using DateOffset to replace `millisecond` component in " - "datetime object is not supported." + "datetime object is not supported. Use " + "`microsecond=timestamp.microsecond % 1000 + ms * 1000` " + "instead." ) offset = relativedelta(**kwds_no_nanos) use_relativedelta = True From 0fb51a19b8746c44ec10de67b2c7df17859794e9 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Tue, 8 Mar 2022 18:13:26 +0000 Subject: [PATCH 21/21] Move replacement doc to docstring --- doc/source/user_guide/timeseries.rst | 6 +----- pandas/_libs/tslibs/offsets.pyx | 6 ++++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/source/user_guide/timeseries.rst b/doc/source/user_guide/timeseries.rst index 8a6feb0133825..b524205ed7679 100644 --- a/doc/source/user_guide/timeseries.rst +++ b/doc/source/user_guide/timeseries.rst @@ -853,9 +853,6 @@ savings time. However, all :class:`DateOffset` subclasses that are an hour or sm The basic :class:`DateOffset` acts similar to ``dateutil.relativedelta`` (`relativedelta documentation`_) that shifts a date time by the corresponding calendar duration specified. The arithmetic operator (``+``) can be used to perform the shift. -Multiplication (*) and subtraction are also supported. Besides arithmetic operations, -basic :class:`DateOffset` can also be specified to replace certain component of the -timestamp. .. ipython:: python @@ -871,8 +868,7 @@ timestamp. two_business_days = 2 * pd.offsets.BDay() friday + two_business_days (friday + two_business_days).day_name() - # Replaces "hour" component on ts - ts + pd.DateOffset(hour=8) + Most ``DateOffsets`` have associated frequencies strings, or offset aliases, that can be passed into ``freq`` keyword arguments. The available date offsets and associated frequency strings can be found below: diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index e466100f98667..f19d34d99c814 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -1232,6 +1232,9 @@ class DateOffset(RelativeDeltaOffset, metaclass=OffsetMeta): Since 0 is a bit weird, we suggest avoiding its use. + Besides, adding a DateOffsets specified by the singular form of the date + component can be used to replace certain component of the timestamp. + Parameters ---------- n : int, default 1 @@ -1286,6 +1289,9 @@ class DateOffset(RelativeDeltaOffset, metaclass=OffsetMeta): Timestamp('2017-03-01 09:10:11') >>> ts + DateOffset(day=31) Timestamp('2017-01-31 09:10:11') + + >>> ts + pd.DateOffset(hour=8) + Timestamp('2017-01-01 08:10:11') """ def __setattr__(self, name, value): raise AttributeError("DateOffset objects are immutable.")