1
"""Calendar printing functions
3
Note when comparing these calendars to the ones printed by cal(1): By
4
default, these calendars have Monday as the first day of the week, and
5
Sunday as the last (the European convention). Use setfirstweekday() to
6
set the first day of the week (0=Monday, 6=Sunday)."""
10
import locale as _locale
12
__all__ = ["IllegalMonthError", "IllegalWeekdayError", "setfirstweekday",
13
"firstweekday", "isleap", "leapdays", "weekday", "monthrange",
14
"monthcalendar", "prmonth", "month", "prcal", "calendar",
15
"timegm", "month_name", "month_abbr", "day_name", "day_abbr"]
17
# Exception raised for bad input (with string parameter for details)
20
# Exceptions raised for bad input
21
class IllegalMonthError(ValueError):
22
def __init__(self, month):
25
return "bad month number %r; must be 1-12" % self.month
28
class IllegalWeekdayError(ValueError):
29
def __init__(self, weekday):
30
self.weekday = weekday
32
return "bad weekday number %r; must be 0 (Monday) to 6 (Sunday)" % self.weekday
35
# Constants for months referenced later
39
# Number of days per month (except for February in leap years)
40
mdays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
42
# This module used to have hard-coded lists of day and month names, as
43
# English strings. The classes following emulate a read-only version of
44
# that, but supply localized names. Note that the values are computed
45
# fresh on each call, in case the user changes locale between calls.
47
class _localized_month:
49
_months = [datetime.date(2001, i+1, 1).strftime for i in range(12)]
50
_months.insert(0, lambda x: "")
52
def __init__(self, format):
55
def __getitem__(self, i):
56
funcs = self._months[i]
57
if isinstance(i, slice):
58
return [f(self.format) for f in funcs]
60
return funcs(self.format)
68
# January 1, 2001, was a Monday.
69
_days = [datetime.date(2001, 1, i+1).strftime for i in range(7)]
71
def __init__(self, format):
74
def __getitem__(self, i):
76
if isinstance(i, slice):
77
return [f(self.format) for f in funcs]
79
return funcs(self.format)
85
# Full and abbreviated names of weekdays
86
day_name = _localized_day('%A')
87
day_abbr = _localized_day('%a')
89
# Full and abbreviated names of months (1-based arrays!!!)
90
month_name = _localized_month('%B')
91
month_abbr = _localized_month('%b')
93
# Constants for weekdays
94
(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7)
98
"""Return 1 for leap years, 0 for non-leap years."""
99
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
102
def leapdays(y1, y2):
103
"""Return number of leap years in range [y1, y2).
107
return (y2//4 - y1//4) - (y2//100 - y1//100) + (y2//400 - y1//400)
110
def weekday(year, month, day):
111
"""Return weekday (0-6 ~ Mon-Sun) for year (1970-...), month (1-12),
113
return datetime.date(year, month, day).weekday()
116
def monthrange(year, month):
117
"""Return weekday (0-6 ~ Mon-Sun) and number of days (28-31) for
119
if not 1 <= month <= 12:
120
raise IllegalMonthError(month)
121
day1 = weekday(year, month, 1)
122
ndays = mdays[month] + (month == February and isleap(year))
126
class Calendar(object):
128
Base calendar class. This class doesn't do any formatting. It simply
129
provides data to subclasses.
132
def __init__(self, firstweekday=0):
133
self.firstweekday = firstweekday # 0 = Monday, 6 = Sunday
135
def getfirstweekday(self):
136
return self._firstweekday % 7
138
def setfirstweekday(self, firstweekday):
139
self._firstweekday = firstweekday
141
firstweekday = property(getfirstweekday, setfirstweekday)
143
def iterweekdays(self):
145
Return a iterator for one week of weekday numbers starting with the
146
configured first one.
148
for i in range(self.firstweekday, self.firstweekday + 7):
151
def itermonthdates(self, year, month):
153
Return an iterator for one month. The iterator will yield datetime.date
154
values and will always iterate through complete weeks, so it will yield
155
dates outside the specified month.
157
date = datetime.date(year, month, 1)
158
# Go back to the beginning of the week
159
days = (date.weekday() - self.firstweekday) % 7
160
date -= datetime.timedelta(days=days)
161
oneday = datetime.timedelta(days=1)
165
if date.month != month and date.weekday() == self.firstweekday:
168
def itermonthdays2(self, year, month):
170
Like itermonthdates(), but will yield (day number, weekday number)
171
tuples. For days outside the specified month the day number is 0.
173
for date in self.itermonthdates(year, month):
174
if date.month != month:
175
yield (0, date.weekday())
177
yield (date.day, date.weekday())
179
def itermonthdays(self, year, month):
181
Like itermonthdates(), but will yield day numbers. For days outside
182
the specified month the day number is 0.
184
for date in self.itermonthdates(year, month):
185
if date.month != month:
190
def monthdatescalendar(self, year, month):
192
Return a matrix (list of lists) representing a month's calendar.
193
Each row represents a week; week entries are datetime.date values.
195
dates = list(self.itermonthdates(year, month))
196
return [ dates[i:i+7] for i in range(0, len(dates), 7) ]
198
def monthdays2calendar(self, year, month):
200
Return a matrix representing a month's calendar.
201
Each row represents a week; week entries are
202
(day number, weekday number) tuples. Day numbers outside this month
205
days = list(self.itermonthdays2(year, month))
206
return [ days[i:i+7] for i in range(0, len(days), 7) ]
208
def monthdayscalendar(self, year, month):
210
Return a matrix representing a month's calendar.
211
Each row represents a week; days outside this month are zero.
213
days = list(self.itermonthdays(year, month))
214
return [ days[i:i+7] for i in range(0, len(days), 7) ]
216
def yeardatescalendar(self, year, width=3):
218
Return the data for the specified year ready for formatting. The return
219
value is a list of month rows. Each month row contains upto width months.
220
Each month contains between 4 and 6 weeks and each week contains 1-7
221
days. Days are datetime.date objects.
224
self.monthdatescalendar(year, i)
225
for i in range(January, January+12)
227
return [months[i:i+width] for i in range(0, len(months), width) ]
229
def yeardays2calendar(self, year, width=3):
231
Return the data for the specified year ready for formatting (similar to
232
yeardatescalendar()). Entries in the week lists are
233
(day number, weekday number) tuples. Day numbers outside this month are
237
self.monthdays2calendar(year, i)
238
for i in range(January, January+12)
240
return [months[i:i+width] for i in range(0, len(months), width) ]
242
def yeardayscalendar(self, year, width=3):
244
Return the data for the specified year ready for formatting (similar to
245
yeardatescalendar()). Entries in the week lists are day numbers.
246
Day numbers outside this month are zero.
249
self.monthdayscalendar(year, i)
250
for i in range(January, January+12)
252
return [months[i:i+width] for i in range(0, len(months), width) ]
255
class TextCalendar(Calendar):
257
Subclass of Calendar that outputs a calendar as a simple plain text
258
similar to the UNIX program cal.
261
def prweek(self, theweek, width):
263
Print a single week (no newline).
265
print(self.formatweek(theweek, width), end=' ')
267
def formatday(self, day, weekday, width):
269
Returns a formatted day.
274
s = '%2i' % day # right-align single-digit days
275
return s.center(width)
277
def formatweek(self, theweek, width):
279
Returns a single week in a string (no newline).
281
return ' '.join(self.formatday(d, wd, width) for (d, wd) in theweek)
283
def formatweekday(self, day, width):
285
Returns a formatted week day name.
291
return names[day][:width].center(width)
293
def formatweekheader(self, width):
295
Return a header for a week.
297
return ' '.join(self.formatweekday(i, width) for i in self.iterweekdays())
299
def formatmonthname(self, theyear, themonth, width, withyear=True):
301
Return a formatted month name.
303
s = month_name[themonth]
305
s = "%s %r" % (s, theyear)
306
return s.center(width)
308
def prmonth(self, theyear, themonth, w=0, l=0):
310
Print a month's calendar.
312
print(self.formatmonth(theyear, themonth, w, l), end=' ')
314
def formatmonth(self, theyear, themonth, w=0, l=0):
316
Return a month's calendar string (multi-line).
320
s = self.formatmonthname(theyear, themonth, 7 * (w + 1) - 1)
323
s += self.formatweekheader(w).rstrip()
325
for week in self.monthdays2calendar(theyear, themonth):
326
s += self.formatweek(week, w).rstrip()
330
def formatyear(self, theyear, w=2, l=1, c=6, m=3):
332
Returns a year's calendar as a multi-line string.
337
colwidth = (w + 1) * 7 - 1
340
a(repr(theyear).center(colwidth*m+c*(m-1)).rstrip())
342
header = self.formatweekheader(w)
343
for (i, row) in enumerate(self.yeardays2calendar(theyear, m)):
345
months = range(m*i+1, min(m*(i+1)+1, 13))
347
names = (self.formatmonthname(theyear, k, colwidth, False)
349
a(formatstring(names, colwidth, c).rstrip())
351
headers = (header for k in months)
352
a(formatstring(headers, colwidth, c).rstrip())
354
# max number of weeks for this row
355
height = max(len(cal) for cal in row)
356
for j in range(height):
362
weeks.append(self.formatweek(cal[j], w))
363
a(formatstring(weeks, colwidth, c).rstrip())
367
def pryear(self, theyear, w=0, l=0, c=6, m=3):
368
"""Print a year's calendar."""
369
print(self.formatyear(theyear, w, l, c, m))
372
class HTMLCalendar(Calendar):
374
This calendar returns complete HTML pages.
377
# CSS classes for the day <td>s
378
cssclasses = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
380
def formatday(self, day, weekday):
382
Return a day as a table cell.
385
return '<td class="noday"> </td>' # day outside month
387
return '<td class="%s">%d</td>' % (self.cssclasses[weekday], day)
389
def formatweek(self, theweek):
391
Return a complete week as a table row.
393
s = ''.join(self.formatday(d, wd) for (d, wd) in theweek)
394
return '<tr>%s</tr>' % s
396
def formatweekday(self, day):
398
Return a weekday name as a table header.
400
return '<th class="%s">%s</th>' % (self.cssclasses[day], day_abbr[day])
402
def formatweekheader(self):
404
Return a header for a week as a table row.
406
s = ''.join(self.formatweekday(i) for i in self.iterweekdays())
407
return '<tr>%s</tr>' % s
409
def formatmonthname(self, theyear, themonth, withyear=True):
411
Return a month name as a table row.
414
s = '%s %s' % (month_name[themonth], theyear)
416
s = '%s' % month_name[themonth]
417
return '<tr><th colspan="7" class="month">%s</th></tr>' % s
419
def formatmonth(self, theyear, themonth, withyear=True):
421
Return a formatted month as a table.
425
a('<table border="0" cellpadding="0" cellspacing="0" class="month">')
427
a(self.formatmonthname(theyear, themonth, withyear=withyear))
429
a(self.formatweekheader())
431
for week in self.monthdays2calendar(theyear, themonth):
432
a(self.formatweek(week))
438
def formatyear(self, theyear, width=3):
440
Return a formatted year as a table of tables.
444
width = max(width, 1)
445
a('<table border="0" cellpadding="0" cellspacing="0" class="year">')
447
a('<tr><th colspan="%d" class="year">%s</th></tr>' % (width, theyear))
448
for i in range(January, January+12, width):
450
months = range(i, min(i+width, 13))
454
a(self.formatmonth(theyear, m, withyear=False))
460
def formatyearpage(self, theyear, width=3, css='calendar.css', encoding=None):
462
Return a formatted year as a complete HTML page.
465
encoding = sys.getdefaultencoding()
468
a('<?xml version="1.0" encoding="%s"?>\n' % encoding)
469
a('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n')
472
a('<meta http-equiv="Content-Type" content="text/html; charset=%s" />\n' % encoding)
474
a('<link rel="stylesheet" type="text/css" href="%s" />\n' % css)
475
a('<title>Calendar for %d</title>\n' % theyear)
478
a(self.formatyear(theyear, width))
481
return ''.join(v).encode(encoding, "xmlcharrefreplace")
484
class different_locale:
485
def __init__(self, locale):
489
self.oldlocale = _locale.setlocale(_locale.LC_TIME, self.locale)
490
#return _locale.getlocale(_locale.LC_TIME)[1]
492
def __exit__(self, *args):
493
_locale.setlocale(_locale.LC_TIME, self.oldlocale)
496
class LocaleTextCalendar(TextCalendar):
498
This class can be passed a locale name in the constructor and will return
499
month and weekday names in the specified locale. If this locale includes
500
an encoding all strings containing month and weekday names will be returned
504
def __init__(self, firstweekday=0, locale=None):
505
TextCalendar.__init__(self, firstweekday)
507
locale = _locale.getdefaultlocale()
510
def formatweekday(self, day, width):
511
with different_locale(self.locale):
517
return name[:width].center(width)
519
def formatmonthname(self, theyear, themonth, width, withyear=True):
520
with different_locale(self.locale):
521
s = month_name[themonth]
523
s = "%s %r" % (s, theyear)
524
return s.center(width)
527
class LocaleHTMLCalendar(HTMLCalendar):
529
This class can be passed a locale name in the constructor and will return
530
month and weekday names in the specified locale. If this locale includes
531
an encoding all strings containing month and weekday names will be returned
534
def __init__(self, firstweekday=0, locale=None):
535
HTMLCalendar.__init__(self, firstweekday)
537
locale = _locale.getdefaultlocale()
540
def formatweekday(self, day):
541
with different_locale(self.locale):
543
return '<th class="%s">%s</th>' % (self.cssclasses[day], s)
545
def formatmonthname(self, theyear, themonth, withyear=True):
546
with different_locale(self.locale):
547
s = month_name[themonth]
549
s = '%s %s' % (s, theyear)
550
return '<tr><th colspan="7" class="month">%s</th></tr>' % s
553
# Support for old module level interface
556
firstweekday = c.getfirstweekday
558
def setfirstweekday(firstweekday):
559
if not MONDAY <= firstweekday <= SUNDAY:
560
raise IllegalWeekdayError(firstweekday)
561
c.firstweekday = firstweekday
563
monthcalendar = c.monthdayscalendar
566
weekheader = c.formatweekheader
568
month = c.formatmonth
569
calendar = c.formatyear
573
# Spacing of month columns for multi-column year calendar
574
_colwidth = 7*3 - 1 # Amount printed by prweek()
575
_spacing = 6 # Number of spaces between columns
578
def format(cols, colwidth=_colwidth, spacing=_spacing):
579
"""Prints multi-column formatting for year calendars"""
580
print(formatstring(cols, colwidth, spacing))
583
def formatstring(cols, colwidth=_colwidth, spacing=_spacing):
584
"""Returns a string formatted from n strings, centered within n columns."""
586
return spacing.join(c.center(colwidth) for c in cols)
590
_EPOCH_ORD = datetime.date(EPOCH, 1, 1).toordinal()
594
"""Unrelated but handy function to calculate Unix timestamp from GMT."""
595
year, month, day, hour, minute, second = tuple[:6]
596
days = datetime.date(year, month, 1).toordinal() - _EPOCH_ORD + day - 1
597
hours = days*24 + hour
598
minutes = hours*60 + minute
599
seconds = minutes*60 + second
605
parser = optparse.OptionParser(usage="usage: %prog [options] [year [month]]")
608
dest="width", type="int", default=2,
609
help="width of date column (default 2, text only)"
613
dest="lines", type="int", default=1,
614
help="number of lines for each week (default 1, text only)"
618
dest="spacing", type="int", default=6,
619
help="spacing between months (default 6, text only)"
623
dest="months", type="int", default=3,
624
help="months per row (default 3, text only)"
628
dest="css", default="calendar.css",
629
help="CSS to use for page (html only)"
633
dest="locale", default=None,
634
help="locale to be used from month and weekday names"
638
dest="encoding", default=None,
639
help="Encoding to use for output"
643
dest="type", default="text",
644
choices=("text", "html"),
645
help="output type (text or html)"
648
(options, args) = parser.parse_args(args)
650
if options.locale and not options.encoding:
651
parser.error("if --locale is specified --encoding is required")
654
locale = options.locale, options.encoding
656
if options.type == "html":
658
cal = LocaleHTMLCalendar(locale=locale)
661
encoding = options.encoding
663
encoding = sys.getdefaultencoding()
664
optdict = dict(encoding=encoding, css=options.css)
666
print(cal.formatyearpage(datetime.date.today().year, **optdict))
668
print(cal.formatyearpage(int(args[1]), **optdict))
670
parser.error("incorrect number of arguments")
674
cal = LocaleTextCalendar(locale=locale)
677
optdict = dict(w=options.width, l=options.lines)
679
optdict["c"] = options.spacing
680
optdict["m"] = options.months
682
result = cal.formatyear(datetime.date.today().year, **optdict)
684
result = cal.formatyear(int(args[1]), **optdict)
686
result = cal.formatmonth(int(args[1]), int(args[2]), **optdict)
688
parser.error("incorrect number of arguments")
691
result = result.encode(options.encoding)
695
if __name__ == "__main__":