Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 670
Homunculus Reticulli Avatar asked Aug 09 '12 12:08

Homunculus Reticulli


3 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]

like image 127
JGFMK Avatar answered Oct 25 '22 05:10

JGFMK


Your problem is here:

adj = temp.weekday()-week_day

First of all, you are subtracting things the wrong way: you need to subtract the actual day from the desired one, not the other way around.

Second, you need to ensure that the result of the subtraction is not negative - it should be put in the range 0-6 using % 7.

The result:

adj = (week_day - temp.weekday()) % 7

In addition, in your second version, you need to add nth_week-1 weeks like you do in your first version.

Complete example:

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

>>> nth_weekday(datetime(2011,8,9), 3, 4)
datetime.datetime(2011, 8, 19, 0, 0)
like image 39
interjay Avatar answered Oct 25 '22 05:10

interjay


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])
like image 21
Chris Avatar answered Oct 25 '22 05:10

Chris