• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 True for leap years, False 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.getlocale(_locale.LC_TIME)
490        _locale.setlocale(_locale.LC_TIME, self.locale)
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    try:
568        firstweekday.__index__
569    except AttributeError:
570        raise IllegalWeekdayError(firstweekday)
571    if not MONDAY <= firstweekday <= SUNDAY:
572        raise IllegalWeekdayError(firstweekday)
573    c.firstweekday = firstweekday
574
575monthcalendar = c.monthdayscalendar
576prweek = c.prweek
577week = c.formatweek
578weekheader = c.formatweekheader
579prmonth = c.prmonth
580month = c.formatmonth
581calendar = c.formatyear
582prcal = c.pryear
583
584
585# Spacing of month columns for multi-column year calendar
586_colwidth = 7*3 - 1         # Amount printed by prweek()
587_spacing = 6                # Number of spaces between columns
588
589
590def format(cols, colwidth=_colwidth, spacing=_spacing):
591    """Prints multi-column formatting for year calendars"""
592    print formatstring(cols, colwidth, spacing)
593
594
595def formatstring(cols, colwidth=_colwidth, spacing=_spacing):
596    """Returns a string formatted from n strings, centered within n columns."""
597    spacing *= ' '
598    return spacing.join(c.center(colwidth) for c in cols)
599
600
601EPOCH = 1970
602_EPOCH_ORD = datetime.date(EPOCH, 1, 1).toordinal()
603
604
605def timegm(tuple):
606    """Unrelated but handy function to calculate Unix timestamp from GMT."""
607    year, month, day, hour, minute, second = tuple[:6]
608    days = datetime.date(year, month, 1).toordinal() - _EPOCH_ORD + day - 1
609    hours = days*24 + hour
610    minutes = hours*60 + minute
611    seconds = minutes*60 + second
612    return seconds
613
614
615def main(args):
616    import optparse
617    parser = optparse.OptionParser(usage="usage: %prog [options] [year [month]]")
618    parser.add_option(
619        "-w", "--width",
620        dest="width", type="int", default=2,
621        help="width of date column (default 2, text only)"
622    )
623    parser.add_option(
624        "-l", "--lines",
625        dest="lines", type="int", default=1,
626        help="number of lines for each week (default 1, text only)"
627    )
628    parser.add_option(
629        "-s", "--spacing",
630        dest="spacing", type="int", default=6,
631        help="spacing between months (default 6, text only)"
632    )
633    parser.add_option(
634        "-m", "--months",
635        dest="months", type="int", default=3,
636        help="months per row (default 3, text only)"
637    )
638    parser.add_option(
639        "-c", "--css",
640        dest="css", default="calendar.css",
641        help="CSS to use for page (html only)"
642    )
643    parser.add_option(
644        "-L", "--locale",
645        dest="locale", default=None,
646        help="locale to be used from month and weekday names"
647    )
648    parser.add_option(
649        "-e", "--encoding",
650        dest="encoding", default=None,
651        help="Encoding to use for output"
652    )
653    parser.add_option(
654        "-t", "--type",
655        dest="type", default="text",
656        choices=("text", "html"),
657        help="output type (text or html)"
658    )
659
660    (options, args) = parser.parse_args(args)
661
662    if options.locale and not options.encoding:
663        parser.error("if --locale is specified --encoding is required")
664        sys.exit(1)
665
666    locale = options.locale, options.encoding
667
668    if options.type == "html":
669        if options.locale:
670            cal = LocaleHTMLCalendar(locale=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=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
705
706
707if __name__ == "__main__":
708    main(sys.argv)
709