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 $ -- https://mail.python.org/mailman/listinfo/python-list