Change to flush and close logic to fix #1760556.
[python.git] / Lib / calendar.py
blob84fabc75c70062435fb98289a4c39523a952551e
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)."""
8 from __future__ import with_statement
9 import sys, datetime, locale
11 __all__ = ["IllegalMonthError", "IllegalWeekdayError", "setfirstweekday",
12 "firstweekday", "isleap", "leapdays", "weekday", "monthrange",
13 "monthcalendar", "prmonth", "month", "prcal", "calendar",
14 "timegm", "month_name", "month_abbr", "day_name", "day_abbr"]
16 # Exception raised for bad input (with string parameter for details)
17 error = ValueError
19 # Exceptions raised for bad input
20 class IllegalMonthError(ValueError):
21 def __init__(self, month):
22 self.month = month
23 def __str__(self):
24 return "bad month number %r; must be 1-12" % self.month
27 class IllegalWeekdayError(ValueError):
28 def __init__(self, weekday):
29 self.weekday = weekday
30 def __str__(self):
31 return "bad weekday number %r; must be 0 (Monday) to 6 (Sunday)" % self.weekday
34 # Constants for months referenced later
35 January = 1
36 February = 2
38 # Number of days per month (except for February in leap years)
39 mdays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
41 # This module used to have hard-coded lists of day and month names, as
42 # English strings. The classes following emulate a read-only version of
43 # that, but supply localized names. Note that the values are computed
44 # fresh on each call, in case the user changes locale between calls.
46 class _localized_month:
48 _months = [datetime.date(2001, i+1, 1).strftime for i in range(12)]
49 _months.insert(0, lambda x: "")
51 def __init__(self, format):
52 self.format = format
54 def __getitem__(self, i):
55 funcs = self._months[i]
56 if isinstance(i, slice):
57 return [f(self.format) for f in funcs]
58 else:
59 return funcs(self.format)
61 def __len__(self):
62 return 13
65 class _localized_day:
67 # January 1, 2001, was a Monday.
68 _days = [datetime.date(2001, 1, i+1).strftime for i in range(7)]
70 def __init__(self, format):
71 self.format = format
73 def __getitem__(self, i):
74 funcs = self._days[i]
75 if isinstance(i, slice):
76 return [f(self.format) for f in funcs]
77 else:
78 return funcs(self.format)
80 def __len__(self):
81 return 7
84 # Full and abbreviated names of weekdays
85 day_name = _localized_day('%A')
86 day_abbr = _localized_day('%a')
88 # Full and abbreviated names of months (1-based arrays!!!)
89 month_name = _localized_month('%B')
90 month_abbr = _localized_month('%b')
92 # Constants for weekdays
93 (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7)
96 def isleap(year):
97 """Return 1 for leap years, 0 for non-leap years."""
98 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
101 def leapdays(y1, y2):
102 """Return number of leap years in range [y1, y2).
103 Assume y1 <= y2."""
104 y1 -= 1
105 y2 -= 1
106 return (y2//4 - y1//4) - (y2//100 - y1//100) + (y2//400 - y1//400)
109 def weekday(year, month, day):
110 """Return weekday (0-6 ~ Mon-Sun) for year (1970-...), month (1-12),
111 day (1-31)."""
112 return datetime.date(year, month, day).weekday()
115 def monthrange(year, month):
116 """Return weekday (0-6 ~ Mon-Sun) and number of days (28-31) for
117 year, month."""
118 if not 1 <= month <= 12:
119 raise IllegalMonthError(month)
120 day1 = weekday(year, month, 1)
121 ndays = mdays[month] + (month == February and isleap(year))
122 return day1, ndays
125 class Calendar(object):
127 Base calendar class. This class doesn't do any formatting. It simply
128 provides data to subclasses.
131 def __init__(self, firstweekday=0):
132 self.firstweekday = firstweekday # 0 = Monday, 6 = Sunday
134 def getfirstweekday(self):
135 return self._firstweekday % 7
137 def setfirstweekday(self, firstweekday):
138 self._firstweekday = firstweekday
140 firstweekday = property(getfirstweekday, setfirstweekday)
142 def iterweekdays(self):
144 Return a iterator for one week of weekday numbers starting with the
145 configured first one.
147 for i in range(self.firstweekday, self.firstweekday + 7):
148 yield i%7
150 def itermonthdates(self, year, month):
152 Return an iterator for one month. The iterator will yield datetime.date
153 values and will always iterate through complete weeks, so it will yield
154 dates outside the specified month.
156 date = datetime.date(year, month, 1)
157 # Go back to the beginning of the week
158 days = (date.weekday() - self.firstweekday) % 7
159 date -= datetime.timedelta(days=days)
160 oneday = datetime.timedelta(days=1)
161 while True:
162 yield date
163 date += oneday
164 if date.month != month and date.weekday() == self.firstweekday:
165 break
167 def itermonthdays2(self, year, month):
169 Like itermonthdates(), but will yield (day number, weekday number)
170 tuples. For days outside the specified month the day number is 0.
172 for date in self.itermonthdates(year, month):
173 if date.month != month:
174 yield (0, date.weekday())
175 else:
176 yield (date.day, date.weekday())
178 def itermonthdays(self, year, month):
180 Like itermonthdates(), but will yield day numbers tuples. For days
181 outside the specified month the day number is 0.
183 for date in self.itermonthdates(year, month):
184 if date.month != month:
185 yield 0
186 else:
187 yield date.day
189 def monthdatescalendar(self, year, month):
191 Return a matrix (list of lists) representing a month's calendar.
192 Each row represents a week; week entries are datetime.date values.
194 dates = list(self.itermonthdates(year, month))
195 return [ dates[i:i+7] for i in range(0, len(dates), 7) ]
197 def monthdays2calendar(self, year, month):
199 Return a matrix representing a month's calendar.
200 Each row represents a week; week entries are
201 (day number, weekday number) tuples. Day numbers outside this month
202 are zero.
204 days = list(self.itermonthdays2(year, month))
205 return [ days[i:i+7] for i in range(0, len(days), 7) ]
207 def monthdayscalendar(self, year, month):
209 Return a matrix representing a month's calendar.
210 Each row represents a week; days outside this month are zero.
212 days = list(self.itermonthdays(year, month))
213 return [ days[i:i+7] for i in range(0, len(days), 7) ]
215 def yeardatescalendar(self, year, width=3):
217 Return the data for the specified year ready for formatting. The return
218 value is a list of month rows. Each month row contains upto width months.
219 Each month contains between 4 and 6 weeks and each week contains 1-7
220 days. Days are datetime.date objects.
222 months = [
223 self.monthdatescalendar(year, i)
224 for i in range(January, January+12)
226 return [months[i:i+width] for i in range(0, len(months), width) ]
228 def yeardays2calendar(self, year, width=3):
230 Return the data for the specified year ready for formatting (similar to
231 yeardatescalendar()). Entries in the week lists are
232 (day number, weekday number) tuples. Day numbers outside this month are
233 zero.
235 months = [
236 self.monthdays2calendar(year, i)
237 for i in range(January, January+12)
239 return [months[i:i+width] for i in range(0, len(months), width) ]
241 def yeardayscalendar(self, year, width=3):
243 Return the data for the specified year ready for formatting (similar to
244 yeardatescalendar()). Entries in the week lists are day numbers.
245 Day numbers outside this month are zero.
247 months = [
248 self.monthdayscalendar(year, i)
249 for i in range(January, January+12)
251 return [months[i:i+width] for i in range(0, len(months), width) ]
254 class TextCalendar(Calendar):
256 Subclass of Calendar that outputs a calendar as a simple plain text
257 similar to the UNIX program cal.
260 def prweek(self, theweek, width):
262 Print a single week (no newline).
264 print self.week(theweek, width),
266 def formatday(self, day, weekday, width):
268 Returns a formatted day.
270 if day == 0:
271 s = ''
272 else:
273 s = '%2i' % day # right-align single-digit days
274 return s.center(width)
276 def formatweek(self, theweek, width):
278 Returns a single week in a string (no newline).
280 return ' '.join(self.formatday(d, wd, width) for (d, wd) in theweek)
282 def formatweekday(self, day, width):
284 Returns a formatted week day name.
286 if width >= 9:
287 names = day_name
288 else:
289 names = day_abbr
290 return names[day][:width].center(width)
292 def formatweekheader(self, width):
294 Return a header for a week.
296 return ' '.join(self.formatweekday(i, width) for i in self.iterweekdays())
298 def formatmonthname(self, theyear, themonth, width, withyear=True):
300 Return a formatted month name.
302 s = month_name[themonth]
303 if withyear:
304 s = "%s %r" % (s, theyear)
305 return s.center(width)
307 def prmonth(self, theyear, themonth, w=0, l=0):
309 Print a month's calendar.
311 print self.formatmonth(theyear, themonth, w, l),
313 def formatmonth(self, theyear, themonth, w=0, l=0):
315 Return a month's calendar string (multi-line).
317 w = max(2, w)
318 l = max(1, l)
319 s = self.formatmonthname(theyear, themonth, 7 * (w + 1) - 1)
320 s = s.rstrip()
321 s += '\n' * l
322 s += self.formatweekheader(w).rstrip()
323 s += '\n' * l
324 for week in self.monthdays2calendar(theyear, themonth):
325 s += self.formatweek(week, w).rstrip()
326 s += '\n' * l
327 return s
329 def formatyear(self, theyear, w=2, l=1, c=6, m=3):
331 Returns a year's calendar as a multi-line string.
333 w = max(2, w)
334 l = max(1, l)
335 c = max(2, c)
336 colwidth = (w + 1) * 7 - 1
337 v = []
338 a = v.append
339 a(repr(theyear).center(colwidth*m+c*(m-1)).rstrip())
340 a('\n'*l)
341 header = self.formatweekheader(w)
342 for (i, row) in enumerate(self.yeardays2calendar(theyear, m)):
343 # months in this row
344 months = range(m*i+1, min(m*(i+1)+1, 13))
345 a('\n'*l)
346 names = (self.formatmonthname(theyear, k, colwidth, False)
347 for k in months)
348 a(formatstring(names, colwidth, c).rstrip())
349 a('\n'*l)
350 headers = (header for k in months)
351 a(formatstring(headers, colwidth, c).rstrip())
352 a('\n'*l)
353 # max number of weeks for this row
354 height = max(len(cal) for cal in row)
355 for j in range(height):
356 weeks = []
357 for cal in row:
358 if j >= len(cal):
359 weeks.append('')
360 else:
361 weeks.append(self.formatweek(cal[j], w))
362 a(formatstring(weeks, colwidth, c).rstrip())
363 a('\n' * l)
364 return ''.join(v)
366 def pryear(self, theyear, w=0, l=0, c=6, m=3):
367 """Print a year's calendar."""
368 print self.formatyear(theyear, w, l, c, m)
371 class HTMLCalendar(Calendar):
373 This calendar returns complete HTML pages.
376 # CSS classes for the day <td>s
377 cssclasses = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
379 def formatday(self, day, weekday):
381 Return a day as a table cell.
383 if day == 0:
384 return '<td class="noday">&nbsp;</td>' # day outside month
385 else:
386 return '<td class="%s">%d</td>' % (self.cssclasses[weekday], day)
388 def formatweek(self, theweek):
390 Return a complete week as a table row.
392 s = ''.join(self.formatday(d, wd) for (d, wd) in theweek)
393 return '<tr>%s</tr>' % s
395 def formatweekday(self, day):
397 Return a weekday name as a table header.
399 return '<th class="%s">%s</th>' % (self.cssclasses[day], day_abbr[day])
401 def formatweekheader(self):
403 Return a header for a week as a table row.
405 s = ''.join(self.formatweekday(i) for i in self.iterweekdays())
406 return '<tr>%s</tr>' % s
408 def formatmonthname(self, theyear, themonth, withyear=True):
410 Return a month name as a table row.
412 if withyear:
413 s = '%s %s' % (month_name[themonth], theyear)
414 else:
415 s = '%s' % month_name[themonth]
416 return '<tr><th colspan="7" class="month">%s</th></tr>' % s
418 def formatmonth(self, theyear, themonth, withyear=True):
420 Return a formatted month as a table.
422 v = []
423 a = v.append
424 a('<table border="0" cellpadding="0" cellspacing="0" class="month">')
425 a('\n')
426 a(self.formatmonthname(theyear, themonth, withyear=withyear))
427 a('\n')
428 a(self.formatweekheader())
429 a('\n')
430 for week in self.monthdays2calendar(theyear, themonth):
431 a(self.formatweek(week))
432 a('\n')
433 a('</table>')
434 a('\n')
435 return ''.join(v)
437 def formatyear(self, theyear, width=3):
439 Return a formatted year as a table of tables.
441 v = []
442 a = v.append
443 width = max(width, 1)
444 a('<table border="0" cellpadding="0" cellspacing="0" class="year">')
445 a('\n')
446 a('<tr><th colspan="%d" class="year">%s</th></tr>' % (width, theyear))
447 for i in range(January, January+12, width):
448 # months in this row
449 months = range(i, min(i+width, 13))
450 a('<tr>')
451 for m in months:
452 a('<td>')
453 a(self.formatmonth(theyear, m, withyear=False))
454 a('</td>')
455 a('</tr>')
456 a('</table>')
457 return ''.join(v)
459 def formatyearpage(self, theyear, width=3, css='calendar.css', encoding=None):
461 Return a formatted year as a complete HTML page.
463 if encoding is None:
464 encoding = sys.getdefaultencoding()
465 v = []
466 a = v.append
467 a('<?xml version="1.0" encoding="%s"?>\n' % encoding)
468 a('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n')
469 a('<html>\n')
470 a('<head>\n')
471 a('<meta http-equiv="Content-Type" content="text/html; charset=%s" />\n' % encoding)
472 if css is not None:
473 a('<link rel="stylesheet" type="text/css" href="%s" />\n' % css)
474 a('<title>Calendar for %d</title>\n' % theyear)
475 a('</head>\n')
476 a('<body>\n')
477 a(self.formatyear(theyear, width))
478 a('</body>\n')
479 a('</html>\n')
480 return ''.join(v).encode(encoding, "xmlcharrefreplace")
483 class TimeEncoding:
484 def __init__(self, locale):
485 self.locale = locale
487 def __enter__(self):
488 self.oldlocale = locale.setlocale(locale.LC_TIME, self.locale)
489 return locale.getlocale(locale.LC_TIME)[1]
491 def __exit__(self, *args):
492 locale.setlocale(locale.LC_TIME, self.oldlocale)
495 class LocaleTextCalendar(TextCalendar):
497 This class can be passed a locale name in the constructor and will return
498 month and weekday names in the specified locale. If this locale includes
499 an encoding all strings containing month and weekday names will be returned
500 as unicode.
503 def __init__(self, firstweekday=0, locale=None):
504 TextCalendar.__init__(self, firstweekday)
505 if locale is None:
506 locale = locale.getdefaultlocale()
507 self.locale = locale
509 def formatweekday(self, day, width):
510 with TimeEncoding(self.locale) as encoding:
511 if width >= 9:
512 names = day_name
513 else:
514 names = day_abbr
515 name = names[day]
516 if encoding is not None:
517 name = name.decode(encoding)
518 return name[:width].center(width)
520 def formatmonthname(self, theyear, themonth, width, withyear=True):
521 with TimeEncoding(self.locale) as encoding:
522 s = month_name[themonth]
523 if encoding is not None:
524 s = s.decode(encoding)
525 if withyear:
526 s = "%s %r" % (s, theyear)
527 return s.center(width)
530 class LocaleHTMLCalendar(HTMLCalendar):
532 This class can be passed a locale name in the constructor and will return
533 month and weekday names in the specified locale. If this locale includes
534 an encoding all strings containing month and weekday names will be returned
535 as unicode.
537 def __init__(self, firstweekday=0, locale=None):
538 HTMLCalendar.__init__(self, firstweekday)
539 if locale is None:
540 locale = locale.getdefaultlocale()
541 self.locale = locale
543 def formatweekday(self, day):
544 with TimeEncoding(self.locale) as encoding:
545 s = day_abbr[day]
546 if encoding is not None:
547 s = s.decode(encoding)
548 return '<th class="%s">%s</th>' % (self.cssclasses[day], s)
550 def formatmonthname(self, theyear, themonth, withyear=True):
551 with TimeEncoding(self.locale) as encoding:
552 s = month_name[themonth]
553 if encoding is not None:
554 s = s.decode(encoding)
555 if withyear:
556 s = '%s %s' % (s, theyear)
557 return '<tr><th colspan="7" class="month">%s</th></tr>' % s
560 # Support for old module level interface
561 c = TextCalendar()
563 firstweekday = c.getfirstweekday
565 def setfirstweekday(firstweekday):
566 if not MONDAY <= firstweekday <= SUNDAY:
567 raise IllegalWeekdayError(firstweekday)
568 c.firstweekday = firstweekday
570 monthcalendar = c.monthdayscalendar
571 prweek = c.prweek
572 week = c.formatweek
573 weekheader = c.formatweekheader
574 prmonth = c.prmonth
575 month = c.formatmonth
576 calendar = c.formatyear
577 prcal = c.pryear
580 # Spacing of month columns for multi-column year calendar
581 _colwidth = 7*3 - 1 # Amount printed by prweek()
582 _spacing = 6 # Number of spaces between columns
585 def format(cols, colwidth=_colwidth, spacing=_spacing):
586 """Prints multi-column formatting for year calendars"""
587 print formatstring(cols, colwidth, spacing)
590 def formatstring(cols, colwidth=_colwidth, spacing=_spacing):
591 """Returns a string formatted from n strings, centered within n columns."""
592 spacing *= ' '
593 return spacing.join(c.center(colwidth) for c in cols)
596 EPOCH = 1970
597 _EPOCH_ORD = datetime.date(EPOCH, 1, 1).toordinal()
600 def timegm(tuple):
601 """Unrelated but handy function to calculate Unix timestamp from GMT."""
602 year, month, day, hour, minute, second = tuple[:6]
603 days = datetime.date(year, month, 1).toordinal() - _EPOCH_ORD + day - 1
604 hours = days*24 + hour
605 minutes = hours*60 + minute
606 seconds = minutes*60 + second
607 return seconds
610 def main(args):
611 import optparse
612 parser = optparse.OptionParser(usage="usage: %prog [options] [year [month]]")
613 parser.add_option(
614 "-w", "--width",
615 dest="width", type="int", default=2,
616 help="width of date column (default 2, text only)"
618 parser.add_option(
619 "-l", "--lines",
620 dest="lines", type="int", default=1,
621 help="number of lines for each week (default 1, text only)"
623 parser.add_option(
624 "-s", "--spacing",
625 dest="spacing", type="int", default=6,
626 help="spacing between months (default 6, text only)"
628 parser.add_option(
629 "-m", "--months",
630 dest="months", type="int", default=3,
631 help="months per row (default 3, text only)"
633 parser.add_option(
634 "-c", "--css",
635 dest="css", default="calendar.css",
636 help="CSS to use for page (html only)"
638 parser.add_option(
639 "-L", "--locale",
640 dest="locale", default=None,
641 help="locale to be used from month and weekday names"
643 parser.add_option(
644 "-e", "--encoding",
645 dest="encoding", default=None,
646 help="Encoding to use for output"
648 parser.add_option(
649 "-t", "--type",
650 dest="type", default="text",
651 choices=("text", "html"),
652 help="output type (text or html)"
655 (options, args) = parser.parse_args(args)
657 if options.locale and not options.encoding:
658 parser.error("if --locale is specified --encoding is required")
659 sys.exit(1)
661 if options.type == "html":
662 if options.locale:
663 cal = LocaleHTMLCalendar(locale=options.locale)
664 else:
665 cal = HTMLCalendar()
666 encoding = options.encoding
667 if encoding is None:
668 encoding = sys.getdefaultencoding()
669 optdict = dict(encoding=encoding, css=options.css)
670 if len(args) == 1:
671 print cal.formatyearpage(datetime.date.today().year, **optdict)
672 elif len(args) == 2:
673 print cal.formatyearpage(int(args[1]), **optdict)
674 else:
675 parser.error("incorrect number of arguments")
676 sys.exit(1)
677 else:
678 if options.locale:
679 cal = LocaleTextCalendar(locale=options.locale)
680 else:
681 cal = TextCalendar()
682 optdict = dict(w=options.width, l=options.lines)
683 if len(args) != 3:
684 optdict["c"] = options.spacing
685 optdict["m"] = options.months
686 if len(args) == 1:
687 result = cal.formatyear(datetime.date.today().year, **optdict)
688 elif len(args) == 2:
689 result = cal.formatyear(int(args[1]), **optdict)
690 elif len(args) == 3:
691 result = cal.formatmonth(int(args[1]), int(args[2]), **optdict)
692 else:
693 parser.error("incorrect number of arguments")
694 sys.exit(1)
695 if options.encoding:
696 result = result.encode(options.encoding)
697 print result
700 if __name__ == "__main__":
701 main(sys.argv)