nth weekday calculation in Python - whats wrong with this code?

Viewed 7306

I'm trying to calculate the nth weekday for a given date. For example, I should be able to calculate the 3rd wednesday in the month for a given date.

I have written 2 versions of a function that is supposed to do that:

from datetime import datetime, timedelta

### version 1
def nth_weekday(the_date, nth_week, week_day):
    temp = the_date.replace(day=1)
    adj = (nth_week-1)*7 + temp.weekday()-week_day
    return temp + timedelta(days=adj)

### version 2
def nth_weekday(the_date, nth_week, week_day):
    temp = the_date.replace(day=1)
    adj = temp.weekday()-week_day
    temp += timedelta(days=adj)
    temp += timedelta(weeks=nth_week)
    return temp

Console output

# Calculate the 3rd Friday for the date 2011-08-09
x=nth_weekday(datetime(year=2011,month=8,day=9),3,4)
print 'output:',x.strftime('%d%b%y') 

# output: 11Aug11 (Expected: '19Aug11')

The logic in both functions is obviously wrong, but I can't seem to locate the bug - can anyone spot what is wrong with the code - and how do I fix it to return the correct value?

8 Answers

The problem with the one-liner with the most votes is it doesn't work.

It can however be used as a basis for refinement:

You see this is what you get:

c = calendar.Calendar(calendar.SUNDAY).monthdatescalendar(2018, 7)
for c2 in c:
    print(c2[0])

2018-07-01
2018-07-08
2018-07-15
2018-07-22
2018-07-29

c = calendar.Calendar(calendar.SUNDAY).monthdatescalendar(2018, 8)
for c2 in c:
    print(c2[0])

2018-07-29
2018-08-05
2018-08-12
2018-08-19
2018-08-26

If you think about it it's trying to organise the calendars into nested lists to print a weeks worth of dates at a time. So stragglers from other months come into play. By using a new list of valid days that fall in the month - this does the trick.


Answer with appended list

import calendar
import datetime
def get_nth_DOW_for_YY_MM(dow, yy, mm, nth) -> datetime.date:
    #dow - Python Cal - 6 Sun 0 Mon ...  5 Sat
    #nth is 1 based... -1. is ok for last.
    i = -1 if nth == -1 or nth == 5 else nth -1
    valid_days = []
    for d in calendar.Calendar(dow).monthdatescalendar(yy, mm):
        if d[0].month == mm:
            valid_days.append(d[0])
    return valid_days[i]

So here's how it could be called:

firstSundayInJuly2018 = get_nth_DOW_for_YY_MM(calendar.SUNDAY, 2018, 7, 1)
firstSundayInAugust2018 = get_nth_DOW_for_YY_MM(calendar.SUNDAY, 2018, 8, 1)
print(firstSundayInJuly2018)
print(firstSundayInAugust2018)

And here is the output:

2018-07-01 
2018-08-05

get_nth_DOW_for_YY_MM() can be refactored using lambda expressions like so:

Answer with lambda expression refactoring

import calendar
import datetime
def get_nth_DOW_for_YY_MM(dow, yy, mm, nth) -> datetime.date:
    #dow - Python Cal - 6 Sun 0 Mon ...  5 Sat
    #nth is 1 based... -1. is ok for last.
    i = -1 if nth == -1 or nth == 5 else nth -1
    return list(filter(lambda x: x.month == mm, \
          list(map(lambda x: x[0], \ 
            calendar.Calendar(dow).monthdatescalendar(yy, mm) \
          )) \
        ))[i]

The one-liner answer does not seem to work if the target day falls on the first of the month. For instance, if you want the 2nd Friday of every month, then the one-liner approach

calendar.Calendar(4).monthdatescalendar(year, month)[2][0]

for March 2013 will return March 15th 2013 when it should be March 8th 2013. Perhaps add in a check like

if date(year, month, 1).weekday() == x:
    delivery_date.append(calendar.Calendar(x).monthdatescalendar(year, month)[n-1][0])
else:
    delivery_date.append(calendar.Calendar(x).monthdatescalendar(year, month)[n][0])

Alternatively this will work for Python 2, returns the occurance of weekday in the said month, i.e if 16 June 2018 is the input, then returns the occurance of the day on 16th June 2018

You may substitute the month/year/date integers to anything you might want - right now it's getting the input / date from the system via datetime

Omit out print statements or use pass where they're not needed

import calendar
import datetime
import pprint

month_number = int(datetime.datetime.now().strftime('%m'))
year_number = int(datetime.datetime.now().strftime('%Y'))
date_number = int(datetime.datetime.now().strftime('%d'))
day_ofweek = str(datetime.datetime.now().strftime('%A'))


def weekday_occurance():
print "\nFinding current date here\n"
for week in xrange(5):
    try:
        calendar.monthcalendar(year_number, month_number)[week].index(date_number)
        occurance = week + 1
        print "Date %s of month %s and year %s is %s #%s in this month." % (date_number,month_number,year_number,day_ofweek,occurance)
        return occurance
        break
    except ValueError as e:
        print "The date specified is %s which is week %s" % (e,week)



myocc = weekday_occurance()
print myocc

A little tweak would make the one-liner work correctly:

import calendar
calendar.Calendar((weekday+1)%7).monthdatescalendar(year, month)[n_th][-1]

Here n_th should be interpreted as c-style, e.g. 0 is the first index.

Example: to find 1st Sunday in July 2018 one could type:

>>> calendar.Calendar(0).monthdatescalendar(2018, 7)[0][-1]
datetime.date(2018, 7, 1)

People here seem to like one-liner, I will propose below.

import calendar
[cal[0] for cal in calendar.Calendar(x).monthdatescalendar(year, month) if cal[0].month == month][n]

The relativedelta module that's an extension from the Python dateutil package (pip install python-dateutil) does exactly what you want:

from dateutil import relativedelta
import datetime


def nth_weekday(the_date, nth_week, week_day):
    return the_date.replace(day=1) + relativedelta.relativedelta(
        weekday=week_day(nth_week)
    )

print(nth_weekday(datetime.date.today(), 3, relativedelta.FR))

The key part here evaluates to weekday=relativedelta.FR(3): the third Friday of the month. Here are the relevant part of the docs for the weekday parameter,

weekday:

One of the weekday instances (MO, TU, etc) available in the relativedelta module. These instances may receive a parameter N, specifying the Nth weekday, which could be positive or negative (like MO(+1) or MO(-2)).

Related