source: python/trunk/Lib/calendar.py@ 383

Last change on this file since 383 was 2, checked in by Yuri Dario, 15 years ago

Initial import for vendor code.

  • Property svn:eol-style set to native
File size: 22.5 KB
Line 
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)
Note: See TracBrowser for help on using the repository browser.