diff options
author | Walter Dörwald <walter@livinglogic.de> | 2006-03-31 15:26:22 (GMT) |
---|---|---|
committer | Walter Dörwald <walter@livinglogic.de> | 2006-03-31 15:26:22 (GMT) |
commit | 58917a608336e5eb316664c118aa721defe0b39d (patch) | |
tree | 85589ab401fa010237bb7c9daf655c990e06e771 /Lib/calendar.py | |
parent | 3f144f9fe07474b0086a96e49656a9e046936f57 (diff) | |
download | cpython-58917a608336e5eb316664c118aa721defe0b39d.zip cpython-58917a608336e5eb316664c118aa721defe0b39d.tar.gz cpython-58917a608336e5eb316664c118aa721defe0b39d.tar.bz2 |
Bug #947906: An object oriented interface has been added to the calendar
module. It's possible to generate HTML calendar now and the module can be
called as a script (e.g. via ``python -mcalendar``).
Diffstat (limited to 'Lib/calendar.py')
-rw-r--r-- | Lib/calendar.py | 589 |
1 files changed, 471 insertions, 118 deletions
diff --git a/Lib/calendar.py b/Lib/calendar.py index 3ffcff5..7569621 100644 --- a/Lib/calendar.py +++ b/Lib/calendar.py @@ -5,17 +5,31 @@ default, these calendars have Monday as the first day of the week, and Sunday as the last (the European convention). Use setfirstweekday() to set the first day of the week (0=Monday, 6=Sunday).""" -import datetime +import sys, datetime -__all__ = ["error","setfirstweekday","firstweekday","isleap", - "leapdays","weekday","monthrange","monthcalendar", - "prmonth","month","prcal","calendar","timegm", - "month_name", "month_abbr", "day_name", "day_abbr", - "weekheader"] +__all__ = ["IllegalMonthError", "IllegalWeekdayError", "setfirstweekday", + "firstweekday", "isleap", "leapdays", "weekday", "monthrange", + "monthcalendar", "prmonth", "month", "prcal", "calendar", + "timegm", "month_name", "month_abbr", "day_name", "day_abbr"] # Exception raised for bad input (with string parameter for details) error = ValueError +# Exceptions raised for bad input +class IllegalMonthError(ValueError): + def __init__(self, month): + self.month = month + def __str__(self): + return "bad month number %r; must be 1-12" % self.month + + +class IllegalWeekdayError(ValueError): + def __init__(self, weekday): + self.weekday = weekday + def __str__(self): + return "bad weekday number %r; must be 0 (Monday) to 6 (Sunday)" % self.weekday + + # Constants for months referenced later January = 1 February = 2 @@ -30,7 +44,7 @@ mdays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] class _localized_month: - _months = [datetime.date(2001, i+1, 1).strftime for i in range(12)] + _months = [datetime.date(2001, i+1, 1).strftime for i in xrange(12)] _months.insert(0, lambda x: "") def __init__(self, format): @@ -46,10 +60,11 @@ class _localized_month: def __len__(self): return 13 + class _localized_day: # January 1, 2001, was a Monday. - _days = [datetime.date(2001, 1, i+1).strftime for i in range(7)] + _days = [datetime.date(2001, 1, i+1).strftime for i in xrange(7)] def __init__(self, format): self.format = format @@ -64,6 +79,7 @@ class _localized_day: def __len__(self): return 7 + # Full and abbreviated names of weekdays day_name = _localized_day('%A') day_abbr = _localized_day('%a') @@ -75,23 +91,12 @@ month_abbr = _localized_month('%b') # Constants for weekdays (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7) -_firstweekday = 0 # 0 = Monday, 6 = Sunday - -def firstweekday(): - return _firstweekday - -def setfirstweekday(weekday): - """Set weekday (Monday=0, Sunday=6) to start each week.""" - global _firstweekday - if not MONDAY <= weekday <= SUNDAY: - raise ValueError, \ - 'bad weekday number; must be 0 (Monday) to 6 (Sunday)' - _firstweekday = weekday def isleap(year): """Return 1 for leap years, 0 for non-leap years.""" return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) + def leapdays(y1, y2): """Return number of leap years in range [y1, y2). Assume y1 <= y2.""" @@ -99,128 +104,414 @@ def leapdays(y1, y2): y2 -= 1 return (y2//4 - y1//4) - (y2//100 - y1//100) + (y2//400 - y1//400) + def weekday(year, month, day): """Return weekday (0-6 ~ Mon-Sun) for year (1970-...), month (1-12), day (1-31).""" return datetime.date(year, month, day).weekday() + def monthrange(year, month): """Return weekday (0-6 ~ Mon-Sun) and number of days (28-31) for year, month.""" if not 1 <= month <= 12: - raise ValueError, 'bad month number' + raise IllegalMonthError(month) day1 = weekday(year, month, 1) ndays = mdays[month] + (month == February and isleap(year)) return day1, ndays -def monthcalendar(year, month): - """Return a matrix representing a month's calendar. - Each row represents a week; days outside this month are zero.""" - day1, ndays = monthrange(year, month) - rows = [] - r7 = range(7) - day = (_firstweekday - day1 + 6) % 7 - 5 # for leading 0's in first week - while day <= ndays: - row = [0, 0, 0, 0, 0, 0, 0] - for i in r7: - if 1 <= day <= ndays: row[i] = day - day = day + 1 - rows.append(row) - return rows - -def prweek(theweek, width): - """Print a single week (no newline).""" - print week(theweek, width), - -def week(theweek, width): - """Returns a single week in a string (no newline).""" - days = [] - for day in theweek: + +class Calendar(object): + """ + Base calendar class. This class doesn't do any formatting. It simply + provides data to subclasses. + """ + + def __init__(self, firstweekday=0): + self._firstweekday = firstweekday # 0 = Monday, 6 = Sunday + + def firstweekday(self): + return self._firstweekday + + def setfirstweekday(self, weekday): + """ + Set weekday (Monday=0, Sunday=6) to start each week. + """ + if not MONDAY <= weekday <= SUNDAY: + raise IllegalWeekdayError(weekday) + self._firstweekday = weekday + + def iterweekdays(self): + """ + Return a iterator for one week of weekday numbers starting with the + configured first one. + """ + for i in xrange(self._firstweekday, self._firstweekday + 7): + yield i%7 + + def itermonthdates(self, year, month): + """ + Return an iterator for one month. The iterator will yield datetime.date + values and will always iterate through complete weeks, so it will yield + dates outside the specified month. + """ + date = datetime.date(year, month, 1) + # Go back to the beginning of the week + days = (date.weekday() - self._firstweekday) % 7 + date -= datetime.timedelta(days=days) + oneday = datetime.timedelta(days=1) + while True: + yield date + date += oneday + if date.month != month and date.weekday() == self._firstweekday: + break + + def itermonthdays2(self, year, month): + """ + Like itermonthdates(), but will yield (day number, weekday number) + tuples. For days outside the specified month the day number is 0. + """ + for date in self.itermonthdates(year, month): + if date.month != month: + yield (0, date.weekday()) + else: + yield (date.day, date.weekday()) + + def itermonthdays(self, year, month): + """ + Like itermonthdates(), but will yield day numbers tuples. For days + outside the specified month the day number is 0. + """ + for date in self.itermonthdates(year, month): + if date.month != month: + yield 0 + else: + yield date.day + + def monthdatescalendar(self, year, month): + """ + Return a matrix (list of lists) representing a month's calendar. + Each row represents a week; week entries are datetime.date values. + """ + dates = list(self.itermonthdates(year, month)) + return [ dates[i:i+7] for i in xrange(0, len(dates), 7) ] + + def monthdays2calendar(self, year, month): + """ + Return a matrix representing a month's calendar. + Each row represents a week; week entries are + (day number, weekday number) tuples. Day numbers outside this month + are zero. + """ + days = list(self.itermonthdays2(year, month)) + return [ days[i:i+7] for i in xrange(0, len(days), 7) ] + + def monthdayscalendar(self, year, month): + """ + Return a matrix representing a month's calendar. + Each row represents a week; days outside this month are zero. + """ + days = list(self.itermonthdays(year, month)) + return [ days[i:i+7] for i in xrange(0, len(days), 7) ] + + def yeardatescalendar(self, year, width=3): + """ + Return the data for the specified year ready for formatting. The return + value is a list of month rows. Each month row contains upto width months. + Each month contains between 4 and 6 weeks and each week contains 1-7 + days. Days are datetime.date objects. + """ + months = [ + self.monthdatescalendar(year, i) + for i in xrange(January, January+12) + ] + return [months[i:i+width] for i in xrange(0, len(months), width) ] + + def yeardays2calendar(self, year, width=3): + """ + Return the data for the specified year ready for formatting (similar to + yeardatescalendar()). Entries in the week lists are + (day number, weekday number) tuples. Day numbers outside this month are + zero. + """ + months = [ + self.monthdays2calendar(year, i) + for i in xrange(January, January+12) + ] + return [months[i:i+width] for i in xrange(0, len(months), width) ] + + def yeardayscalendar(self, year, width=3): + """ + Return the data for the specified year ready for formatting (similar to + yeardatescalendar()). Entries in the week lists are day numbers. + Day numbers outside this month are zero. + """ + months = [ + self.monthdayscalendar(year, i) + for i in xrange(January, January+12) + ] + return [months[i:i+width] for i in xrange(0, len(months), width) ] + + +class TextCalendar(Calendar): + """ + Subclass of Calendar that outputs a calendar as a simple plain text + similar to the UNIX program cal. + """ + + def prweek(theweek, width): + """ + Print a single week (no newline). + """ + print self.week(theweek, width), + + def formatday(self, day, weekday, width): + """ + Returns a formatted day. + """ if day == 0: s = '' else: s = '%2i' % day # right-align single-digit days - days.append(s.center(width)) - return ' '.join(days) - -def weekheader(width): - """Return a header for a week.""" - if width >= 9: - names = day_name - else: - names = day_abbr - days = [] - for i in range(_firstweekday, _firstweekday + 7): - days.append(names[i%7][:width].center(width)) - return ' '.join(days) - -def prmonth(theyear, themonth, w=0, l=0): - """Print a month's calendar.""" - print month(theyear, themonth, w, l), - -def month(theyear, themonth, w=0, l=0): - """Return a month's calendar string (multi-line).""" - w = max(2, w) - l = max(1, l) - s = ("%s %r" % (month_name[themonth], theyear)).center( - 7 * (w + 1) - 1).rstrip() + \ - '\n' * l + weekheader(w).rstrip() + '\n' * l - for aweek in monthcalendar(theyear, themonth): - s = s + week(aweek, w).rstrip() + '\n' * l - return s[:-l] + '\n' - -# Spacing of month columns for 3-column year calendar + return s.center(width) + + def formatweek(self, theweek, width): + """ + Returns a single week in a string (no newline). + """ + return ' '.join(self.formatday(d, wd, width) for (d, wd) in theweek) + + def formatweekday(self, day, width): + """ + Returns a formatted week day name. + """ + if width >= 9: + names = day_name + else: + names = day_abbr + return names[day][:width].center(width) + + def formatweekheader(self, width): + """ + Return a header for a week. + """ + return ' '.join(self.formatweekday(i, width) for i in self.iterweekdays()) + + def formatmonthname(self, theyear, themonth, width): + """ + Return a formatted month name. + """ + s = "%s %r" % (month_name[themonth], theyear) + return s.center(width) + + def prmonth(self, theyear, themonth, w=0, l=0): + """ + Print a month's calendar. + """ + print self.formatmonth(theyear, themonth, w, l), + + def formatmonth(self, theyear, themonth, w=0, l=0): + """ + Return a month's calendar string (multi-line). + """ + w = max(2, w) + l = max(1, l) + s = self.formatmonthname(theyear, themonth, 7 * (w + 1) - 1) + s = s.rstrip() + s += '\n' * l + s += self.formatweekheader(w).rstrip() + s += '\n' * l + for week in self.monthdays2calendar(theyear, themonth): + s += self.formatweek(week, w).rstrip() + s += '\n' * l + return s + + def formatyear(self, theyear, w=2, l=1, c=6, m=3): + """ + Returns a year's calendar as a multi-line string. + """ + w = max(2, w) + l = max(1, l) + c = max(2, c) + colwidth = (w + 1) * 7 - 1 + v = [] + a = v.append + a(repr(theyear).center(colwidth*m+c*(m-1)).rstrip()) + a('\n'*l) + header = self.formatweekheader(w) + for (i, row) in enumerate(self.yeardays2calendar(theyear, m)): + # months in this row + months = xrange(m*i+1, min(m*(i+1)+1, 13)) + a('\n'*l) + a(formatstring((month_name[k] for k in months), colwidth, c).rstrip()) + a('\n'*l) + a(formatstring((header for k in months), colwidth, c).rstrip()) + a('\n'*l) + # max number of weeks for this row + height = max(len(cal) for cal in row) + for j in xrange(height): + weeks = [] + for cal in row: + if j >= len(cal): + weeks.append('') + else: + weeks.append(self.formatweek(cal[j], w)) + a(formatstring(weeks, colwidth, c).rstrip()) + a('\n' * l) + return ''.join(v) + + def pryear(self, theyear, w=0, l=0, c=6, m=3): + """Print a year's calendar.""" + print self.formatyear(theyear, w, l, c, m) + + +class HTMLCalendar(Calendar): + """ + This calendar returns complete HTML pages. + """ + + # CSS classes for the day <td>s + cssclasses = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] + + def formatday(self, day, weekday): + """ + Return a day as a table cell. + """ + if day == 0: + return '<td class="noday"> </td>' # day outside month + else: + return '<td class="%s">%d</td>' % (self.cssclasses[weekday], day) + + def formatweek(self, theweek): + """ + Return a complete week as a table row. + """ + s = ''.join(self.formatday(d, wd) for (d, wd) in theweek) + return '<tr>%s</tr>' % s + + def formatweekday(self, day): + """ + Return a weekday name as a table header. + """ + return '<th class="%s">%s</th>' % (self.cssclasses[day], day_abbr[day]) + + def formatweekheader(self): + """ + Return a header for a week as a table row. + """ + s = ''.join(self.formatweekday(i) for i in self.iterweekdays()) + return '<tr>%s</tr>' % s + + def formatmonthname(self, theyear, themonth, withyear=True): + """ + Return a month name as a table row. + """ + if withyear: + s = '%s %s' % (month_name[themonth], theyear) + else: + s = '%s' % month_name[themonth] + return '<tr><th colspan="7" class="month">%s</th></tr>' % s + + def formatmonth(self, theyear, themonth, withyear=True): + """ + Return a formatted month as a table. + """ + v = [] + a = v.append + a('<table border="0" cellpadding="0" cellspacing="0" class="month">') + a('\n') + a(self.formatmonthname(theyear, themonth, withyear=withyear)) + a('\n') + a(self.formatweekheader()) + a('\n') + for week in self.monthdays2calendar(theyear, themonth): + a(self.formatweek(week)) + a('\n') + a('</table>') + a('\n') + return ''.join(v) + + def formatyear(self, theyear, width=3): + """ + Return a formatted year as a table of tables. + """ + v = [] + a = v.append + width = max(width, 1) + a('<table border="0" cellpadding="0" cellspacing="0" class="year">') + a('\n') + a('<tr><th colspan="%d" class="year">%s</th></tr>' % (width, theyear)) + for i in xrange(January, January+12, width): + # months in this row + months = xrange(i, min(i+width, 13)) + a('<tr>') + for m in months: + a('<td>') + a(self.formatmonth(theyear, m, withyear=False)) + a('</td>') + a('</tr>') + a('</table>') + return ''.join(v) + + def formatyearpage(self, theyear, width=3, css='calendar.css', encoding=None): + """ + Return a formatted year as a complete HTML page. + """ + if encoding is None: + encoding = sys.getdefaultencoding() + v = [] + a = v.append + a('<?xml version="1.0" encoding="%s"?>\n' % encoding) + a('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n') + a('<html>\n') + a('<head>\n') + a('<meta http-equiv="Content-Type" content="text/html; charset=%s" />\n' % encoding) + if css is not None: + a('<link rel="stylesheet" type="text/css" href="%s" />\n' % css) + a('<title>Calendar for %d</title\n' % theyear) + a('</head>\n') + a('<body>\n') + a(self.formatyear(theyear, width)) + a('</body>\n') + a('</html>\n') + return ''.join(v).encode(encoding) + + +# Support for old module level interface +c = TextCalendar() + +firstweekday = c.firstweekday +setfirstweekday = c.setfirstweekday +monthcalendar = c.monthdayscalendar +prweek = c.prweek +week = c.formatweek +weekheader = c.formatweekheader +prmonth = c.prmonth +month = c.formatmonth +calendar = c.formatyear +prcal = c.pryear + + +# Spacing of month columns for multi-column year calendar _colwidth = 7*3 - 1 # Amount printed by prweek() _spacing = 6 # Number of spaces between columns -def format3c(a, b, c, colwidth=_colwidth, spacing=_spacing): - """Prints 3-column formatting for year calendars""" - print format3cstring(a, b, c, colwidth, spacing) - -def format3cstring(a, b, c, colwidth=_colwidth, spacing=_spacing): - """Returns a string formatted from 3 strings, centered within 3 columns.""" - return (a.center(colwidth) + ' ' * spacing + b.center(colwidth) + - ' ' * spacing + c.center(colwidth)) - -def prcal(year, w=0, l=0, c=_spacing): - """Print a year's calendar.""" - print calendar(year, w, l, c), - -def calendar(year, w=0, l=0, c=_spacing): - """Returns a year's calendar as a multi-line string.""" - w = max(2, w) - l = max(1, l) - c = max(2, c) - colwidth = (w + 1) * 7 - 1 - s = repr(year).center(colwidth * 3 + c * 2).rstrip() + '\n' * l - header = weekheader(w) - header = format3cstring(header, header, header, colwidth, c).rstrip() - for q in range(January, January+12, 3): - s = (s + '\n' * l + - format3cstring(month_name[q], month_name[q+1], month_name[q+2], - colwidth, c).rstrip() + - '\n' * l + header + '\n' * l) - data = [] - height = 0 - for amonth in range(q, q + 3): - cal = monthcalendar(year, amonth) - if len(cal) > height: - height = len(cal) - data.append(cal) - for i in range(height): - weeks = [] - for cal in data: - if i >= len(cal): - weeks.append('') - else: - weeks.append(week(cal[i], w)) - s = s + format3cstring(weeks[0], weeks[1], weeks[2], - colwidth, c).rstrip() + '\n' * l - return s[:-l] + '\n' + +def format(cols, colwidth=_colwidth, spacing=_spacing): + """Prints multi-column formatting for year calendars""" + print formatstring(cols, colwidth, spacing) + + +def formatstring(cols, colwidth=_colwidth, spacing=_spacing): + """Returns a string formatted from n strings, centered within n columns.""" + spacing *= ' ' + return spacing.join(c.center(colwidth) for c in cols) + EPOCH = 1970 _EPOCH_ORD = datetime.date(EPOCH, 1, 1).toordinal() + def timegm(tuple): """Unrelated but handy function to calculate Unix timestamp from GMT.""" year, month, day, hour, minute, second = tuple[:6] @@ -229,3 +520,65 @@ def timegm(tuple): minutes = hours*60 + minute seconds = minutes*60 + second return seconds + + +def main(args): + import optparse + parser = optparse.OptionParser(usage="usage: %prog [options] [year] [month]") + parser.add_option("-w", "--width", + dest="width", type="int", default=2, + help="width of date column (default 2, text only)") + parser.add_option("-l", "--lines", + dest="lines", type="int", default=1, + help="number of lines for each week (default 1, text only)") + parser.add_option("-s", "--spacing", + dest="spacing", type="int", default=6, + help="spacing between months (default 6, text only)") + parser.add_option("-m", "--months", + dest="months", type="int", default=3, + help="months per row (default 3, text only)") + parser.add_option("-c", "--css", + dest="css", default="calendar.css", + help="CSS to use for page (html only)") + parser.add_option("-e", "--encoding", + dest="encoding", default=None, + help="Encoding to use for CSS output (html only)") + parser.add_option("-t", "--type", + dest="type", default="text", + choices=("text", "html"), + help="output type (text or html)") + + (options, args) = parser.parse_args(args) + + if options.type == "html": + cal = HTMLCalendar() + encoding = options.encoding + if encoding is None: + encoding = sys.getdefaultencoding() + optdict = dict(encoding=encoding, css=options.css) + if len(args) == 1: + print cal.formatyearpage(datetime.date.today().year, **optdict) + elif len(args) == 2: + print cal.formatyearpage(int(args[1]), **optdict) + else: + parser.error("incorrect number of arguments") + sys.exit(1) + else: + cal = TextCalendar() + optdict = dict(w=options.width, l=options.lines) + if len(args) != 3: + optdict["c"] = options.spacing + optdict["m"] = options.months + if len(args) == 1: + print cal.formatyear(datetime.date.today().year, **optdict) + elif len(args) == 2: + print cal.formatyear(int(args[1]), **optdict) + elif len(args) == 3: + print cal.formatmonth(int(args[1]), int(args[2]), **optdict) + else: + parser.error("incorrect number of arguments") + sys.exit(1) + + +if __name__ == "__main__": + main(sys.argv) |