Nick Miller
Nick Miller

Reputation: 29

Python2: Retrieve Sunday - Saturday Week Start/End Dates For Given Date Range

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

Answers (2)

Paul
Paul

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

matthew.
matthew.

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

Related Questions