questions re: calendar module

Peter Otten __peter__ at web.de
Sun Aug 2 10:42:19 EDT 2020


Richard Damon wrote:

> I would likely just build the formatter to start by assuming 6 week
> months, and then near the end, after stacking the side by side months,
> see if it can be trimmed out (easier to remove at the end then add if
> needed)

If you like some itertools gymnastics: you can format square 
boxes of text with two nested zip_longest():

import itertools

def gen_lines(blurbs, columncount, columnwidth):
    first = True
    for row in itertools.zip_longest(
            *[iter(blurbs)] * columncount,
            fillvalue=""
    ):
        if first:
            first = False
        else:
            yield ""
        for columns in itertools.zip_longest(
                *[blurb for blurb in row],
                fillvalue=""
        ):
            yield "  ".join(
                column.ljust(columnwidth) for column in columns
            ).rstrip()

BLURBS = [
    "aaa\aaaaaaaa\naaaaaaa",
    "bbbb\nbb\nbbbbbbb\nbb\nbbbbb",
    "ccc",
    "ddd\nddd"
]
BLURBS = [blurb.splitlines() for blurb in BLURBS]

for line in gen_lines(BLURBS, 2, 10):
    print(line)
print("\n")
for line in gen_lines(BLURBS, 3, 10):
    print(line)
print("\n")

As calendar provides formatted months with TextCalendar.formatmonth() 
you can easily feed that to gen_lines():

import calendar

def monthrange(start, stop):
    y, m = start
    start = y * 12 + m - 1
    y, m = stop
    stop = y * 12 + m - 1
    for ym0 in range(start, stop):
        y, m0 = divmod(ym0, 12)
        yield y, m+1

tc = calendar.TextCalendar()
months = (
    tc.formatmonth(*month).splitlines() for month in
    monthrange((2020, 10), (2021, 3))
)
for line in gen_lines(months, 3, 21):
    print(line)

However, I found reusing the building blocks from calendar to add week 
indices harder than expected. I ended up using brute force and am not 
really satisfied with the result. You can have a look:

$ cat print_cal.py
#!/usr/bin/python3
"""Print a calendar with an arbitrary number of months in parallel columns.
"""
import calendar
import datetime
import functools
import itertools


SEP_WIDTH = 4


def get_weeknumber(year, month, day=1):
    """Week of year for date (year, month, day).
    """
    return datetime.date(year, month, day).isocalendar()[1]


class MyTextCalendar(calendar.TextCalendar):
    """Tweak TextCalendar to prepend weeks with week number.
    """
    month_width = 24

    def weeknumber(self, year, month, day=1):
        """Week of year or calendar-specific week index for a given date.
        """
        return get_weeknumber(year, month, max(day, 1))

    def formatmonthname(self, theyear, themonth, width, withyear=True):
        return "   " + super().formatmonthname(
            theyear, themonth, width, withyear=withyear
        )

    def formatweekheader(self, width):
        return "   " + super().formatweekheader(width)

    def formatweek(self, theweek, width):
        week, theweek = theweek
        return "%2d " % week + ' '.join(
            self.formatday(d, wd, width) for (d, wd) in theweek
        )

    def monthdays2calendar(self, year, month):
        return [
            (self.weeknumber(year, month, week[0][0]), week)
            for week in super().monthdays2calendar(year, month)
        ]


class MyIndexedTextCalendar(MyTextCalendar):
    """Replace week number with an index.
    """
    def __init__(self, firstweekday=0):
        super().__init__(firstweekday)
        self.weekindices = itertools.count(1)

    @functools.lru_cache(maxsize=1)
    def get_index(self, weeknumber):
        """Convert the week number into an index.
        """
        return next(self.weekindices)

    def weeknumber(self, year, month, day=1):
        return self.get_index(super().weeknumber(year, month, day))


def monthindex(year, month):
    """Convert year, month to a single integer.

    >>> monthindex(2020, 3)
    24242

    >>> t = 2021, 7
    >>> t == monthtuple(monthindex(*t))
    True
    """
    return 12 * year + month - 1


def monthtuple(index):
    """Inverse of monthindex().
    """
    year, month0 = divmod(index, 12)
    return year, month0 + 1


def yearmonth(year_month):
    """Convert yyyy-mm to a (year, month) tuple.

    >>> yearmonth("2020-03")
    (2020, 3)
    """
    return tuple(map(int, year_month.split("-")))


def months(first, last):
    """Closed interval of months.

    >>> list(months((2020, 3), (2020, 5)))
    [(2020, 3), (2020, 4), (2020, 5)]
    """
    for monthnum in range(monthindex(*first), monthindex(*last)+1):
        yield monthtuple(monthnum)


def dump_calendar(first, last, months_per_row, cal=MyTextCalendar()):
    """Print calendar from `first` month to and including `last` month.

    >>> dump_calendar((2020, 11), (2021, 1), 2)
          November 2020               December 2020
       Mo Tu We Th Fr Sa Su        Mo Tu We Th Fr Sa Su
    44                    1     49     1  2  3  4  5  6
    45  2  3  4  5  6  7  8     50  7  8  9 10 11 12 13
    46  9 10 11 12 13 14 15     51 14 15 16 17 18 19 20
    47 16 17 18 19 20 21 22     52 21 22 23 24 25 26 27
    48 23 24 25 26 27 28 29     53 28 29 30 31
    49 30
    <BLANKLINE>
           January 2021
       Mo Tu We Th Fr Sa Su
    53              1  2  3
     1  4  5  6  7  8  9 10
     2 11 12 13 14 15 16 17
     3 18 19 20 21 22 23 24
     4 25 26 27 28 29 30 31
    <BLANKLINE>
    """
    for line in gen_calendar(first, last, months_per_row, cal):
        print(line)


def gen_calendar(first, last, months_per_row, cal, *, sep=" "*SEP_WIDTH):
    """Generate lines for calendar covering months from `first`
    including `last` with `months_per_row` in parallel.
    """
    month_blurbs = (cal.formatmonth(*month) for month in months(first, last))
    for month_row in itertools.zip_longest(
            *[month_blurbs] * months_per_row,
            fillvalue=""
    ):
        for columns in itertools.zip_longest(
                *[month.splitlines() for month in month_row],
                fillvalue=""):
            yield sep.join(
                column.ljust(cal.month_width) for column in columns
            ).rstrip()
        yield ""


def main():
    """Command line interface.
    """
    import argparse
    import shutil

    parser = argparse.ArgumentParser()
    parser.add_argument(
        "first", type=yearmonth,
        help="First month (format yyyy-mm) in calendar"
    )
    parser.add_argument(
        "last", type=yearmonth,
        help="Last month (format yyyy-mm) in calendar"
    )
    parser.add_argument(
        "--weeknumber", choices=["none", "iso", "index"], default="iso"
    )
    parser.add_argument("--months-per-row", type=int)
    args = parser.parse_args()

    if args.weeknumber == "none":
        cal = calendar.TextCalendar()
        cal.month_width = 21
    elif args.weeknumber == "iso":
        cal = MyTextCalendar()
    elif args.weeknumber == "index":
        cal = MyIndexedTextCalendar()
    else:
        assert False

    months_per_row = args.months_per_row
    if months_per_row is None:
        size = shutil.get_terminal_size()
        months_per_row = size.columns // (cal.month_width + SEP_WIDTH)

    dump_calendar(args.first, args.last, months_per_row, cal)


if __name__ == "__main__":
    main()

$ ./print_cal.py 2020-10 2021-5 --weeknumber index
       October 2020               November 2020               December 2020                January 2021
   Mo Tu We Th Fr Sa Su        Mo Tu We Th Fr Sa Su        Mo Tu We Th Fr Sa Su        Mo Tu We Th Fr Sa Su
 1           1  2  3  4      5                    1     10     1  2  3  4  5  6     14              1  2  3
 2  5  6  7  8  9 10 11      6  2  3  4  5  6  7  8     11  7  8  9 10 11 12 13     15  4  5  6  7  8  9 10
 3 12 13 14 15 16 17 18      7  9 10 11 12 13 14 15     12 14 15 16 17 18 19 20     16 11 12 13 14 15 16 17
 4 19 20 21 22 23 24 25      8 16 17 18 19 20 21 22     13 21 22 23 24 25 26 27     17 18 19 20 21 22 23 24
 5 26 27 28 29 30 31         9 23 24 25 26 27 28 29     14 28 29 30 31              18 25 26 27 28 29 30 31
                            10 30

      February 2021                 March 2021                  April 2021                   May 2021
   Mo Tu We Th Fr Sa Su        Mo Tu We Th Fr Sa Su        Mo Tu We Th Fr Sa Su        Mo Tu We Th Fr Sa Su
19  1  2  3  4  5  6  7     23  1  2  3  4  5  6  7     27           1  2  3  4     31                 1  2
20  8  9 10 11 12 13 14     24  8  9 10 11 12 13 14     28  5  6  7  8  9 10 11     32  3  4  5  6  7  8  9
21 15 16 17 18 19 20 21     25 15 16 17 18 19 20 21     29 12 13 14 15 16 17 18     33 10 11 12 13 14 15 16
22 22 23 24 25 26 27 28     26 22 23 24 25 26 27 28     30 19 20 21 22 23 24 25     34 17 18 19 20 21 22 23
                            27 29 30 31                 31 26 27 28 29 30           35 24 25 26 27 28 29 30
                                                                                    36 31

$



More information about the Python-list mailing list