# Utility for Python date ranges

I'm trying to write something that can take a description and a date range and return all the dates that satisfy that description.

Descriptions can be anything from the following...

parsed_tests_dict = [
{"Saturday": [5]},
{"Sunday": [6]},
{"Weekend": [5, 6]},
{"Weekday": [0, 1, 2, 3, 4]},
{"Monday-Saturday": [0, 1, 2, 3, 4, 5]},
{"Jan": "", "Feb": "", "Mar": "", "Nov": "", "Dec": ""},
{"Jan-Feb": "", "Jan-Mar": "", "Jan-Apr":"", "Mar-Oct": "Apr-Oct", "May-Oct": "",
"Jul-Nov": "", "Nov-Feb": "", "Nov-Mar": "", "Nov-Dec": "", "Dec-Jun": ""}


So we could have a situation whereby Monday-Saturday appears, so the utility would bucket all the dates within the date range that fall between Monday-Saturday and then another bucket for all the dates that are Sunday.

Now there can also be a situation whereby, we need to bucket Jan-Mar, Apr-Oct, Nov-Dec AND Weekends - in this scenario, we would go through say 1-year date range, bucket every weekday that is between Jan-Mar, Apr-Oct and Nov-Dec and finally, all the Weekends in between would be bucketed under Weekends

I want to try to be as efficient and fast as possible and have been trying a method of this structure:

STR2WKDAY = {
'mon': 0, 'tue': 1, 'wed': 2, 'thu': 3, 'fri': 4, 'sat': 5, 'sun': 6,
'wkd': [0, 1, 2, 3, 4], 'wke': [5, 6], 'all': [0, 1, 2, 3, 4, 5, 6],
}
STR2MON = {
'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6,
'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12,
}


This is what I have so far, but I would be interested to see other solutions to this problem as mine feels a bit 'wrong' -

class DateRange(Database):

parsed_dates = []

def __init__(self, days, weekdays, months):
super().__init__()
self.days = str(days)
self.weekdays = frozenset(DateRange.flatn(weekdays))  # flat set of ints
self.months = frozenset(DateRange.flatn(months))  # flat set of ints

def __repr__(self):
return self.days

def accepts(self, dt):
return any([
dt.weekday() in self.weekdays,
dt.month in self.months,
])

@staticmethod
def get_days(description):
for days in description:
wkdays = list()
months = list()
for item in days:
item = item.strip().lower().replace('ee', '', 1)  # weekend,weekday => wke,wkd
if '-' not in item:
if item[:3] in Constants.STR2WKDAY:
wkdays.append(Constants.STR2WKDAY[item[:3]])
elif item[:3] in Constants.STR2MON:
months.append(Constants.STR2MON[item[:3]])
else:
f, t = item.split('-', 2)
f = f.strip()
t = t.strip()
if f[:3] in Constants.STR2WKDAY:
f = Constants.STR2WKDAY[f[:3]]
t = Constants.STR2WKDAY[t[:3]]
if f > t:
wkdays.append([i for i in range(t + 1)])
wkdays.append([i for i in range(f, 7)])
else:
wkdays.append([i for i in range(f, t + 1)])
elif f[:3] in Constants.STR2MON and t[:3] in Constants.STR2MON:
f = Constants.STR2MON[f[:3]]
t = Constants.STR2MON[t[:3]]
if f > t:
months.append([i for i in range(t + 1)])
months.append([i for i in range(f, 13)])
else:
months.append([i for i in range(f, t + 1)])

DateRange.parsed_dates.append(DateRange(days[0], wkdays, months))

@staticmethod
def flatn(array):
if isinstance(array, list):
for element in array:
yield from DateRange.flatn(element)
else:
yield (array)

@staticmethod
def date_range(from_date, to_date):
for n in range((to_date - from_date).days + 1):
yield from_date + timedelta(n)


Then calling this I just pass a Numpy array of descriptions atm to get_days() - I.E:

[['Saturdays'], ['Sundays'], ['Weekdays']]

This will then bucket every date that falls into each description. - This is long, so thanks to anyone who has a go and I appreciate any advice as I'm fairly new to programming.

## Example usage

from datetime import timedelta, datetime
from collections import defaultdict

class Constants:
STR2WKDAY = {
'mon': 0, 'tue': 1, 'wed': 2, 'thu': 3, 'fri': 4, 'sat': 5, 'sun': 6,
'wkd': [0, 1, 2, 3, 4], 'wke': [5, 6], 'all': [0, 1, 2, 3, 4, 5, 6],
}
STR2MON = {
'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6,
'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12,
}

def usage(start_date, end_date, description):
active_dates_dict = defaultdict(list)
# populate the list in DateRange with dates that match the description for given date range
DateRange.active_days(description)
for date in DateRange.parsed_dates:
# iterate over the dates and organise them into buckets that match the description
for dt in [dt for dt in DateRange.date_range(datetime.strptime(start_date, '%Y-%m-%d').date(),
datetime.strptime(end_date, '%Y-%m-%d').date())
if date.accepts(dt)]:
# add the datetime objects to a list of values with the description as the key
active_dates_dict[str(date)].append(str(dt.isoformat()))

return active_dates_dict

if __name__ == '__main__':
a = usage(start_date="2020-01-01", end_date="2020-01-04", description=[['Monday-Saturday']])
print(a)

'''

Possible descriptions - it's worth noting that you can have more than one of these descriptions at the same time

i.e     we could have Nov-Dec AND Weekend so we would need to bucket all dates that are from Nov-Dec that are also no weekends
and then the remaining dates would do in the Weekend bucket

Saturday
Sunday
Weekend
Weekday
Monday-Saturday
Jan
Feb
Mar
Nov
Dec
Jan-Feb
Jan-Mar
Jan-Apr
Mar-Oct
Apr-Oct
May-Oct
Jul-Nov
Nov-Feb
Nov-Mar
Nov-Dec
Dec-Jun

Not supported yet (Not sure how) - nice to have but not a Must have

3rd Nov - Dec
9th Mar - 2nd Nov
Jan-8th Mar
3rd Nov-8th Mar
'''


Any improvements are appreciated

• What is the Database in class DateRange(Database)? Is it your custom class too, or provided by an external library? Can I get it from somewhere to run the code locally? – Kacper Floriański Mar 4 '20 at 20:59
• @Bob your recent edits (e.g. today, yesterday)have been rejected because we cannot assume you have the same identity as the OP. If that is the case please click the “contact [us]” link at the bottom of the page and use to form to request an account merge. – Sᴀᴍ Onᴇᴌᴀ Mar 6 '20 at 14:51

The pandas library has various functions for creating, manipulating and selecting date ranges. For example, date_range(start, stop) creates a range of dates. bdate_range(start, stop, ...) can take a mask to only create certain days of the week, and can skip holidays.

One way to create your example date range:

import pandas as pd

a = pd.bdate_range(start="2020-01-01", end="2020-01-04", freq='C', weekmask='Mon Tue Wed Thu Fri Sat')


Another method might be to create the dates and then select the dates matching the description. Something like:

# create a range of dates
a = pd.date_range(start="2020-01-01", end="2020-12-31")

# select dates according to a description
weekday = a.dayofweek < 5

jan_to_mar = a.month.isin([0,1,2])
apr_to_oct = a.month.isin([3,4,5,6,7,8,9])
nov_to_dec = a.month.isin([10,11])

weekdays_in_jan_to_mar = a[weekday & jan_to_mar]
weekdays_in_apr_to_oct = a[weekday & apr_to_oct]
weekdays_in_nov_to_dec = a[weekday & nov_to_dec]

weekends = a[~weekday]


This later method could be generalized along the lines of get_days() to put the desired values in the call to a.month.isin() (or a.dayofweek.isin()).

DAYOFWEEK = dict(zip('mon tue wed thu fri sat sun'.split(), range(7)))
MONTH = dict(zip("jan feb mar apr may jun jul aug sep oct nov dec".split(), range(1, 13)))

def get_buckets(start, end, descriptions):
dates = pd.date_range(start, end)
buckets = []

for description in descriptions:

description = description.strip().lower().split('-')
start = description[0][:3]
end = start if len(description) == 1 else description[1][:3]

if start in DAYOFWEEK:
start = DAYOFWEEK[start]
end = DAYOFWEEK[end]
span = 0, len(DAYOFWEEK)
selector = dates.dayofweek

else:
start = MONTH[start]
end = MONTH[end]
span = 1, len(MONTH) + 1
selector = dates.month

if start <= end:
rng = set(range(start, end + 1))
else:
rng = set(range(*span)) - set(range(start - 1, end, - 1))

buckets.append(dates[selector.isin(rng)])

return buckets


By the way, the description of the problem implies that the buckets are mutually exclusive, but the code does not behave that way. get_dates() and usage() will both put a date into multiple buckets