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