0

I need to arrive at the date given the nth weekday (w) of a month (m) and year (Y). using the code below python always assigns the day of the month as the 1st.

from datetime import datetime
from time import mktime, strptime
def meetup_day(year,month,day_name,week_num):
    args = (str(year),str(month),day_name)
    time_code = strptime(" ".join(args), "%Y %m %A")
    return datetime.fromtimestamp(mktime(time_code))

when I print out time_code to debug why mktime always returns the date as (Y,m,1,0,0) it returns the time tuple with the value for tm_mday=1, but the value for tm_wday is correct. Why doesn't python notice the fact that the first of the month doesn't always fall on the weekday specified?

time.struct_time(tm_year=2013, tm_mon=5, tm_mday=1, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=1, tm_yday=121, tm_isdst=-1)

derived from using ('2013', '5', 'Tuesday') as input for args

For reference, I understand that there are several of each weekday during the month, but figured Python would default to the first week where encountering specified weekday.

  • Some pertinent information that I should add: 1) week_num is the number within the month, not the year 2) week_num can take an ordinal form (1st, 2nd, etc), making it easy to parse, as well as 'last', and 'teenth' for the 7 days 13-19. – Px_Overflow Oct 05 '14 at 02:25
  • I've updated [my answer](http://stackoverflow.com/a/26197514/3903832) to address that. Please take a look at it. – Yoel Oct 06 '14 at 19:52

3 Answers3

0

Parsing the weekday without a day-of-the-month or weeknumber never works, not in Python, and not in C either.

This is not all that clear from the datetime documentation, but the closely related %U and %W formats have this footnote:

When used with the strptime() function, %U and %W are only used in calculations when the day of the week and the year are specified.

In practice this means it applies the other way around too; specifying a day of the week is meaningless without the week number.

If you are parsing just the day of the week you need to handle this in code:

from datetime import datetime, timedelta

def meetup_day(year, month, day_name, week_num):
    weekdays = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
    weekday = weekdays.index(day_name.lower())
    dt = datetime(year, month, 1)
    return dt + timedelta(days=(weekday - dt.weekday()) + (weekday < dt.weekday()) * 7)

This'll return the first date with that weekday in the month.

Or include the week number:

def meetup_day(year, month, day_name, week_num):
    return datetime.strptime(' '.join(map(str, (year, month, day_name, week_num))),
                             '%Y %m %A %U')

Either way, you get:

>>> meetup_day(2014, 10, 'tuesday', 40)
datetime.datetime(2014, 10, 7, 0, 0)
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
0

@MartijnPieters's answer shows how this can be done with the datetime module. An alternative solution would be using the calendar module as well, which allows you to specify the required week_num within the month more easily (to my understanding, that's the input you have), as follows:

import datetime
import calendar

def meetup_day(year, month, day_name, week_num):
    year_int = int(year)
    month_int = int(month)
    day_int = list(calendar.day_name).index(day_name)
    relevant_mdays = [i for i in zip(*calendar.monthcalendar(year_int, month_int))[day_int] if i != 0]
    return datetime.date(year_int, month_int, relevant_mdays[week_num])

Execution example:

In [5]: meetup_day('2013', '5', 'Tuesday', 0)
Out[5]: datetime.date(2013, 5, 7)

In [6]: meetup_day('2013', '5', 'Tuesday', 1)
Out[6]: datetime.date(2013, 5, 14)

Explanation:

list(calendar.day_name).index(day_name) converts the day_name to a number:

In [9]: list(calendar.day_name).index(day_name)
Out[9]: 1

calendar.monthcalendar(year_int, month_int) returns a matrix representing a month’s calendar:

In [14]: calendar.monthcalendar(year_int, month_int)
Out[14]: 
[[0, 0, 1, 2, 3, 4, 5],
 [6, 7, 8, 9, 10, 11, 12],
 [13, 14, 15, 16, 17, 18, 19],
 [20, 21, 22, 23, 24, 25, 26],
 [27, 28, 29, 30, 31, 0, 0]]

zip(*_) on the returned value, transposes that matrix:

In [15]: zip(*_)
Out[15]: 
[(0, 6, 13, 20, 27),
 (0, 7, 14, 21, 28),
 (1, 8, 15, 22, 29),
 (2, 9, 16, 23, 30),
 (3, 10, 17, 24, 31),
 (4, 11, 18, 25, 0),
 (5, 12, 19, 26, 0)]

That allows us to grab just the days in the month of the relevant weekday:

In [21]: _[day_int]
Out[21]: (0, 7, 14, 21, 28)

Now we just need to get rid of the first and\or last instance if that weekday isn't available on the first and\or last week of the month:

In [27]: [i for i in _ if i != 0]
Out[27]: [7, 14, 21, 28]

Next, we extract the relevant day of the month:

In [30]: _[1]
Out[30]: 14

Lastly, we get a datetime object:

In [33]: datetime.date(year_int, month_int, _)
Out[33]: datetime.date(2013, 5, 14)

All of the above can also be implemented as a one-liner:

In [35]: datetime.date(int(year), int(month), [i for i in zip(*calendar.monthcalendar(int(year), int(month)))[list(calendar.day_name).index(day_name)] if i != 0][week_num])
Out[35]: datetime.date(2013, 5, 14)

EDIT:

Following @Px_Overflow's comment regarding the format of week_num, here is an updated solution:

def meetup_day(year, month, day_name, week_num):
    year_int = int(year)
    month_int = int(month)
    day_int = list(calendar.day_name).index(day_name)
    relevant_mdays = [i for i in zip(*calendar.monthcalendar(year_int, month_int))[day_int] if i != 0]
    if week_num == 'teenth':
        mday = next((i for i in relevant_mdays if i in range(13, 20)), None)
    else:
        if week_num == 'last':
            week_int = -1
        else:
            week_int = int(week_num[0]) - 1
        mday = relevant_mdays[week_int]
    return datetime.date(year_int, month_int, mday)

Execution example:

In [10]: meetup_day('2013', '5', 'Tuesday', '1st')
Out[10]: datetime.date(2013, 5, 7)

In [11]: meetup_day('2013', '5', 'Tuesday', '2nd')
Out[11]: datetime.date(2013, 5, 14)

In [12]: meetup_day('2013', '5', 'Tuesday', '3rd')
Out[12]: datetime.date(2013, 5, 21)

In [13]: meetup_day('2013', '5', 'Tuesday', '4th')
Out[13]: datetime.date(2013, 5, 28)

In [14]: meetup_day('2013', '5', 'Tuesday', 'teenth')
Out[14]: datetime.date(2013, 5, 14)

In [15]: meetup_day('2013', '5', 'Tuesday', 'last')
Out[15]: datetime.date(2013, 5, 28)

Explanation:

The only addition is the calculation of mday in the if clause. It's pretty straight-forward, perhaps except the case of if week_day == teenth. When this occurs, the first common element from relevant_mdays and range(13, 20) is returned.

Conversion to Python 3:

As @Px_Overflow has now clarified he is working on Python 3, the function needs a minor change since in Python 3 zip is no longer subscriptable, as it is a generator. Wrapping the zip call in a list call converts its output to a list, and thus solves our problem:

def meetup_day(year, month, day_name, week_num):
    year_int = int(year)
    month_int = int(month)
    day_int = list(calendar.day_name).index(day_name)
    relevant_mdays = [i for i in list(zip(*calendar.monthcalendar(year_int, month_int)))[day_int] if i != 0]
    if week_num == 'teenth':
        mday = next((i for i in relevant_mdays if i in range(13, 20)), None)
    else:
        if week_num == 'last':
            week_int = -1
        else:
            week_int = int(week_num[0]) - 1
        mday = relevant_mdays[week_int]
    return datetime.date(year_int, month_int, mday)
Community
  • 1
  • 1
Yoel
  • 9,144
  • 7
  • 42
  • 57
  • Thank you. This works well with a tweak. zip isn't subscriptable (had to look that one up) so I moved the [day_int] index to the end of the relevant_mdays assignment. There are 2 example problems on which this didn't work; (2013,5,Tuesday,1st) and (2013,9,Thursday,3rd). I'll try to respond soon with a solution. – Px_Overflow Oct 07 '14 at 02:58
  • Oh, I had no idea you're working on Python 3 as it is not stated anywhere. I added a fix to solve your problem. This should work on all valid inputs. – Yoel Oct 07 '14 at 09:19
  • @Px_Overflow, if this answer solved your problem, please consider [accepting](http://meta.stackexchange.com/q/5234) it by clicking the check-mark that is to the left of the answer. Note that you may accept only one answer and that there is absolutely no obligation to do this. If you are still facing the same issue, please comment and describe what's wrong. – Yoel Oct 08 '14 at 00:20
  • That 2 to 3 fix worked. Thank you for your help. I learned much more about Python than just the solution for this problem. – Px_Overflow Oct 09 '14 at 00:49
0

Your input for strptime is ambiguous. In accordance with Python Zen, koan 12, the part that's ambiguous is ignored. Arguably, a better way of action would be to throw an exception.

ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152