PageRenderTime 42ms CodeModel.GetById 2ms app.highlight 33ms RepoModel.GetById 1ms app.codeStats 0ms

/Lib/calendar.py

http://unladen-swallow.googlecode.com/
Python | 704 lines | 678 code | 12 blank | 14 comment | 7 complexity | 5c2fa451bb3831b389251f0b3adabc03 MD5 | raw file
  1"""Calendar printing functions
  2
  3Note when comparing these calendars to the ones printed by cal(1): By
  4default, these calendars have Monday as the first day of the week, and
  5Sunday as the last (the European convention). Use setfirstweekday() to
  6set the first day of the week (0=Monday, 6=Sunday)."""
  7
  8import sys
  9import datetime
 10import locale as _locale
 11
 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"]
 16
 17# Exception raised for bad input (with string parameter for details)
 18error = ValueError
 19
 20# Exceptions raised for bad input
 21class IllegalMonthError(ValueError):
 22    def __init__(self, month):
 23        self.month = month
 24    def __str__(self):
 25        return "bad month number %r; must be 1-12" % self.month
 26
 27
 28class IllegalWeekdayError(ValueError):
 29    def __init__(self, weekday):
 30        self.weekday = weekday
 31    def __str__(self):
 32        return "bad weekday number %r; must be 0 (Monday) to 6 (Sunday)" % self.weekday
 33
 34
 35# Constants for months referenced later
 36January = 1
 37February = 2
 38
 39# Number of days per month (except for February in leap years)
 40mdays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
 41
 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.
 46
 47class _localized_month:
 48
 49    _months = [datetime.date(2001, i+1, 1).strftime for i in range(12)]
 50    _months.insert(0, lambda x: "")
 51
 52    def __init__(self, format):
 53        self.format = format
 54
 55    def __getitem__(self, i):
 56        funcs = self._months[i]
 57        if isinstance(i, slice):
 58            return [f(self.format) for f in funcs]
 59        else:
 60            return funcs(self.format)
 61
 62    def __len__(self):
 63        return 13
 64
 65
 66class _localized_day:
 67
 68    # January 1, 2001, was a Monday.
 69    _days = [datetime.date(2001, 1, i+1).strftime for i in range(7)]
 70
 71    def __init__(self, format):
 72        self.format = format
 73
 74    def __getitem__(self, i):
 75        funcs = self._days[i]
 76        if isinstance(i, slice):
 77            return [f(self.format) for f in funcs]
 78        else:
 79            return funcs(self.format)
 80
 81    def __len__(self):
 82        return 7
 83
 84
 85# Full and abbreviated names of weekdays
 86day_name = _localized_day('%A')
 87day_abbr = _localized_day('%a')
 88
 89# Full and abbreviated names of months (1-based arrays!!!)
 90month_name = _localized_month('%B')
 91month_abbr = _localized_month('%b')
 92
 93# Constants for weekdays
 94(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7)
 95
 96
 97def isleap(year):
 98    """Return 1 for leap years, 0 for non-leap years."""
 99    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
100
101
102def leapdays(y1, y2):
103    """Return number of leap years in range [y1, y2).
104       Assume y1 <= y2."""
105    y1 -= 1
106    y2 -= 1
107    return (y2//4 - y1//4) - (y2//100 - y1//100) + (y2//400 - y1//400)
108
109
110def weekday(year, month, day):
111    """Return weekday (0-6 ~ Mon-Sun) for year (1970-...), month (1-12),
112       day (1-31)."""
113    return datetime.date(year, month, day).weekday()
114
115
116def monthrange(year, month):
117    """Return weekday (0-6 ~ Mon-Sun) and number of days (28-31) for
118       year, month."""
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))
123    return day1, ndays
124
125
126class Calendar(object):
127    """
128    Base calendar class. This class doesn't do any formatting. It simply
129    provides data to subclasses.
130    """
131
132    def __init__(self, firstweekday=0):
133        self.firstweekday = firstweekday # 0 = Monday, 6 = Sunday
134
135    def getfirstweekday(self):
136        return self._firstweekday % 7
137
138    def setfirstweekday(self, firstweekday):
139        self._firstweekday = firstweekday
140
141    firstweekday = property(getfirstweekday, setfirstweekday)
142
143    def iterweekdays(self):
144        """
145        Return a iterator for one week of weekday numbers starting with the
146        configured first one.
147        """
148        for i in range(self.firstweekday, self.firstweekday + 7):
149            yield i%7
150
151    def itermonthdates(self, year, month):
152        """
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.
156        """
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)
162        while True:
163            yield date
164            date += oneday
165            if date.month != month and date.weekday() == self.firstweekday:
166                break
167
168    def itermonthdays2(self, year, month):
169        """
170        Like itermonthdates(), but will yield (day number, weekday number)
171        tuples. For days outside the specified month the day number is 0.
172        """
173        for date in self.itermonthdates(year, month):
174            if date.month != month:
175                yield (0, date.weekday())
176            else:
177                yield (date.day, date.weekday())
178
179    def itermonthdays(self, year, month):
180        """
181        Like itermonthdates(), but will yield day numbers. For days outside
182        the specified month the day number is 0.
183        """
184        for date in self.itermonthdates(year, month):
185            if date.month != month:
186                yield 0
187            else:
188                yield date.day
189
190    def monthdatescalendar(self, year, month):
191        """
192        Return a matrix (list of lists) representing a month's calendar.
193        Each row represents a week; week entries are datetime.date values.
194        """
195        dates = list(self.itermonthdates(year, month))
196        return [ dates[i:i+7] for i in range(0, len(dates), 7) ]
197
198    def monthdays2calendar(self, year, month):
199        """
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
203        are zero.
204        """
205        days = list(self.itermonthdays2(year, month))
206        return [ days[i:i+7] for i in range(0, len(days), 7) ]
207
208    def monthdayscalendar(self, year, month):
209        """
210        Return a matrix representing a month's calendar.
211        Each row represents a week; days outside this month are zero.
212        """
213        days = list(self.itermonthdays(year, month))
214        return [ days[i:i+7] for i in range(0, len(days), 7) ]
215
216    def yeardatescalendar(self, year, width=3):
217        """
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.
222        """
223        months = [
224            self.monthdatescalendar(year, i)
225            for i in range(January, January+12)
226        ]
227        return [months[i:i+width] for i in range(0, len(months), width) ]
228
229    def yeardays2calendar(self, year, width=3):
230        """
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
234        zero.
235        """
236        months = [
237            self.monthdays2calendar(year, i)
238            for i in range(January, January+12)
239        ]
240        return [months[i:i+width] for i in range(0, len(months), width) ]
241
242    def yeardayscalendar(self, year, width=3):
243        """
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.
247        """
248        months = [
249            self.monthdayscalendar(year, i)
250            for i in range(January, January+12)
251        ]
252        return [months[i:i+width] for i in range(0, len(months), width) ]
253
254
255class TextCalendar(Calendar):
256    """
257    Subclass of Calendar that outputs a calendar as a simple plain text
258    similar to the UNIX program cal.
259    """
260
261    def prweek(self, theweek, width):
262        """
263        Print a single week (no newline).
264        """
265        print self.formatweek(theweek, width),
266
267    def formatday(self, day, weekday, width):
268        """
269        Returns a formatted day.
270        """
271        if day == 0:
272            s = ''
273        else:
274            s = '%2i' % day             # right-align single-digit days
275        return s.center(width)
276
277    def formatweek(self, theweek, width):
278        """
279        Returns a single week in a string (no newline).
280        """
281        return ' '.join(self.formatday(d, wd, width) for (d, wd) in theweek)
282
283    def formatweekday(self, day, width):
284        """
285        Returns a formatted week day name.
286        """
287        if width >= 9:
288            names = day_name
289        else:
290            names = day_abbr
291        return names[day][:width].center(width)
292
293    def formatweekheader(self, width):
294        """
295        Return a header for a week.
296        """
297        return ' '.join(self.formatweekday(i, width) for i in self.iterweekdays())
298
299    def formatmonthname(self, theyear, themonth, width, withyear=True):
300        """
301        Return a formatted month name.
302        """
303        s = month_name[themonth]
304        if withyear:
305            s = "%s %r" % (s, theyear)
306        return s.center(width)
307
308    def prmonth(self, theyear, themonth, w=0, l=0):
309        """
310        Print a month's calendar.
311        """
312        print self.formatmonth(theyear, themonth, w, l),
313
314    def formatmonth(self, theyear, themonth, w=0, l=0):
315        """
316        Return a month's calendar string (multi-line).
317        """
318        w = max(2, w)
319        l = max(1, l)
320        s = self.formatmonthname(theyear, themonth, 7 * (w + 1) - 1)
321        s = s.rstrip()
322        s += '\n' * l
323        s += self.formatweekheader(w).rstrip()
324        s += '\n' * l
325        for week in self.monthdays2calendar(theyear, themonth):
326            s += self.formatweek(week, w).rstrip()
327            s += '\n' * l
328        return s
329
330    def formatyear(self, theyear, w=2, l=1, c=6, m=3):
331        """
332        Returns a year's calendar as a multi-line string.
333        """
334        w = max(2, w)
335        l = max(1, l)
336        c = max(2, c)
337        colwidth = (w + 1) * 7 - 1
338        v = []
339        a = v.append
340        a(repr(theyear).center(colwidth*m+c*(m-1)).rstrip())
341        a('\n'*l)
342        header = self.formatweekheader(w)
343        for (i, row) in enumerate(self.yeardays2calendar(theyear, m)):
344            # months in this row
345            months = range(m*i+1, min(m*(i+1)+1, 13))
346            a('\n'*l)
347            names = (self.formatmonthname(theyear, k, colwidth, False)
348                     for k in months)
349            a(formatstring(names, colwidth, c).rstrip())
350            a('\n'*l)
351            headers = (header for k in months)
352            a(formatstring(headers, colwidth, c).rstrip())
353            a('\n'*l)
354            # max number of weeks for this row
355            height = max(len(cal) for cal in row)
356            for j in range(height):
357                weeks = []
358                for cal in row:
359                    if j >= len(cal):
360                        weeks.append('')
361                    else:
362                        weeks.append(self.formatweek(cal[j], w))
363                a(formatstring(weeks, colwidth, c).rstrip())
364                a('\n' * l)
365        return ''.join(v)
366
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)
370
371
372class HTMLCalendar(Calendar):
373    """
374    This calendar returns complete HTML pages.
375    """
376
377    # CSS classes for the day <td>s
378    cssclasses = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
379
380    def formatday(self, day, weekday):
381        """
382        Return a day as a table cell.
383        """
384        if day == 0:
385            return '<td class="noday">&nbsp;</td>' # day outside month
386        else:
387            return '<td class="%s">%d</td>' % (self.cssclasses[weekday], day)
388
389    def formatweek(self, theweek):
390        """
391        Return a complete week as a table row.
392        """
393        s = ''.join(self.formatday(d, wd) for (d, wd) in theweek)
394        return '<tr>%s</tr>' % s
395
396    def formatweekday(self, day):
397        """
398        Return a weekday name as a table header.
399        """
400        return '<th class="%s">%s</th>' % (self.cssclasses[day], day_abbr[day])
401
402    def formatweekheader(self):
403        """
404        Return a header for a week as a table row.
405        """
406        s = ''.join(self.formatweekday(i) for i in self.iterweekdays())
407        return '<tr>%s</tr>' % s
408
409    def formatmonthname(self, theyear, themonth, withyear=True):
410        """
411        Return a month name as a table row.
412        """
413        if withyear:
414            s = '%s %s' % (month_name[themonth], theyear)
415        else:
416            s = '%s' % month_name[themonth]
417        return '<tr><th colspan="7" class="month">%s</th></tr>' % s
418
419    def formatmonth(self, theyear, themonth, withyear=True):
420        """
421        Return a formatted month as a table.
422        """
423        v = []
424        a = v.append
425        a('<table border="0" cellpadding="0" cellspacing="0" class="month">')
426        a('\n')
427        a(self.formatmonthname(theyear, themonth, withyear=withyear))
428        a('\n')
429        a(self.formatweekheader())
430        a('\n')
431        for week in self.monthdays2calendar(theyear, themonth):
432            a(self.formatweek(week))
433            a('\n')
434        a('</table>')
435        a('\n')
436        return ''.join(v)
437
438    def formatyear(self, theyear, width=3):
439        """
440        Return a formatted year as a table of tables.
441        """
442        v = []
443        a = v.append
444        width = max(width, 1)
445        a('<table border="0" cellpadding="0" cellspacing="0" class="year">')
446        a('\n')
447        a('<tr><th colspan="%d" class="year">%s</th></tr>' % (width, theyear))
448        for i in range(January, January+12, width):
449            # months in this row
450            months = range(i, min(i+width, 13))
451            a('<tr>')
452            for m in months:
453                a('<td>')
454                a(self.formatmonth(theyear, m, withyear=False))
455                a('</td>')
456            a('</tr>')
457        a('</table>')
458        return ''.join(v)
459
460    def formatyearpage(self, theyear, width=3, css='calendar.css', encoding=None):
461        """
462        Return a formatted year as a complete HTML page.
463        """
464        if encoding is None:
465            encoding = sys.getdefaultencoding()
466        v = []
467        a = v.append
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')
470        a('<html>\n')
471        a('<head>\n')
472        a('<meta http-equiv="Content-Type" content="text/html; charset=%s" />\n' % encoding)
473        if css is not None:
474            a('<link rel="stylesheet" type="text/css" href="%s" />\n' % css)
475        a('<title>Calendar for %d</title>\n' % theyear)
476        a('</head>\n')
477        a('<body>\n')
478        a(self.formatyear(theyear, width))
479        a('</body>\n')
480        a('</html>\n')
481        return ''.join(v).encode(encoding, "xmlcharrefreplace")
482
483
484class TimeEncoding:
485    def __init__(self, locale):
486        self.locale = locale
487
488    def __enter__(self):
489        self.oldlocale = _locale.setlocale(_locale.LC_TIME, self.locale)
490        return _locale.getlocale(_locale.LC_TIME)[1]
491
492    def __exit__(self, *args):
493        _locale.setlocale(_locale.LC_TIME, self.oldlocale)
494
495
496class LocaleTextCalendar(TextCalendar):
497    """
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
501    as unicode.
502    """
503
504    def __init__(self, firstweekday=0, locale=None):
505        TextCalendar.__init__(self, firstweekday)
506        if locale is None:
507            locale = _locale.getdefaultlocale()
508        self.locale = locale
509
510    def formatweekday(self, day, width):
511        with TimeEncoding(self.locale) as encoding:
512            if width >= 9:
513                names = day_name
514            else:
515                names = day_abbr
516            name = names[day]
517            if encoding is not None:
518                name = name.decode(encoding)
519            return name[:width].center(width)
520
521    def formatmonthname(self, theyear, themonth, width, withyear=True):
522        with TimeEncoding(self.locale) as encoding:
523            s = month_name[themonth]
524            if encoding is not None:
525                s = s.decode(encoding)
526            if withyear:
527                s = "%s %r" % (s, theyear)
528            return s.center(width)
529
530
531class LocaleHTMLCalendar(HTMLCalendar):
532    """
533    This class can be passed a locale name in the constructor and will return
534    month and weekday names in the specified locale. If this locale includes
535    an encoding all strings containing month and weekday names will be returned
536    as unicode.
537    """
538    def __init__(self, firstweekday=0, locale=None):
539        HTMLCalendar.__init__(self, firstweekday)
540        if locale is None:
541            locale = _locale.getdefaultlocale()
542        self.locale = locale
543
544    def formatweekday(self, day):
545        with TimeEncoding(self.locale) as encoding:
546            s = day_abbr[day]
547            if encoding is not None:
548                s = s.decode(encoding)
549            return '<th class="%s">%s</th>' % (self.cssclasses[day], s)
550
551    def formatmonthname(self, theyear, themonth, withyear=True):
552        with TimeEncoding(self.locale) as encoding:
553            s = month_name[themonth]
554            if encoding is not None:
555                s = s.decode(encoding)
556            if withyear:
557                s = '%s %s' % (s, theyear)
558            return '<tr><th colspan="7" class="month">%s</th></tr>' % s
559
560
561# Support for old module level interface
562c = TextCalendar()
563
564firstweekday = c.getfirstweekday
565
566def setfirstweekday(firstweekday):
567    if not MONDAY <= firstweekday <= SUNDAY:
568        raise IllegalWeekdayError(firstweekday)
569    c.firstweekday = firstweekday
570
571monthcalendar = c.monthdayscalendar
572prweek = c.prweek
573week = c.formatweek
574weekheader = c.formatweekheader
575prmonth = c.prmonth
576month = c.formatmonth
577calendar = c.formatyear
578prcal = c.pryear
579
580
581# Spacing of month columns for multi-column year calendar
582_colwidth = 7*3 - 1         # Amount printed by prweek()
583_spacing = 6                # Number of spaces between columns
584
585
586def format(cols, colwidth=_colwidth, spacing=_spacing):
587    """Prints multi-column formatting for year calendars"""
588    print formatstring(cols, colwidth, spacing)
589
590
591def formatstring(cols, colwidth=_colwidth, spacing=_spacing):
592    """Returns a string formatted from n strings, centered within n columns."""
593    spacing *= ' '
594    return spacing.join(c.center(colwidth) for c in cols)
595
596
597EPOCH = 1970
598_EPOCH_ORD = datetime.date(EPOCH, 1, 1).toordinal()
599
600
601def timegm(tuple):
602    """Unrelated but handy function to calculate Unix timestamp from GMT."""
603    year, month, day, hour, minute, second = tuple[:6]
604    days = datetime.date(year, month, 1).toordinal() - _EPOCH_ORD + day - 1
605    hours = days*24 + hour
606    minutes = hours*60 + minute
607    seconds = minutes*60 + second
608    return seconds
609
610
611def main(args):
612    import optparse
613    parser = optparse.OptionParser(usage="usage: %prog [options] [year [month]]")
614    parser.add_option(
615        "-w", "--width",
616        dest="width", type="int", default=2,
617        help="width of date column (default 2, text only)"
618    )
619    parser.add_option(
620        "-l", "--lines",
621        dest="lines", type="int", default=1,
622        help="number of lines for each week (default 1, text only)"
623    )
624    parser.add_option(
625        "-s", "--spacing",
626        dest="spacing", type="int", default=6,
627        help="spacing between months (default 6, text only)"
628    )
629    parser.add_option(
630        "-m", "--months",
631        dest="months", type="int", default=3,
632        help="months per row (default 3, text only)"
633    )
634    parser.add_option(
635        "-c", "--css",
636        dest="css", default="calendar.css",
637        help="CSS to use for page (html only)"
638    )
639    parser.add_option(
640        "-L", "--locale",
641        dest="locale", default=None,
642        help="locale to be used from month and weekday names"
643    )
644    parser.add_option(
645        "-e", "--encoding",
646        dest="encoding", default=None,
647        help="Encoding to use for output"
648    )
649    parser.add_option(
650        "-t", "--type",
651        dest="type", default="text",
652        choices=("text", "html"),
653        help="output type (text or html)"
654    )
655
656    (options, args) = parser.parse_args(args)
657
658    if options.locale and not options.encoding:
659        parser.error("if --locale is specified --encoding is required")
660        sys.exit(1)
661
662    locale = options.locale, options.encoding
663
664    if options.type == "html":
665        if options.locale:
666            cal = LocaleHTMLCalendar(locale=locale)
667        else:
668            cal = HTMLCalendar()
669        encoding = options.encoding
670        if encoding is None:
671            encoding = sys.getdefaultencoding()
672        optdict = dict(encoding=encoding, css=options.css)
673        if len(args) == 1:
674            print cal.formatyearpage(datetime.date.today().year, **optdict)
675        elif len(args) == 2:
676            print cal.formatyearpage(int(args[1]), **optdict)
677        else:
678            parser.error("incorrect number of arguments")
679            sys.exit(1)
680    else:
681        if options.locale:
682            cal = LocaleTextCalendar(locale=locale)
683        else:
684            cal = TextCalendar()
685        optdict = dict(w=options.width, l=options.lines)
686        if len(args) != 3:
687            optdict["c"] = options.spacing
688            optdict["m"] = options.months
689        if len(args) == 1:
690            result = cal.formatyear(datetime.date.today().year, **optdict)
691        elif len(args) == 2:
692            result = cal.formatyear(int(args[1]), **optdict)
693        elif len(args) == 3:
694            result = cal.formatmonth(int(args[1]), int(args[2]), **optdict)
695        else:
696            parser.error("incorrect number of arguments")
697            sys.exit(1)
698        if options.encoding:
699            result = result.encode(options.encoding)
700        print result
701
702
703if __name__ == "__main__":
704    main(sys.argv)