Skip to content

Commit 76fa98b

Browse files
authored
ENH: Add milliseconds field support for pd.DateOffset (#43598)
1 parent 5af2229 commit 76fa98b

File tree

4 files changed

+167
-11
lines changed

4 files changed

+167
-11
lines changed

doc/source/user_guide/timeseries.rst

+1
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,7 @@ arithmetic operator (``+``) can be used to perform the shift.
869869
friday + two_business_days
870870
(friday + two_business_days).day_name()
871871
872+
872873
Most ``DateOffsets`` have associated frequencies strings, or offset aliases, that can be passed
873874
into ``freq`` keyword arguments. The available date offsets and associated frequency strings can be found below:
874875

doc/source/whatsnew/v1.5.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Other enhancements
4444
- Implemented a complex-dtype :class:`Index`, passing a complex-dtype array-like to ``pd.Index`` will now retain complex dtype instead of casting to ``object`` (:issue:`45845`)
4545
- Improved error message in :class:`~pandas.core.window.Rolling` when ``window`` is a frequency and ``NaT`` is in the rolling axis (:issue:`46087`)
4646
- :class:`Series` and :class:`DataFrame` with ``IntegerDtype`` now supports bitwise operations (:issue:`34463`)
47+
- Add ``milliseconds`` field support for :class:`~pandas.DateOffset` (:issue:`43371`)
4748
-
4849

4950
.. ---------------------------------------------------------------------------

pandas/_libs/tslibs/offsets.pyx

+21-3
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,9 @@ cdef _validate_business_time(t_input):
307307

308308
_relativedelta_kwds = {"years", "months", "weeks", "days", "year", "month",
309309
"day", "weekday", "hour", "minute", "second",
310-
"microsecond", "nanosecond", "nanoseconds", "hours",
311-
"minutes", "seconds", "microseconds"}
310+
"microsecond", "millisecond", "nanosecond",
311+
"nanoseconds", "hours", "minutes", "seconds",
312+
"milliseconds", "microseconds"}
312313

313314

314315
cdef _determine_offset(kwds):
@@ -323,11 +324,19 @@ cdef _determine_offset(kwds):
323324

324325
_kwds_use_relativedelta = ('years', 'months', 'weeks', 'days',
325326
'year', 'month', 'week', 'day', 'weekday',
326-
'hour', 'minute', 'second', 'microsecond')
327+
'hour', 'minute', 'second', 'microsecond',
328+
'millisecond')
327329

328330
use_relativedelta = False
329331
if len(kwds_no_nanos) > 0:
330332
if any(k in _kwds_use_relativedelta for k in kwds_no_nanos):
333+
if "millisecond" in kwds_no_nanos:
334+
raise NotImplementedError(
335+
"Using DateOffset to replace `millisecond` component in "
336+
"datetime object is not supported. Use "
337+
"`microsecond=timestamp.microsecond % 1000 + ms * 1000` "
338+
"instead."
339+
)
331340
offset = relativedelta(**kwds_no_nanos)
332341
use_relativedelta = True
333342
else:
@@ -1223,6 +1232,9 @@ class DateOffset(RelativeDeltaOffset, metaclass=OffsetMeta):
12231232
12241233
Since 0 is a bit weird, we suggest avoiding its use.
12251234
1235+
Besides, adding a DateOffsets specified by the singular form of the date
1236+
component can be used to replace certain component of the timestamp.
1237+
12261238
Parameters
12271239
----------
12281240
n : int, default 1
@@ -1243,6 +1255,7 @@ class DateOffset(RelativeDeltaOffset, metaclass=OffsetMeta):
12431255
- hours
12441256
- minutes
12451257
- seconds
1258+
- milliseconds
12461259
- microseconds
12471260
- nanoseconds
12481261
@@ -1274,6 +1287,11 @@ class DateOffset(RelativeDeltaOffset, metaclass=OffsetMeta):
12741287
>>> ts = pd.Timestamp('2017-01-01 09:10:11')
12751288
>>> ts + DateOffset(months=2)
12761289
Timestamp('2017-03-01 09:10:11')
1290+
>>> ts + DateOffset(day=31)
1291+
Timestamp('2017-01-31 09:10:11')
1292+
1293+
>>> ts + pd.DateOffset(hour=8)
1294+
Timestamp('2017-01-01 08:10:11')
12771295
"""
12781296
def __setattr__(self, name, value):
12791297
raise AttributeError("DateOffset objects are immutable.")

pandas/tests/tseries/offsets/test_offsets.py

+144-8
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@
6161

6262
_ApplyCases = List[Tuple[BaseOffset, Dict[datetime, datetime]]]
6363

64+
_ARITHMETIC_DATE_OFFSET = [
65+
"years",
66+
"months",
67+
"weeks",
68+
"days",
69+
"hours",
70+
"minutes",
71+
"seconds",
72+
"milliseconds",
73+
"microseconds",
74+
]
75+
6476

6577
class TestCommon(Base):
6678
# executed value created by Base._get_offset
@@ -551,28 +563,144 @@ def test_mul(self):
551563
assert DateOffset(2) == 2 * DateOffset(1)
552564
assert DateOffset(2) == DateOffset(1) * 2
553565

554-
def test_constructor(self):
555-
556-
assert (self.d + DateOffset(months=2)) == datetime(2008, 3, 2)
557-
assert (self.d - DateOffset(months=2)) == datetime(2007, 11, 2)
566+
@pytest.mark.parametrize("relativedelta_kwd", list(liboffsets._relativedelta_kwds))
567+
def test_constructor(self, relativedelta_kwd, request):
568+
if relativedelta_kwd == "millisecond":
569+
request.node.add_marker(
570+
pytest.mark.xfail(
571+
raises=NotImplementedError,
572+
reason="Constructing DateOffset object with `millisecond` is not "
573+
"yet supported.",
574+
)
575+
)
576+
offset = DateOffset(**{relativedelta_kwd: 2})
577+
assert offset.kwds == {relativedelta_kwd: 2}
578+
assert getattr(offset, relativedelta_kwd) == 2
558579

580+
def test_default_constructor(self):
559581
assert (self.d + DateOffset(2)) == datetime(2008, 1, 4)
560582

583+
def test_is_anchored(self):
561584
assert not DateOffset(2).is_anchored()
562585
assert DateOffset(1).is_anchored()
563586

564-
d = datetime(2008, 1, 31)
565-
assert (d + DateOffset(months=1)) == datetime(2008, 2, 29)
566-
567587
def test_copy(self):
568588
assert DateOffset(months=2).copy() == DateOffset(months=2)
589+
assert DateOffset(milliseconds=1).copy() == DateOffset(milliseconds=1)
590+
591+
@pytest.mark.parametrize(
592+
"arithmatic_offset_type, expected",
593+
zip(
594+
_ARITHMETIC_DATE_OFFSET,
595+
[
596+
"2009-01-02",
597+
"2008-02-02",
598+
"2008-01-09",
599+
"2008-01-03",
600+
"2008-01-02 01:00:00",
601+
"2008-01-02 00:01:00",
602+
"2008-01-02 00:00:01",
603+
"2008-01-02 00:00:00.001000000",
604+
"2008-01-02 00:00:00.000001000",
605+
],
606+
),
607+
)
608+
def test_add(self, arithmatic_offset_type, expected):
609+
assert DateOffset(**{arithmatic_offset_type: 1}) + self.d == Timestamp(expected)
610+
assert self.d + DateOffset(**{arithmatic_offset_type: 1}) == Timestamp(expected)
611+
612+
@pytest.mark.parametrize(
613+
"arithmatic_offset_type, expected",
614+
zip(
615+
_ARITHMETIC_DATE_OFFSET,
616+
[
617+
"2007-01-02",
618+
"2007-12-02",
619+
"2007-12-26",
620+
"2008-01-01",
621+
"2008-01-01 23:00:00",
622+
"2008-01-01 23:59:00",
623+
"2008-01-01 23:59:59",
624+
"2008-01-01 23:59:59.999000000",
625+
"2008-01-01 23:59:59.999999000",
626+
],
627+
),
628+
)
629+
def test_sub(self, arithmatic_offset_type, expected):
630+
assert self.d - DateOffset(**{arithmatic_offset_type: 1}) == Timestamp(expected)
631+
with pytest.raises(TypeError, match="Cannot subtract datetime from offset"):
632+
DateOffset(**{arithmatic_offset_type: 1}) - self.d
633+
634+
@pytest.mark.parametrize(
635+
"arithmatic_offset_type, n, expected",
636+
zip(
637+
_ARITHMETIC_DATE_OFFSET,
638+
range(1, 10),
639+
[
640+
"2009-01-02",
641+
"2008-03-02",
642+
"2008-01-23",
643+
"2008-01-06",
644+
"2008-01-02 05:00:00",
645+
"2008-01-02 00:06:00",
646+
"2008-01-02 00:00:07",
647+
"2008-01-02 00:00:00.008000000",
648+
"2008-01-02 00:00:00.000009000",
649+
],
650+
),
651+
)
652+
def test_mul_add(self, arithmatic_offset_type, n, expected):
653+
assert DateOffset(**{arithmatic_offset_type: 1}) * n + self.d == Timestamp(
654+
expected
655+
)
656+
assert n * DateOffset(**{arithmatic_offset_type: 1}) + self.d == Timestamp(
657+
expected
658+
)
659+
assert self.d + DateOffset(**{arithmatic_offset_type: 1}) * n == Timestamp(
660+
expected
661+
)
662+
assert self.d + n * DateOffset(**{arithmatic_offset_type: 1}) == Timestamp(
663+
expected
664+
)
665+
666+
@pytest.mark.parametrize(
667+
"arithmatic_offset_type, n, expected",
668+
zip(
669+
_ARITHMETIC_DATE_OFFSET,
670+
range(1, 10),
671+
[
672+
"2007-01-02",
673+
"2007-11-02",
674+
"2007-12-12",
675+
"2007-12-29",
676+
"2008-01-01 19:00:00",
677+
"2008-01-01 23:54:00",
678+
"2008-01-01 23:59:53",
679+
"2008-01-01 23:59:59.992000000",
680+
"2008-01-01 23:59:59.999991000",
681+
],
682+
),
683+
)
684+
def test_mul_sub(self, arithmatic_offset_type, n, expected):
685+
assert self.d - DateOffset(**{arithmatic_offset_type: 1}) * n == Timestamp(
686+
expected
687+
)
688+
assert self.d - n * DateOffset(**{arithmatic_offset_type: 1}) == Timestamp(
689+
expected
690+
)
691+
692+
def test_leap_year(self):
693+
d = datetime(2008, 1, 31)
694+
assert (d + DateOffset(months=1)) == datetime(2008, 2, 29)
569695

570696
def test_eq(self):
571697
offset1 = DateOffset(days=1)
572698
offset2 = DateOffset(days=365)
573699

574700
assert offset1 != offset2
575701

702+
assert DateOffset(milliseconds=3) != DateOffset(milliseconds=7)
703+
576704

577705
class TestOffsetNames:
578706
def test_get_offset_name(self):
@@ -741,7 +869,15 @@ def test_month_offset_name(month_classes):
741869

742870

743871
@pytest.mark.parametrize("kwd", sorted(liboffsets._relativedelta_kwds))
744-
def test_valid_relativedelta_kwargs(kwd):
872+
def test_valid_relativedelta_kwargs(kwd, request):
873+
if kwd == "millisecond":
874+
request.node.add_marker(
875+
pytest.mark.xfail(
876+
raises=NotImplementedError,
877+
reason="Constructing DateOffset object with `millisecond` is not "
878+
"yet supported.",
879+
)
880+
)
745881
# Check that all the arguments specified in liboffsets._relativedelta_kwds
746882
# are in fact valid relativedelta keyword args
747883
DateOffset(**{kwd: 1})

0 commit comments

Comments
 (0)