Reputation: 29
There are many posts that address similar issues, but none of them had the same constraints that I have with my problem.
I’m writing a script that fetches any number of weeks’ worth of data from a data center. Which weeks it fetches depends on the date range provided to my script by an outside user. The data center’s week runs from Sunday to Saturday. Python’s week runs from Monday to Sunday.
I need to be able to get the dates for the Sunday before and the Saturday after each date in the date range. To complicate matters, neither the week start date nor the week end date can fall outside of the requested range. This prevents me from simply subtracting a day from each date in the range.
Some example scenarios:
Example 1)
requested_date_range = [datetime(2016,7,1,0,0),datetime(2016,8,5,0,0)]
what I get from the various Python utilities (dateutil, datetime_periods, etc):
[
[datetime(2016,6,27,0,0),datetime(2016,7,3,0,0)],
[datetime(2016,7,4,0,0),datetime(2016,7,10,0,0)],
[datetime(2016,7,11,0,0),datetime(2016,7,17,0,0)],
[datetime(2016,7,18,0,0),datetime(2016,7,24,0,0)],
[datetime(2016,7,25,0,0),datetime(2016,7,31,0,0)],
[datetime(2016,8,1,0,0),datetime(2016,8,7,0,0)]
]
what I actually need:
[
[datetime(2016,7,1,0,0),datetime(2016,7,2,0,0)], #"week" starts on first day of requested range and ends on the following Saturday
[datetime(2016,7,3,0,0),datetime(2016,7,9,0,0)], #Sunday through Saturday
[datetime(2016,7,10,0,0),datetime(2016,7,16,0,0)], #Sunday through Saturday
[datetime(2016,7,17,0,0),datetime(2016,7,23,0,0)], #Sunday through Saturday
[datetime(2016,7,24,0,0),datetime(2016,7,30,0,0)], #Sunday through Saturday
[datetime(2016,7,31,0,0),datetime(2016,8,5,0,0)] #"week" starts on Sunday and ends on last day of requested range
]
Example 2)
requested_date_range = [datetime(2016,7,3,0,0),datetime(2016,8,7,0,0)]
what I get from the various Python utilities (dateutil, datetime_periods, etc):
[
[datetime(2016,6,27,0,0),datetime(2016,7,3,0,0)],
[datetime(2016,7,4,0,0),datetime(2016,7,10,0,0)],
[datetime(2016,7,11,0,0),datetime(2016,7,17,0,0)],
[datetime(2016,7,18,0,0),datetime(2016,7,24,0,0)],
[datetime(2016,7,25,0,0),datetime(2016,7,31,0,0)],
[datetime(2016,8,1,0,0),datetime(2016,8,7,0,0)]
]
what I actually need:
[
[datetime(2016,7,3,0,0),datetime(2016,7,9,0,0)], #"week" starts on first day of requested range
[datetime(2016,7,10,0,0),datetime(2016,7,16,0,0)], #Sunday through Saturday
[datetime(2016,7,17,0,0),datetime(2016,7,23,0,0)], #Sunday through Saturday
[datetime(2016,7,24,0,0),datetime(2016,7,30,0,0)], #Sunday through Saturday
[datetime(2016,7,31,0,0),datetime(2016,8,6,0,0)], #Sunday through Saturday
[datetime(2016,8,7,0,0),datetime(2016,8,7,0,0)] #"week" ends up being only one day long because the max requested date falls on a Sunday
]
Upvotes: 2
Views: 814
Reputation: 10863
You should be able to do this pretty easily using dateutil.relativedelta
. An example function below:
from dateutil.relativedelta import relativedelta
from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU
def week_range(range_start, range_end):
dts = []
WEEK_START = relativedelta(weekday=SU(+2))
WEEK_END = relativedelta(weekday=SA)
c_wstart = range_start + relativedelta(weekday=SU(+1))
c_wend = c_wstart + WEEK_END
if range_start < c_wstart:
dts.append((range_start, range_start + WEEK_END))
while True:
if c_wend > range_end:
c_wend = range_end
dts.append((c_wstart, c_wend))
if c_wend >= range_end:
break
c_wstart = c_wstart + WEEK_START
c_wend = c_wstart + WEEK_END
if c_wstart > range_end:
break
return dts
In the above function, I first take the range beginning and add relativedelta(weekday=SU)
to it, which gives me the first Sunday on or after the original date. I then consecutively add relativedelta(weekday=SU(+2))
to the "current week" to get the second Sunday on or after the current date (which, since my "week start" is always a Sunday, is always the next Sunday).
For each date I generate, I just add relativedelta(weekday=SA)
to it to generate the coming Saturday, and if I'm outside the date range, I "clip" the last date to be the date range.
Using your examples:
>>> week_range(datetime(2016, 7, 1), datetime(2016, 8, 5))
[(datetime.datetime(2016, 7, 1, 0, 0), datetime.datetime(2016, 7, 2, 0, 0)),
(datetime.datetime(2016, 7, 3, 0, 0), datetime.datetime(2016, 7, 9, 0, 0)),
(datetime.datetime(2016, 7, 10, 0, 0), datetime.datetime(2016, 7, 16, 0, 0)),
(datetime.datetime(2016, 7, 17, 0, 0), datetime.datetime(2016, 7, 23, 0, 0)),
(datetime.datetime(2016, 7, 24, 0, 0), datetime.datetime(2016, 7, 30, 0, 0)),
(datetime.datetime(2016, 7, 31, 0, 0), datetime.datetime(2016, 8, 5, 0, 0))]
>>> week_range(datetime(2016, 7, 3), datetime(2016, 8, 7))
[(datetime.datetime(2016, 7, 3, 0, 0), datetime.datetime(2016, 7, 9, 0, 0)),
(datetime.datetime(2016, 7, 10, 0, 0), datetime.datetime(2016, 7, 16, 0, 0)),
(datetime.datetime(2016, 7, 17, 0, 0), datetime.datetime(2016, 7, 23, 0, 0)),
(datetime.datetime(2016, 7, 24, 0, 0), datetime.datetime(2016, 7, 30, 0, 0)),
(datetime.datetime(2016, 7, 31, 0, 0), datetime.datetime(2016, 8, 6, 0, 0)),
(datetime.datetime(2016, 8, 7, 0, 0), datetime.datetime(2016, 8, 7, 0, 0))]
Depending on your taste, you can also accomplish something similar using an rruleset
:
from dateutil.rrule import rrule, rruleset
from dateutil.rrule import WEEKLY, SU, SA
from datetime import timedelta
from itertools import zip_longest, chain
def week_range_rrule(range_start, range_end, weekday_start=SU, weekday_end=SA):
# Beginning of the week rule
rr1 = rrule(WEEKLY, byweekday=weekday_start,
dtstart=range_start, until=range_end)
# End of the week rule - adding 1 second to the range end because
# "until" isn't inclusive
rr2 = rrule(WEEKLY, byweekday=weekday_end,
dtstart=range_start+relativedelta(SA),
until=range_end+timedelta(seconds=1))
# Combine these into a rule set
rrs = rruleset()
rrs.rrule(rr1)
rrs.rrule(rr2)
# Explicitly add range start and end to the rules, in case they don't
# fall on neat week boundaries
rrs.rdate(range_start)
rrs.rdate(range_end)
if next(iter(rr2)) == range_start:
rrs = chain((range_start, ), rrs)
# Modified version of the "grouper" recipe from itertools
args = [iter(rrs)] * 2
return list(zip_longest(*args, fillvalue=range_end))
Note that if you want the first one to be lazy, just replace all instances of dts.append(x)
with yield x
. If you want the second one to be lazy, just remove the list()
wrapper around the zip_longest
in the return statement.
Upvotes: 1
Reputation: 176
Here's a slightly less verbose, albeit terse answer.
import datetime as dt
if __name__ == "__main__":
weekend_index = (6, 5) # Sunday, Saturday
requested_range = (dt.datetime(2016, 7, 9, 0, 0), dt.datetime(2016, 8, 11, 0, 0))
start, end = requested_range
sun, sat = weekend_index
cur = start
my_range = []
while cur < end:
cr = []
cr.append(cur)
cur = end if end < cur+dt.timedelta(days=6) else (cur+dt.timedelta(days=(sun if cur.weekday() == sun else (sat-cur.weekday()))))
cr.append(cur)
cur += dt.timedelta(days=1)
my_range.append(cr)
print(my_range) # Returns:
# [[datetime.datetime(2016, 7, 9, 0, 0), datetime.datetime(2016, 7, 9, 0, 0)],
# [datetime.datetime(2016, 7, 10, 0, 0), datetime.datetime(2016, 7, 16, 0, 0)],
# [datetime.datetime(2016, 7, 17, 0, 0), datetime.datetime(2016, 7, 23, 0, 0)],
# [datetime.datetime(2016, 7, 24, 0, 0), datetime.datetime(2016, 7, 30, 0, 0)],
# [datetime.datetime(2016, 7, 31, 0, 0), datetime.datetime(2016, 8, 6, 0, 0)],
# [datetime.datetime(2016, 8, 7, 0, 0), datetime.datetime(2016, 8, 11, 0, 0)]]
Upvotes: 1