]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/calendar.py
dist/mkfile: run binds in subshell
[plan9front.git] / sys / lib / python / calendar.py
1 """Calendar printing functions
2
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)."""
7
8 from __future__ import with_statement
9 import sys, datetime, locale
10
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"]
15
16 # Exception raised for bad input (with string parameter for details)
17 error = ValueError
18
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
25
26
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
32
33
34 # Constants for months referenced later
35 January = 1
36 February = 2
37
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]
40
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.
45
46 class _localized_month:
47
48     _months = [datetime.date(2001, i+1, 1).strftime for i in xrange(12)]
49     _months.insert(0, lambda x: "")
50
51     def __init__(self, format):
52         self.format = format
53
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)
60
61     def __len__(self):
62         return 13
63
64
65 class _localized_day:
66
67     # January 1, 2001, was a Monday.
68     _days = [datetime.date(2001, 1, i+1).strftime for i in xrange(7)]
69
70     def __init__(self, format):
71         self.format = format
72
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)
79
80     def __len__(self):
81         return 7
82
83
84 # Full and abbreviated names of weekdays
85 day_name = _localized_day('%A')
86 day_abbr = _localized_day('%a')
87
88 # Full and abbreviated names of months (1-based arrays!!!)
89 month_name = _localized_month('%B')
90 month_abbr = _localized_month('%b')
91
92 # Constants for weekdays
93 (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7)
94
95
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)
99
100
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)
107
108
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()
113
114
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
123
124
125 class Calendar(object):
126     """
127     Base calendar class. This class doesn't do any formatting. It simply
128     provides data to subclasses.
129     """
130
131     def __init__(self, firstweekday=0):
132         self.firstweekday = firstweekday # 0 = Monday, 6 = Sunday
133
134     def getfirstweekday(self):
135         return self._firstweekday % 7
136
137     def setfirstweekday(self, firstweekday):
138         self._firstweekday = firstweekday
139
140     firstweekday = property(getfirstweekday, setfirstweekday)
141
142     def iterweekdays(self):
143         """
144         Return a iterator for one week of weekday numbers starting with the
145         configured first one.
146         """
147         for i in xrange(self.firstweekday, self.firstweekday + 7):
148             yield i%7
149
150     def itermonthdates(self, year, month):
151         """
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.
155         """
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
166
167     def itermonthdays2(self, year, month):
168         """
169         Like itermonthdates(), but will yield (day number, weekday number)
170         tuples. For days outside the specified month the day number is 0.
171         """
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())
177
178     def itermonthdays(self, year, month):
179         """
180         Like itermonthdates(), but will yield day numbers tuples. For days
181         outside the specified month the day number is 0.
182         """
183         for date in self.itermonthdates(year, month):
184             if date.month != month:
185                 yield 0
186             else:
187                 yield date.day
188
189     def monthdatescalendar(self, year, month):
190         """
191         Return a matrix (list of lists) representing a month's calendar.
192         Each row represents a week; week entries are datetime.date values.
193         """
194         dates = list(self.itermonthdates(year, month))
195         return [ dates[i:i+7] for i in xrange(0, len(dates), 7) ]
196
197     def monthdays2calendar(self, year, month):
198         """
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.
203         """
204         days = list(self.itermonthdays2(year, month))
205         return [ days[i:i+7] for i in xrange(0, len(days), 7) ]
206
207     def monthdayscalendar(self, year, month):
208         """
209         Return a matrix representing a month's calendar.
210         Each row represents a week; days outside this month are zero.
211         """
212         days = list(self.itermonthdays(year, month))
213         return [ days[i:i+7] for i in xrange(0, len(days), 7) ]
214
215     def yeardatescalendar(self, year, width=3):
216         """
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.
221         """
222         months = [
223             self.monthdatescalendar(year, i)
224             for i in xrange(January, January+12)
225         ]
226         return [months[i:i+width] for i in xrange(0, len(months), width) ]
227
228     def yeardays2calendar(self, year, width=3):
229         """
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.
234         """
235         months = [
236             self.monthdays2calendar(year, i)
237             for i in xrange(January, January+12)
238         ]
239         return [months[i:i+width] for i in xrange(0, len(months), width) ]
240
241     def yeardayscalendar(self, year, width=3):
242         """
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.
246         """
247         months = [
248             self.monthdayscalendar(year, i)
249             for i in xrange(January, January+12)
250         ]
251         return [months[i:i+width] for i in xrange(0, len(months), width) ]
252
253
254 class TextCalendar(Calendar):
255     """
256     Subclass of Calendar that outputs a calendar as a simple plain text
257     similar to the UNIX program cal.
258     """
259
260     def prweek(self, theweek, width):
261         """
262         Print a single week (no newline).
263         """
264         print self.week(theweek, width),
265
266     def formatday(self, day, weekday, width):
267         """
268         Returns a formatted day.
269         """
270         if day == 0:
271             s = ''
272         else:
273             s = '%2i' % day             # right-align single-digit days
274         return s.center(width)
275
276     def formatweek(self, theweek, width):
277         """
278         Returns a single week in a string (no newline).
279         """
280         return ' '.join(self.formatday(d, wd, width) for (d, wd) in theweek)
281
282     def formatweekday(self, day, width):
283         """
284         Returns a formatted week day name.
285         """
286         if width >= 9:
287             names = day_name
288         else:
289             names = day_abbr
290         return names[day][:width].center(width)
291
292     def formatweekheader(self, width):
293         """
294         Return a header for a week.
295         """
296         return ' '.join(self.formatweekday(i, width) for i in self.iterweekdays())
297
298     def formatmonthname(self, theyear, themonth, width, withyear=True):
299         """
300         Return a formatted month name.
301         """
302         s = month_name[themonth]
303         if withyear:
304             s = "%s %r" % (s, theyear)
305         return s.center(width)
306
307     def prmonth(self, theyear, themonth, w=0, l=0):
308         """
309         Print a month's calendar.
310         """
311         print self.formatmonth(theyear, themonth, w, l),
312
313     def formatmonth(self, theyear, themonth, w=0, l=0):
314         """
315         Return a month's calendar string (multi-line).
316         """
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
328
329     def formatyear(self, theyear, w=2, l=1, c=6, m=3):
330         """
331         Returns a year's calendar as a multi-line string.
332         """
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 = xrange(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 xrange(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)
365
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)
369
370
371 class HTMLCalendar(Calendar):
372     """
373     This calendar returns complete HTML pages.
374     """
375
376     # CSS classes for the day <td>s
377     cssclasses = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
378
379     def formatday(self, day, weekday):
380         """
381         Return a day as a table cell.
382         """
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)
387
388     def formatweek(self, theweek):
389         """
390         Return a complete week as a table row.
391         """
392         s = ''.join(self.formatday(d, wd) for (d, wd) in theweek)
393         return '<tr>%s</tr>' % s
394
395     def formatweekday(self, day):
396         """
397         Return a weekday name as a table header.
398         """
399         return '<th class="%s">%s</th>' % (self.cssclasses[day], day_abbr[day])
400
401     def formatweekheader(self):
402         """
403         Return a header for a week as a table row.
404         """
405         s = ''.join(self.formatweekday(i) for i in self.iterweekdays())
406         return '<tr>%s</tr>' % s
407
408     def formatmonthname(self, theyear, themonth, withyear=True):
409         """
410         Return a month name as a table row.
411         """
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
417
418     def formatmonth(self, theyear, themonth, withyear=True):
419         """
420         Return a formatted month as a table.
421         """
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)
436
437     def formatyear(self, theyear, width=3):
438         """
439         Return a formatted year as a table of tables.
440         """
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 xrange(January, January+12, width):
448             # months in this row
449             months = xrange(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)
458
459     def formatyearpage(self, theyear, width=3, css='calendar.css', encoding=None):
460         """
461         Return a formatted year as a complete HTML page.
462         """
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")
481
482
483 class TimeEncoding:
484     def __init__(self, locale):
485         self.locale = locale
486
487     def __enter__(self):
488         self.oldlocale = locale.setlocale(locale.LC_TIME, self.locale)
489         return locale.getlocale(locale.LC_TIME)[1]
490
491     def __exit__(self, *args):
492         locale.setlocale(locale.LC_TIME, self.oldlocale)
493
494
495 class LocaleTextCalendar(TextCalendar):
496     """
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.
501     """
502
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
508
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)
519
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)
528
529
530 class LocaleHTMLCalendar(HTMLCalendar):
531     """
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.
536     """
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
542
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)
549
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
558
559
560 # Support for old module level interface
561 c = TextCalendar()
562
563 firstweekday = c.getfirstweekday
564
565 def setfirstweekday(firstweekday):
566     if not MONDAY <= firstweekday <= SUNDAY:
567         raise IllegalWeekdayError(firstweekday)
568     c.firstweekday = firstweekday
569
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
578
579
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
583
584
585 def format(cols, colwidth=_colwidth, spacing=_spacing):
586     """Prints multi-column formatting for year calendars"""
587     print formatstring(cols, colwidth, spacing)
588
589
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)
594
595
596 EPOCH = 1970
597 _EPOCH_ORD = datetime.date(EPOCH, 1, 1).toordinal()
598
599
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
608
609
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)"
617     )
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)"
622     )
623     parser.add_option(
624         "-s", "--spacing",
625         dest="spacing", type="int", default=6,
626         help="spacing between months (default 6, text only)"
627     )
628     parser.add_option(
629         "-m", "--months",
630         dest="months", type="int", default=3,
631         help="months per row (default 3, text only)"
632     )
633     parser.add_option(
634         "-c", "--css",
635         dest="css", default="calendar.css",
636         help="CSS to use for page (html only)"
637     )
638     parser.add_option(
639         "-L", "--locale",
640         dest="locale", default=None,
641         help="locale to be used from month and weekday names"
642     )
643     parser.add_option(
644         "-e", "--encoding",
645         dest="encoding", default=None,
646         help="Encoding to use for output"
647     )
648     parser.add_option(
649         "-t", "--type",
650         dest="type", default="text",
651         choices=("text", "html"),
652         help="output type (text or html)"
653     )
654
655     (options, args) = parser.parse_args(args)
656
657     if options.locale and not options.encoding:
658         parser.error("if --locale is specified --encoding is required")
659         sys.exit(1)
660
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
698
699
700 if __name__ == "__main__":
701     main(sys.argv)