I would like to display a calendar with week numbers.
Python provides a nice calendar
module that can generate such a calendar, but I can’t find a way to display week numbers.
Here is an example of the command and its output:
python -m calendar --locale fr --encoding utf-8 --type text 2024
2024
janvier février mars
Lu Ma Me Je Ve Sa Di Lu Ma Me Je Ve Sa Di Lu Ma Me Je Ve Sa Di
1 2 3 4 5 6 7 1 2 3 4 1 2 3
8 9 10 11 12 13 14 5 6 7 8 9 10 11 4 5 6 7 8 9 10
15 16 17 18 19 20 21 12 13 14 15 16 17 18 11 12 13 14 15 16 17
22 23 24 25 26 27 28 19 20 21 22 23 24 25 18 19 20 21 22 23 24
29 30 31 26 27 28 29 25 26 27 28 29 30 31
And here is what I would like to achieve:
2024
janvier février mars
Lu Ma Me Je Ve Sa Di Lu Ma Me Je Ve Sa Di Lu Ma Me Je Ve Sa Di
1 1 2 3 4 5 6 7 5 1 2 3 4 9 1 2 3
2 8 9 10 11 12 13 14 6 5 6 7 8 9 10 11 10 4 5 6 7 8 9 10
3 15 16 17 18 19 20 21 7 12 13 14 15 16 17 18 11 11 12 13 14 15 16 17
4 22 23 24 25 26 27 28 8 19 20 21 22 23 24 25 12 18 19 20 21 22 23 24
5 29 30 31 9 26 27 28 29 13 25 26 27 28 29 30 31
Is it possible to obtain this result with the Python calendar module? If it is not directly possible, how can I extend the module to get this new feature?
I’m open to a totally different approach if it makes sense.
I subclassed TextCalendar
(the original is at https://github.com/python/cpython/blob/3.12/Lib/calendar.py ) and modified 3 methods:
formatmonth
, so that it passes the week number in addition to the
week to formatweek
formatweek
now prints this week number
before the daysformatheader
in order to shift the headers to
accomodate the new fieldfrom calendar import TextCalendar, LocaleTextCalendar
from datetime import date
class WeekNumTextCalendar(TextCalendar):
def formatweekheader(self, width):
"""
Return a header for a week, shifted for the weeknum field.
"""
# shift by 5 characters
return ' '*5 + super().formatweekheader(width)
def formatweek(self, theweek, width, weeknum):
"Prints weeknum before the days of the week"
days = super().formatweek(theweek, width)
return f'{weeknum:>2} | ' + days
def formatmonth(self, theyear, themonth, w=0, l=0):
"""
Return a month's calendar string (multi-line).
Passes an additional argument weeknum to formatweek
"""
w = max(2, w)
l = max(1, l)
s = self.formatmonthname(theyear, themonth, 7 * (w + 1) - 1)
s = s.rstrip()
s += '\n' * l
s += self.formatweekheader(w).rstrip()
s += '\n' * l
for week in self.monthdays2calendar(theyear, themonth):
# week is a list of (day, weekday), where day=0 if not in the current month
# we get the first non-zero dat of the current week
day = next(day for (day, weekday) in week if day!=0)
# isocalendar returns a named tuple (year, week, weekday)
weeknum = date(theyear, themonth, day).isocalendar().week
s += self.formatweek(week, w, weeknum).rstrip()
s += '\n' * l
return s
Sample run:
c = WeekNumTextCalendar()
c.prmonth(2024, 3)
March 2024
Mo Tu We Th Fr Sa Su
9 | 1 2 3
10 | 4 5 6 7 8 9 10
11 | 11 12 13 14 15 16 17
12 | 18 19 20 21 22 23 24
13 | 25 26 27 28 29 30 31
In order to get localized versions, you can do:
class LocaleWeekNumCalendar(WeekNumTextCalendar, LocaleTextCalendar):
"Localized version of WeekNumCalendar"
pass
c = LocaleWeekNumCalendar()
c.prmonth(2024, 3)
mars 2024
lu ma me je ve sa di
9 | 1 2 3
10 | 4 5 6 7 8 9 10
11 | 11 12 13 14 15 16 17
12 | 18 19 20 21 22 23 24
13 | 25 26 27 28 29 30 31
Note that you would still have to update formatyear
in order to print calendars for a whole year.