Repository: crsmithdev/arrow
Pull request #389 (2016-12-07 07:15:30)

Implement weekday shifting for #64
I’m not terribly happy with the implementation - I don’t grok why
dateutil handles some values (negatives and zeroes) the way it does,
but I figure this is good to generate discussion anyway.


Patch:
diff --git a/arrow/arrow.py b/arrow/arrow.py
index cda5536..b2e5c6a 100644
--- a/arrow/arrow.py
+++ b/arrow/arrow.py
@@ -471,13 +471,28 @@ class Arrow(object):
         >>> arw.shift(years=1, months=-1)
         <Arrow [2014-04-11T22:27:34.787885+00:00]>
 
+        Day-of-the-week relative shifting can use either Python's weekday numbers
+        (Monday = 0, Tuesday = 1 .. Sunday = 6) or using dateutil.relativedelta's
+        day instances (MO, TU .. SU).  When using weekday numbers, the returned
+        date will always be greater than or equal to the starting date.
+
+        Using the above code (which is a Saturday) and asking it to shift to Saturday:
+
+        >>> arw.shift(weekday=5)
+        <Arrow [2013-05-11T22:27:34.787885+00:00]>
+
+        While asking for a Monday:
+
+        >>> arw.shift(weekday=0)
+        <Arrow [2013-05-13T22:27:34.787885+00:00]>
+
         '''
 
         relative_kwargs = {}
 
         for key, value in kwargs.items():
 
-            if key in self._ATTRS_PLURAL or key in ['weeks', 'quarters']:
+            if key in self._ATTRS_PLURAL or key in ['weeks', 'quarters', 'weekday']:
                 relative_kwargs[key] = value
             else:
                 raise AttributeError()


Test patch:
diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py
index 65022f9..a4ddea2 100644
--- a/tests/arrow_tests.py
+++ b/tests/arrow_tests.py
@@ -6,6 +6,7 @@ from chai import Chai
 
 from datetime import date, datetime, timedelta
 from dateutil import tz
+from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU
 import simplejson as json
 import warnings
 import calendar
@@ -592,6 +593,48 @@ class ArrowShiftTests(Chai):
         assertEqual(arw.shift(seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46))
         assertEqual(arw.shift(microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1))
 
+        # Remember: Python's weekday 0 is Monday
+        assertEqual(arw.shift(weekday=0), arrow.Arrow(2013, 5, 6, 12, 30, 45))
+        assertEqual(arw.shift(weekday=1), arrow.Arrow(2013, 5, 7, 12, 30, 45))
+        assertEqual(arw.shift(weekday=2), arrow.Arrow(2013, 5, 8, 12, 30, 45))
+        assertEqual(arw.shift(weekday=3), arrow.Arrow(2013, 5, 9, 12, 30, 45))
+        assertEqual(arw.shift(weekday=4), arrow.Arrow(2013, 5, 10, 12, 30, 45))
+        assertEqual(arw.shift(weekday=5), arrow.Arrow(2013, 5, 11, 12, 30, 45))
+        assertEqual(arw.shift(weekday=6), arw)
+
+        with assertRaises(IndexError):
+            arw.shift(weekday=7)
+
+        # Use dateutil.relativedelta's convenient day instances
+        assertEqual(arw.shift(weekday=MO), arrow.Arrow(2013, 5, 6, 12, 30, 45))
+        assertEqual(arw.shift(weekday=MO(0)), arrow.Arrow(2013, 5, 6, 12, 30, 45))
+        assertEqual(arw.shift(weekday=MO(1)), arrow.Arrow(2013, 5, 6, 12, 30, 45))
+        assertEqual(arw.shift(weekday=MO(2)), arrow.Arrow(2013, 5, 13, 12, 30, 45))
+        assertEqual(arw.shift(weekday=TU), arrow.Arrow(2013, 5, 7, 12, 30, 45))
+        assertEqual(arw.shift(weekday=TU(0)), arrow.Arrow(2013, 5, 7, 12, 30, 45))
+        assertEqual(arw.shift(weekday=TU(1)), arrow.Arrow(2013, 5, 7, 12, 30, 45))
+        assertEqual(arw.shift(weekday=TU(2)), arrow.Arrow(2013, 5, 14, 12, 30, 45))
+        assertEqual(arw.shift(weekday=WE), arrow.Arrow(2013, 5, 8, 12, 30, 45))
+        assertEqual(arw.shift(weekday=WE(0)), arrow.Arrow(2013, 5, 8, 12, 30, 45))
+        assertEqual(arw.shift(weekday=WE(1)), arrow.Arrow(2013, 5, 8, 12, 30, 45))
+        assertEqual(arw.shift(weekday=WE(2)), arrow.Arrow(2013, 5, 15, 12, 30, 45))
+        assertEqual(arw.shift(weekday=TH), arrow.Arrow(2013, 5, 9, 12, 30, 45))
+        assertEqual(arw.shift(weekday=TH(0)), arrow.Arrow(2013, 5, 9, 12, 30, 45))
+        assertEqual(arw.shift(weekday=TH(1)), arrow.Arrow(2013, 5, 9, 12, 30, 45))
+        assertEqual(arw.shift(weekday=TH(2)), arrow.Arrow(2013, 5, 16, 12, 30, 45))
+        assertEqual(arw.shift(weekday=FR), arrow.Arrow(2013, 5, 10, 12, 30, 45))
+        assertEqual(arw.shift(weekday=FR(0)), arrow.Arrow(2013, 5, 10, 12, 30, 45))
+        assertEqual(arw.shift(weekday=FR(1)), arrow.Arrow(2013, 5, 10, 12, 30, 45))
+        assertEqual(arw.shift(weekday=FR(2)), arrow.Arrow(2013, 5, 17, 12, 30, 45))
+        assertEqual(arw.shift(weekday=SA), arrow.Arrow(2013, 5, 11, 12, 30, 45))
+        assertEqual(arw.shift(weekday=SA(0)), arrow.Arrow(2013, 5, 11, 12, 30, 45))
+        assertEqual(arw.shift(weekday=SA(1)), arrow.Arrow(2013, 5, 11, 12, 30, 45))
+        assertEqual(arw.shift(weekday=SA(2)), arrow.Arrow(2013, 5, 18, 12, 30, 45))
+        assertEqual(arw.shift(weekday=SU), arw)
+        assertEqual(arw.shift(weekday=SU(0)), arw)
+        assertEqual(arw.shift(weekday=SU(1)), arw)
+        assertEqual(arw.shift(weekday=SU(2)), arrow.Arrow(2013, 5, 12, 12, 30, 45))
+
     def test_shift_negative(self):
 
         arw = arrow.Arrow(2013, 5, 5, 12, 30, 45)
@@ -607,6 +650,27 @@ class ArrowShiftTests(Chai):
         assertEqual(arw.shift(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44))
         assertEqual(arw.shift(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999))
 
+        # Not sure how practical these negative weekdays are
+        assertEqual(arw.shift(weekday=-1), arw.shift(weekday=SU))
+        assertEqual(arw.shift(weekday=-2), arw.shift(weekday=SA))
+        assertEqual(arw.shift(weekday=-3), arw.shift(weekday=FR))
+        assertEqual(arw.shift(weekday=-4), arw.shift(weekday=TH))
+        assertEqual(arw.shift(weekday=-5), arw.shift(weekday=WE))
+        assertEqual(arw.shift(weekday=-6), arw.shift(weekday=TU))
+        assertEqual(arw.shift(weekday=-7), arw.shift(weekday=MO))
+
+        with assertRaises(IndexError):
+            arw.shift(weekday=-8)
+
+        assertEqual(arw.shift(weekday=MO(-1)), arrow.Arrow(2013, 4, 29, 12, 30, 45))
+        assertEqual(arw.shift(weekday=TU(-1)), arrow.Arrow(2013, 4, 30, 12, 30, 45))
+        assertEqual(arw.shift(weekday=WE(-1)), arrow.Arrow(2013, 5, 1, 12, 30, 45))
+        assertEqual(arw.shift(weekday=TH(-1)), arrow.Arrow(2013, 5, 2, 12, 30, 45))
+        assertEqual(arw.shift(weekday=FR(-1)), arrow.Arrow(2013, 5, 3, 12, 30, 45))
+        assertEqual(arw.shift(weekday=SA(-1)), arrow.Arrow(2013, 5, 4, 12, 30, 45))
+        assertEqual(arw.shift(weekday=SU(-1)), arw)
+        assertEqual(arw.shift(weekday=SU(-2)), arrow.Arrow(2013, 4, 28, 12, 30, 45))
+
     def test_shift_quarters_bug(self):
 
         arw = arrow.Arrow(2013, 5, 5, 12, 30, 45)