I don't think we know of any tests that really leak anymore
[python.git] / Lib / calendar.py
blob723bb3c8d492da29f8e368dd0c42b352d1e49c80
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 import sys, datetime, locale
10 __all__ = ["IllegalMonthError", "IllegalWeekdayError", "setfirstweekday",
11 "firstweekday", "isleap", "leapdays", "weekday", "monthrange",
12 "monthcalendar", "prmonth", "month", "prcal", "calendar",
13 "timegm", "month_name", "month_abbr", "day_name", "day_abbr"]
15 # Exception raised for bad input (with string parameter for details)
16 error = ValueError
18 # Exceptions raised for bad input
19 class IllegalMonthError(ValueError):
20 def __init__(self, month):
21 self.month = month
22 def __str__(self):
23 return "bad month number %r; must be 1-12" % self.month
26 class IllegalWeekdayError(ValueError):
27 def __init__(self, weekday):
28 self.weekday = weekday
29 def __str__(self):
30 return "bad weekday number %r; must be 0 (Monday) to 6 (Sunday)" % self.weekday
33 # Constants for months referenced later
34 January = 1
35 February = 2
37 # Number of days per month (except for February in leap years)
38 mdays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
40 # This module used to have hard-coded lists of day and month names, as
41 # English strings. The classes following emulate a read-only version of
42 # that, but supply localized names. Note that the values are computed
43 # fresh on each call, in case the user changes locale between calls.
45 class _localized_month:
47 _months = [datetime.date(2001, i+1, 1).strftime for i in xrange(12)]
48 _months.insert(0, lambda x: "")
50 def __init__(self, format):
51 self.format = format
53 def __getitem__(self, i):
54 funcs = self._months[i]
55 if isinstance(i, slice):
56 return [f(self.format) for f in funcs]
57 else:
58 return funcs(self.format)
60 def __len__(self):
61 return 13
64 class _localized_day:
66 # January 1, 2001, was a Monday.
67 _days = [datetime.date(2001, 1, i+1).strftime for i in xrange(7)]
69 def __init__(self, format):
70 self.format = format
72 def __getitem__(self, i):
73 funcs = self._days[i]
74 if isinstance(i, slice):
75 return [f(self.format) for f in funcs]
76 else:
77 return funcs(self.format)
79 def __len__(self):
80 return 7
83 # Full and abbreviated names of weekdays
84 day_name = _localized_day('%A')
85 day_abbr = _localized_day('%a')
87 # Full and abbreviated names of months (1-based arrays!!!)
88 month_name = _localized_month('%B')
89 month_abbr = _localized_month('%b')
91 # Constants for weekdays
92 (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7)
95 def isleap(year):
96 """Return 1 for leap years, 0 for non-leap years."""
97 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
100 def leapdays(y1, y2):
101 """Return number of leap years in range [y1, y2).
102 Assume y1 <= y2."""
103 y1 -= 1
104 y2 -= 1
105 return (y2//4 - y1//4) - (y2//100 - y1//100) + (y2//400 - y1//400)
108 def weekday(year, month, day):
109 """Return weekday (0-6 ~ Mon-Sun) for year (1970-...), month (1-12),
110 day (1-31)."""
111 return datetime.date(year, month, day).weekday()
114 def monthrange(year, month):
115 """Return weekday (0-6 ~ Mon-Sun) and number of days (28-31) for
116 year, month."""
117 if not 1 <= month <= 12:
118 raise IllegalMonthError(month)
119 day1 = weekday(year, month, 1)
120 ndays = mdays[month] + (month == February and isleap(year))
121 return day1, ndays
124 class Calendar(object):
126 Base calendar class. This class doesn't do any formatting. It simply
127 provides data to subclasses.
130 def __init__(self, firstweekday=0):
131 self.firstweekday = firstweekday # 0 = Monday, 6 = Sunday
133 def getfirstweekday(self):
134 return self._firstweekday % 7
136 def setfirstweekday(self, firstweekday):
137 self._firstweekday = firstweekday
139 firstweekday = property(getfirstweekday, setfirstweekday)
141 def iterweekdays(self):
143 Return a iterator for one week of weekday numbers starting with the
144 configured first one.
146 for i in xrange(self.firstweekday, self.firstweekday + 7):
147 yield i%7
149 def itermonthdates(self, year, month):
151 Return an iterator for one month. The iterator will yield datetime.date
152 values and will always iterate through complete weeks, so it will yield
153 dates outside the specified month.
155 date = datetime.date(year, month, 1)
156 # Go back to the beginning of the week
157 days = (date.weekday() - self.firstweekday) % 7
158 date -= datetime.timedelta(days=days)
159 oneday = datetime.timedelta(days=1)
160 while True:
161 yield date
162 date += oneday
163 if date.month != month and date.weekday() == self.firstweekday:
164 break
166 def itermonthdays2(self, year, month):
168 Like itermonthdates(), but will yield (day number, weekday number)
169 tuples. For days outside the specified month the day number is 0.
171 for date in self.itermonthdates(year, month):
172 if date.month != month:
173 yield (0, date.weekday())
174 else:
175 yield (date.day, date.weekday())
177 def itermonthdays(self, year, month):
179 Like itermonthdates(), but will yield day numbers tuples. For days
180 outside the specified month the day number is 0.
182 for date in self.itermonthdates(year, month):
183 if date.month != month:
184 yield 0
185 else:
186 yield date.day
188 def monthdatescalendar(self, year, month):
190 Return a matrix (list of lists) representing a month's calendar.
191 Each row represents a week; week entries are datetime.date values.
193 dates = list(self.itermonthdates(year, month))
194 return [ dates[i:i+7] for i in xrange(0, len(dates), 7) ]
196 def monthdays2calendar(self, year, month):
198 Return a matrix representing a month's calendar.
199 Each row represents a week; week entries are
200 (day number, weekday number) tuples. Day numbers outside this month
201 are zero.
203 days = list(self.itermonthdays2(year, month))
204 return [ days[i:i+7] for i in xrange(0, len(days), 7) ]
206 def monthdayscalendar(self, year, month):
208 Return a matrix representing a month's calendar.
209 Each row represents a week; days outside this month are zero.
211 days = list(self.itermonthdays(year, month))
212 return [ days[i:i+7] for i in xrange(0, len(days), 7) ]
214 def yeardatescalendar(self, year, width=3):
216 Return the data for the specified year ready for formatting. The return
217 value is a list of month rows. Each month row contains upto width months.
218 Each month contains between 4 and 6 weeks and each week contains 1-7
219 days. Days are datetime.date objects.
221 months = [
222 self.monthdatescalendar(year, i)
223 for i in xrange(January, January+12)
225 return [months[i:i+width] for i in xrange(0, len(months), width) ]
227 def yeardays2calendar(self, year, width=3):
229 Return the data for the specified year ready for formatting (similar to
230 yeardatescalendar()). Entries in the week lists are
231 (day number, weekday number) tuples. Day numbers outside this month are
232 zero.
234 months = [
235 self.monthdays2calendar(year, i)
236 for i in xrange(January, January+12)
238 return [months[i:i+width] for i in xrange(0, len(months), width) ]
240 def yeardayscalendar(self, year, width=3):
242 Return the data for the specified year ready for formatting (similar to
243 yeardatescalendar()). Entries in the week lists are day numbers.
244 Day numbers outside this month are zero.
246 months = [
247 self.monthdayscalendar(year, i)
248 for i in xrange(January, January+12)
250 return [months[i:i+width] for i in xrange(0, len(months), width) ]
253 class TextCalendar(Calendar):
255 Subclass of Calendar that outputs a calendar as a simple plain text
256 similar to the UNIX program cal.
259 def prweek(theweek, width):
261 Print a single week (no newline).
263 print self.week(theweek, width),
265 def formatday(self, day, weekday, width):
267 Returns a formatted day.
269 if day == 0:
270 s = ''
271 else:
272 s = '%2i' % day # right-align single-digit days
273 return s.center(width)
275 def formatweek(self, theweek, width):
277 Returns a single week in a string (no newline).
279 return ' '.join(self.formatday(d, wd, width) for (d, wd) in theweek)
281 def formatweekday(self, day, width):
283 Returns a formatted week day name.
285 if width >= 9:
286 names = day_name
287 else:
288 names = day_abbr
289 return names[day][:width].center(width)
291 def formatweekheader(self, width):
293 Return a header for a week.
295 return ' '.join(self.formatweekday(i, width) for i in self.iterweekdays())
297 def formatmonthname(self, theyear, themonth, width, withyear=True):
299 Return a formatted month name.
301 s = month_name[themonth]
302 if withyear:
303 s = "%s %r" % (s, theyear)
304 return s.center(width)
306 def prmonth(self, theyear, themonth, w=0, l=0):
308 Print a month's calendar.
310 print self.formatmonth(theyear, themonth, w, l),
312 def formatmonth(self, theyear, themonth, w=0, l=0):
314 Return a month's calendar string (multi-line).
316 w = max(2, w)
317 l = max(1, l)
318 s = self.formatmonthname(theyear, themonth, 7 * (w + 1) - 1)
319 s = s.rstrip()
320 s += '\n' * l
321 s += self.formatweekheader(w).rstrip()
322 s += '\n' * l
323 for week in self.monthdays2calendar(theyear, themonth):
324 s += self.formatweek(week, w).rstrip()
325 s += '\n' * l
326 return s
328 def formatyear(self, theyear, w=2, l=1, c=6, m=3):
330 Returns a year's calendar as a multi-line string.
332 w = max(2, w)
333 l = max(1, l)
334 c = max(2, c)
335 colwidth = (w + 1) * 7 - 1
336 v = []
337 a = v.append
338 a(repr(theyear).center(colwidth*m+c*(m-1)).rstrip())
339 a('\n'*l)
340 header = self.formatweekheader(w)
341 for (i, row) in enumerate(self.yeardays2calendar(theyear, m)):
342 # months in this row
343 months = xrange(m*i+1, min(m*(i+1)+1, 13))
344 a('\n'*l)
345 names = (self.formatmonthname(theyear, k, colwidth, False)
346 for k in months)
347 a(formatstring(names, colwidth, c).rstrip())
348 a('\n'*l)
349 headers = (header for k in months)
350 a(formatstring(headers, colwidth, c).rstrip())
351 a('\n'*l)
352 # max number of weeks for this row
353 height = max(len(cal) for cal in row)
354 for j in xrange(height):
355 weeks = []
356 for cal in row:
357 if j >= len(cal):
358 weeks.append('')
359 else:
360 weeks.append(self.formatweek(cal[j], w))
361 a(formatstring(weeks, colwidth, c).rstrip())
362 a('\n' * l)
363 return ''.join(v)
365 def pryear(self, theyear, w=0, l=0, c=6, m=3):
366 """Print a year's calendar."""
367 print self.formatyear(theyear, w, l, c, m)
370 class HTMLCalendar(Calendar):
372 This calendar returns complete HTML pages.
375 # CSS classes for the day <td>s
376 cssclasses = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
378 def formatday(self, day, weekday):
380 Return a day as a table cell.
382 if day == 0:
383 return '<td class="noday">&nbsp;</td>' # day outside month
384 else:
385 return '<td class="%s">%d</td>' % (self.cssclasses[weekday], day)
387 def formatweek(self, theweek):
389 Return a complete week as a table row.
391 s = ''.join(self.formatday(d, wd) for (d, wd) in theweek)
392 return '<tr>%s</tr>' % s
394 def formatweekday(self, day):
396 Return a weekday name as a table header.
398 return '<th class="%s">%s</th>' % (self.cssclasses[day], day_abbr[day])
400 def formatweekheader(self):
402 Return a header for a week as a table row.
404 s = ''.join(self.formatweekday(i) for i in self.iterweekdays())
405 return '<tr>%s</tr>' % s
407 def formatmonthname(self, theyear, themonth, withyear=True):
409 Return a month name as a table row.
411 if withyear:
412 s = '%s %s' % (month_name[themonth], theyear)
413 else:
414 s = '%s' % month_name[themonth]
415 return '<tr><th colspan="7" class="month">%s</th></tr>' % s
417 def formatmonth(self, theyear, themonth, withyear=True):
419 Return a formatted month as a table.
421 v = []
422 a = v.append
423 a('<table border="0" cellpadding="0" cellspacing="0" class="month">')
424 a('\n')
425 a(self.formatmonthname(theyear, themonth, withyear=withyear))
426 a('\n')
427 a(self.formatweekheader())
428 a('\n')
429 for week in self.monthdays2calendar(theyear, themonth):
430 a(self.formatweek(week))
431 a('\n')
432 a('</table>')
433 a('\n')
434 return ''.join(v)
436 def formatyear(self, theyear, width=3):
438 Return a formatted year as a table of tables.
440 v = []
441 a = v.append
442 width = max(width, 1)
443 a('<table border="0" cellpadding="0" cellspacing="0" class="year">')
444 a('\n')
445 a('<tr><th colspan="%d" class="year">%s</th></tr>' % (width, theyear))
446 for i in xrange(January, January+12, width):
447 # months in this row
448 months = xrange(i, min(i+width, 13))
449 a('<tr>')
450 for m in months:
451 a('<td>')
452 a(self.formatmonth(theyear, m, withyear=False))
453 a('</td>')
454 a('</tr>')
455 a('</table>')
456 return ''.join(v)
458 def formatyearpage(self, theyear, width=3, css='calendar.css', encoding=None):
460 Return a formatted year as a complete HTML page.
462 if encoding is None:
463 encoding = sys.getdefaultencoding()
464 v = []
465 a = v.append
466 a('<?xml version="1.0" encoding="%s"?>\n' % encoding)
467 a('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n')
468 a('<html>\n')
469 a('<head>\n')
470 a('<meta http-equiv="Content-Type" content="text/html; charset=%s" />\n' % encoding)
471 if css is not None:
472 a('<link rel="stylesheet" type="text/css" href="%s" />\n' % css)
473 a('<title>Calendar for %d</title\n' % theyear)
474 a('</head>\n')
475 a('<body>\n')
476 a(self.formatyear(theyear, width))
477 a('</body>\n')
478 a('</html>\n')
479 return ''.join(v).encode(encoding, "xmlcharrefreplace")
482 class LocaleTextCalendar(TextCalendar):
484 This class can be passed a locale name in the constructor and will return
485 month and weekday names in the specified locale. If this locale includes
486 an encoding all strings containing month and weekday names will be returned
487 as unicode.
490 def __init__(self, firstweekday=0, locale=None):
491 TextCalendar.__init__(self, firstweekday)
492 if locale is None:
493 locale = locale.getdefaultlocale()
494 self.locale = locale
496 def formatweekday(self, day, width):
497 oldlocale = locale.setlocale(locale.LC_TIME, self.locale)
498 try:
499 encoding = locale.getlocale(locale.LC_TIME)[1]
500 if width >= 9:
501 names = day_name
502 else:
503 names = day_abbr
504 name = names[day]
505 if encoding is not None:
506 name = name.decode(encoding)
507 result = name[:width].center(width)
508 finally:
509 locale.setlocale(locale.LC_TIME, oldlocale)
510 return result
512 def formatmonthname(self, theyear, themonth, width, withyear=True):
513 oldlocale = locale.setlocale(locale.LC_TIME, self.locale)
514 try:
515 encoding = locale.getlocale(locale.LC_TIME)[1]
516 s = month_name[themonth]
517 if encoding is not None:
518 s = s.decode(encoding)
519 if withyear:
520 s = "%s %r" % (s, theyear)
521 result = s.center(width)
522 finally:
523 locale.setlocale(locale.LC_TIME, oldlocale)
524 return result
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
532 as unicode.
534 def __init__(self, firstweekday=0, locale=None):
535 HTMLCalendar.__init__(self, firstweekday)
536 if locale is None:
537 locale = locale.getdefaultlocale()
538 self.locale = locale
540 def formatweekday(self, day):
541 oldlocale = locale.setlocale(locale.LC_TIME, self.locale)
542 try:
543 encoding = locale.getlocale(locale.LC_TIME)[1]
544 s = day_abbr[day]
545 if encoding is not None:
546 s = s.decode(encoding)
547 result = '<th class="%s">%s</th>' % (self.cssclasses[day], s)
548 finally:
549 locale.setlocale(locale.LC_TIME, oldlocale)
550 return result
552 def formatmonthname(self, theyear, themonth, withyear=True):
553 oldlocale = locale.setlocale(locale.LC_TIME, self.locale)
554 try:
555 encoding = locale.getlocale(locale.LC_TIME)[1]
556 s = month_name[themonth]
557 if encoding is not None:
558 s = s.decode(encoding)
559 if withyear:
560 s = '%s %s' % (s, theyear)
561 result = '<tr><th colspan="7" class="month">%s</th></tr>' % s
562 finally:
563 locale.setlocale(locale.LC_TIME, oldlocale)
564 return result
567 # Support for old module level interface
568 c = TextCalendar()
570 firstweekday = c.getfirstweekday
572 def setfirstweekday(firstweekday):
573 if not MONDAY <= firstweekday <= SUNDAY:
574 raise IllegalWeekdayError(firstweekday)
575 c.firstweekday = firstweekday
577 monthcalendar = c.monthdayscalendar
578 prweek = c.prweek
579 week = c.formatweek
580 weekheader = c.formatweekheader
581 prmonth = c.prmonth
582 month = c.formatmonth
583 calendar = c.formatyear
584 prcal = c.pryear
587 # Spacing of month columns for multi-column year calendar
588 _colwidth = 7*3 - 1 # Amount printed by prweek()
589 _spacing = 6 # Number of spaces between columns
592 def format(cols, colwidth=_colwidth, spacing=_spacing):
593 """Prints multi-column formatting for year calendars"""
594 print formatstring(cols, colwidth, spacing)
597 def formatstring(cols, colwidth=_colwidth, spacing=_spacing):
598 """Returns a string formatted from n strings, centered within n columns."""
599 spacing *= ' '
600 return spacing.join(c.center(colwidth) for c in cols)
603 EPOCH = 1970
604 _EPOCH_ORD = datetime.date(EPOCH, 1, 1).toordinal()
607 def timegm(tuple):
608 """Unrelated but handy function to calculate Unix timestamp from GMT."""
609 year, month, day, hour, minute, second = tuple[:6]
610 days = datetime.date(year, month, 1).toordinal() - _EPOCH_ORD + day - 1
611 hours = days*24 + hour
612 minutes = hours*60 + minute
613 seconds = minutes*60 + second
614 return seconds
617 def main(args):
618 import optparse
619 parser = optparse.OptionParser(usage="usage: %prog [options] [year [month]]")
620 parser.add_option(
621 "-w", "--width",
622 dest="width", type="int", default=2,
623 help="width of date column (default 2, text only)"
625 parser.add_option(
626 "-l", "--lines",
627 dest="lines", type="int", default=1,
628 help="number of lines for each week (default 1, text only)"
630 parser.add_option(
631 "-s", "--spacing",
632 dest="spacing", type="int", default=6,
633 help="spacing between months (default 6, text only)"
635 parser.add_option(
636 "-m", "--months",
637 dest="months", type="int", default=3,
638 help="months per row (default 3, text only)"
640 parser.add_option(
641 "-c", "--css",
642 dest="css", default="calendar.css",
643 help="CSS to use for page (html only)"
645 parser.add_option(
646 "-L", "--locale",
647 dest="locale", default=None,
648 help="locale to be used from month and weekday names"
650 parser.add_option(
651 "-e", "--encoding",
652 dest="encoding", default=None,
653 help="Encoding to use for output"
655 parser.add_option(
656 "-t", "--type",
657 dest="type", default="text",
658 choices=("text", "html"),
659 help="output type (text or html)"
662 (options, args) = parser.parse_args(args)
664 if options.locale and not options.encoding:
665 parser.error("if --locale is specified --encoding is required")
666 sys.exit(1)
668 if options.type == "html":
669 if options.locale:
670 cal = LocaleHTMLCalendar(locale=options.locale)
671 else:
672 cal = HTMLCalendar()
673 encoding = options.encoding
674 if encoding is None:
675 encoding = sys.getdefaultencoding()
676 optdict = dict(encoding=encoding, css=options.css)
677 if len(args) == 1:
678 print cal.formatyearpage(datetime.date.today().year, **optdict)
679 elif len(args) == 2:
680 print cal.formatyearpage(int(args[1]), **optdict)
681 else:
682 parser.error("incorrect number of arguments")
683 sys.exit(1)
684 else:
685 if options.locale:
686 cal = LocaleTextCalendar(locale=options.locale)
687 else:
688 cal = TextCalendar()
689 optdict = dict(w=options.width, l=options.lines)
690 if len(args) != 3:
691 optdict["c"] = options.spacing
692 optdict["m"] = options.months
693 if len(args) == 1:
694 result = cal.formatyear(datetime.date.today().year, **optdict)
695 elif len(args) == 2:
696 result = cal.formatyear(int(args[1]), **optdict)
697 elif len(args) == 3:
698 result = cal.formatmonth(int(args[1]), int(args[2]), **optdict)
699 else:
700 parser.error("incorrect number of arguments")
701 sys.exit(1)
702 if options.encoding:
703 result = result.encode(options.encoding)
704 print result
707 if __name__ == "__main__":
708 main(sys.argv)