Search code examples
pythoncalendar

Display calendar with week numbers


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.


Solution

  • 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 days
    • formatheader in order to shift the headers to accomodate the new field

    from 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.