4
\$\begingroup\$

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

\$\endgroup\$
2
  • 2
    \$\begingroup\$ 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? \$\endgroup\$ – Kacper Floriański Mar 4 '20 at 20:59
  • \$\begingroup\$ @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. \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Mar 6 '20 at 14:51
1
\$\begingroup\$

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

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.