@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)