1 """Calendar printing functions
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)."""
8 import sys
, datetime
, locale
10 __all__
= ["IllegalMonthError", "IllegalWeekdayError", "setfirstweekday",
11 "firstweekday", "isleap", "leapdays", "weekday", "monthrange",
12 "monthcalendar", "prmonth", "month", "prcal", "calendar",
13 "timegm", "month_name", "month_abbr", "day_name", "day_abbr"]
15 # Exception raised for bad input (with string parameter for details)
18 # Exceptions raised for bad input
19 class IllegalMonthError(ValueError):
20 def __init__(self
, month
):
23 return "bad month number %r; must be 1-12" % self
.month
26 class IllegalWeekdayError(ValueError):
27 def __init__(self
, weekday
):
28 self
.weekday
= weekday
30 return "bad weekday number %r; must be 0 (Monday) to 6 (Sunday)" % self
.weekday
33 # Constants for months referenced later
37 # Number of days per month (except for February in leap years)
38 mdays
= [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
40 # This module used to have hard-coded lists of day and month names, as
41 # English strings. The classes following emulate a read-only version of
42 # that, but supply localized names. Note that the values are computed
43 # fresh on each call, in case the user changes locale between calls.
45 class _localized_month
:
47 _months
= [datetime
.date(2001, i
+1, 1).strftime
for i
in xrange(12)]
48 _months
.insert(0, lambda x
: "")
50 def __init__(self
, format
):
53 def __getitem__(self
, i
):
54 funcs
= self
._months
[i
]
55 if isinstance(i
, slice):
56 return [f(self
.format
) for f
in funcs
]
58 return funcs(self
.format
)
66 # January 1, 2001, was a Monday.
67 _days
= [datetime
.date(2001, 1, i
+1).strftime
for i
in xrange(7)]
69 def __init__(self
, format
):
72 def __getitem__(self
, i
):
74 if isinstance(i
, slice):
75 return [f(self
.format
) for f
in funcs
]
77 return funcs(self
.format
)
83 # Full and abbreviated names of weekdays
84 day_name
= _localized_day('%A')
85 day_abbr
= _localized_day('%a')
87 # Full and abbreviated names of months (1-based arrays!!!)
88 month_name
= _localized_month('%B')
89 month_abbr
= _localized_month('%b')
91 # Constants for weekdays
92 (MONDAY
, TUESDAY
, WEDNESDAY
, THURSDAY
, FRIDAY
, SATURDAY
, SUNDAY
) = range(7)
96 """Return 1 for leap years, 0 for non-leap years."""
97 return year
% 4 == 0 and (year
% 100 != 0 or year
% 400 == 0)
100 def leapdays(y1
, y2
):
101 """Return number of leap years in range [y1, y2).
105 return (y2
//4 - y1
//4) - (y2
//100 - y1
//100) + (y2
//400 - y1
//400)
108 def weekday(year
, month
, day
):
109 """Return weekday (0-6 ~ Mon-Sun) for year (1970-...), month (1-12),
111 return datetime
.date(year
, month
, day
).weekday()
114 def monthrange(year
, month
):
115 """Return weekday (0-6 ~ Mon-Sun) and number of days (28-31) for
117 if not 1 <= month
<= 12:
118 raise IllegalMonthError(month
)
119 day1
= weekday(year
, month
, 1)
120 ndays
= mdays
[month
] + (month
== February
and isleap(year
))
124 class Calendar(object):
126 Base calendar class. This class doesn't do any formatting. It simply
127 provides data to subclasses.
130 def __init__(self
, firstweekday
=0):
131 self
.firstweekday
= firstweekday
# 0 = Monday, 6 = Sunday
133 def getfirstweekday(self
):
134 return self
._firstweekday
% 7
136 def setfirstweekday(self
, firstweekday
):
137 self
._firstweekday
= firstweekday
139 firstweekday
= property(getfirstweekday
, setfirstweekday
)
141 def iterweekdays(self
):
143 Return a iterator for one week of weekday numbers starting with the
144 configured first one.
146 for i
in xrange(self
.firstweekday
, self
.firstweekday
+ 7):
149 def itermonthdates(self
, year
, month
):
151 Return an iterator for one month. The iterator will yield datetime.date
152 values and will always iterate through complete weeks, so it will yield
153 dates outside the specified month.
155 date
= datetime
.date(year
, month
, 1)
156 # Go back to the beginning of the week
157 days
= (date
.weekday() - self
.firstweekday
) % 7
158 date
-= datetime
.timedelta(days
=days
)
159 oneday
= datetime
.timedelta(days
=1)
163 if date
.month
!= month
and date
.weekday() == self
.firstweekday
:
166 def itermonthdays2(self
, year
, month
):
168 Like itermonthdates(), but will yield (day number, weekday number)
169 tuples. For days outside the specified month the day number is 0.
171 for date
in self
.itermonthdates(year
, month
):
172 if date
.month
!= month
:
173 yield (0, date
.weekday())
175 yield (date
.day
, date
.weekday())
177 def itermonthdays(self
, year
, month
):
179 Like itermonthdates(), but will yield day numbers tuples. For days
180 outside the specified month the day number is 0.
182 for date
in self
.itermonthdates(year
, month
):
183 if date
.month
!= month
:
188 def monthdatescalendar(self
, year
, month
):
190 Return a matrix (list of lists) representing a month's calendar.
191 Each row represents a week; week entries are datetime.date values.
193 dates
= list(self
.itermonthdates(year
, month
))
194 return [ dates
[i
:i
+7] for i
in xrange(0, len(dates
), 7) ]
196 def monthdays2calendar(self
, year
, month
):
198 Return a matrix representing a month's calendar.
199 Each row represents a week; week entries are
200 (day number, weekday number) tuples. Day numbers outside this month
203 days
= list(self
.itermonthdays2(year
, month
))
204 return [ days
[i
:i
+7] for i
in xrange(0, len(days
), 7) ]
206 def monthdayscalendar(self
, year
, month
):
208 Return a matrix representing a month's calendar.
209 Each row represents a week; days outside this month are zero.
211 days
= list(self
.itermonthdays(year
, month
))
212 return [ days
[i
:i
+7] for i
in xrange(0, len(days
), 7) ]
214 def yeardatescalendar(self
, year
, width
=3):
216 Return the data for the specified year ready for formatting. The return
217 value is a list of month rows. Each month row contains upto width months.
218 Each month contains between 4 and 6 weeks and each week contains 1-7
219 days. Days are datetime.date objects.
222 self
.monthdatescalendar(year
, i
)
223 for i
in xrange(January
, January
+12)
225 return [months
[i
:i
+width
] for i
in xrange(0, len(months
), width
) ]
227 def yeardays2calendar(self
, year
, width
=3):
229 Return the data for the specified year ready for formatting (similar to
230 yeardatescalendar()). Entries in the week lists are
231 (day number, weekday number) tuples. Day numbers outside this month are
235 self
.monthdays2calendar(year
, i
)
236 for i
in xrange(January
, January
+12)
238 return [months
[i
:i
+width
] for i
in xrange(0, len(months
), width
) ]
240 def yeardayscalendar(self
, year
, width
=3):
242 Return the data for the specified year ready for formatting (similar to
243 yeardatescalendar()). Entries in the week lists are day numbers.
244 Day numbers outside this month are zero.
247 self
.monthdayscalendar(year
, i
)
248 for i
in xrange(January
, January
+12)
250 return [months
[i
:i
+width
] for i
in xrange(0, len(months
), width
) ]
253 class TextCalendar(Calendar
):
255 Subclass of Calendar that outputs a calendar as a simple plain text
256 similar to the UNIX program cal.
259 def prweek(theweek
, width
):
261 Print a single week (no newline).
263 print self
.week(theweek
, width
),
265 def formatday(self
, day
, weekday
, width
):
267 Returns a formatted day.
272 s
= '%2i' % day
# right-align single-digit days
273 return s
.center(width
)
275 def formatweek(self
, theweek
, width
):
277 Returns a single week in a string (no newline).
279 return ' '.join(self
.formatday(d
, wd
, width
) for (d
, wd
) in theweek
)
281 def formatweekday(self
, day
, width
):
283 Returns a formatted week day name.
289 return names
[day
][:width
].center(width
)
291 def formatweekheader(self
, width
):
293 Return a header for a week.
295 return ' '.join(self
.formatweekday(i
, width
) for i
in self
.iterweekdays())
297 def formatmonthname(self
, theyear
, themonth
, width
, withyear
=True):
299 Return a formatted month name.
301 s
= month_name
[themonth
]
303 s
= "%s %r" % (s
, theyear
)
304 return s
.center(width
)
306 def prmonth(self
, theyear
, themonth
, w
=0, l
=0):
308 Print a month's calendar.
310 print self
.formatmonth(theyear
, themonth
, w
, l
),
312 def formatmonth(self
, theyear
, themonth
, w
=0, l
=0):
314 Return a month's calendar string (multi-line).
318 s
= self
.formatmonthname(theyear
, themonth
, 7 * (w
+ 1) - 1)
321 s
+= self
.formatweekheader(w
).rstrip()
323 for week
in self
.monthdays2calendar(theyear
, themonth
):
324 s
+= self
.formatweek(week
, w
).rstrip()
328 def formatyear(self
, theyear
, w
=2, l
=1, c
=6, m
=3):
330 Returns a year's calendar as a multi-line string.
335 colwidth
= (w
+ 1) * 7 - 1
338 a(repr(theyear
).center(colwidth
*m
+c
*(m
-1)).rstrip())
340 header
= self
.formatweekheader(w
)
341 for (i
, row
) in enumerate(self
.yeardays2calendar(theyear
, m
)):
343 months
= xrange(m
*i
+1, min(m
*(i
+1)+1, 13))
345 names
= (self
.formatmonthname(theyear
, k
, colwidth
, False)
347 a(formatstring(names
, colwidth
, c
).rstrip())
349 headers
= (header
for k
in months
)
350 a(formatstring(headers
, colwidth
, c
).rstrip())
352 # max number of weeks for this row
353 height
= max(len(cal
) for cal
in row
)
354 for j
in xrange(height
):
360 weeks
.append(self
.formatweek(cal
[j
], w
))
361 a(formatstring(weeks
, colwidth
, c
).rstrip())
365 def pryear(self
, theyear
, w
=0, l
=0, c
=6, m
=3):
366 """Print a year's calendar."""
367 print self
.formatyear(theyear
, w
, l
, c
, m
)
370 class HTMLCalendar(Calendar
):
372 This calendar returns complete HTML pages.
375 # CSS classes for the day <td>s
376 cssclasses
= ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
378 def formatday(self
, day
, weekday
):
380 Return a day as a table cell.
383 return '<td class="noday"> </td>' # day outside month
385 return '<td class="%s">%d</td>' % (self
.cssclasses
[weekday
], day
)
387 def formatweek(self
, theweek
):
389 Return a complete week as a table row.
391 s
= ''.join(self
.formatday(d
, wd
) for (d
, wd
) in theweek
)
392 return '<tr>%s</tr>' % s
394 def formatweekday(self
, day
):
396 Return a weekday name as a table header.
398 return '<th class="%s">%s</th>' % (self
.cssclasses
[day
], day_abbr
[day
])
400 def formatweekheader(self
):
402 Return a header for a week as a table row.
404 s
= ''.join(self
.formatweekday(i
) for i
in self
.iterweekdays())
405 return '<tr>%s</tr>' % s
407 def formatmonthname(self
, theyear
, themonth
, withyear
=True):
409 Return a month name as a table row.
412 s
= '%s %s' % (month_name
[themonth
], theyear
)
414 s
= '%s' % month_name
[themonth
]
415 return '<tr><th colspan="7" class="month">%s</th></tr>' % s
417 def formatmonth(self
, theyear
, themonth
, withyear
=True):
419 Return a formatted month as a table.
423 a('<table border="0" cellpadding="0" cellspacing="0" class="month">')
425 a(self
.formatmonthname(theyear
, themonth
, withyear
=withyear
))
427 a(self
.formatweekheader())
429 for week
in self
.monthdays2calendar(theyear
, themonth
):
430 a(self
.formatweek(week
))
436 def formatyear(self
, theyear
, width
=3):
438 Return a formatted year as a table of tables.
442 width
= max(width
, 1)
443 a('<table border="0" cellpadding="0" cellspacing="0" class="year">')
445 a('<tr><th colspan="%d" class="year">%s</th></tr>' % (width
, theyear
))
446 for i
in xrange(January
, January
+12, width
):
448 months
= xrange(i
, min(i
+width
, 13))
452 a(self
.formatmonth(theyear
, m
, withyear
=False))
458 def formatyearpage(self
, theyear
, width
=3, css
='calendar.css', encoding
=None):
460 Return a formatted year as a complete HTML page.
463 encoding
= sys
.getdefaultencoding()
466 a('<?xml version="1.0" encoding="%s"?>\n' % encoding
)
467 a('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n')
470 a('<meta http-equiv="Content-Type" content="text/html; charset=%s" />\n' % encoding
)
472 a('<link rel="stylesheet" type="text/css" href="%s" />\n' % css
)
473 a('<title>Calendar for %d</title\n' % theyear
)
476 a(self
.formatyear(theyear
, width
))
479 return ''.join(v
).encode(encoding
, "xmlcharrefreplace")
482 class LocaleTextCalendar(TextCalendar
):
484 This class can be passed a locale name in the constructor and will return
485 month and weekday names in the specified locale. If this locale includes
486 an encoding all strings containing month and weekday names will be returned
490 def __init__(self
, firstweekday
=0, locale
=None):
491 TextCalendar
.__init
__(self
, firstweekday
)
493 locale
= locale
.getdefaultlocale()
496 def formatweekday(self
, day
, width
):
497 oldlocale
= locale
.setlocale(locale
.LC_TIME
, self
.locale
)
499 encoding
= locale
.getlocale(locale
.LC_TIME
)[1]
505 if encoding
is not None:
506 name
= name
.decode(encoding
)
507 result
= name
[:width
].center(width
)
509 locale
.setlocale(locale
.LC_TIME
, oldlocale
)
512 def formatmonthname(self
, theyear
, themonth
, width
, withyear
=True):
513 oldlocale
= locale
.setlocale(locale
.LC_TIME
, self
.locale
)
515 encoding
= locale
.getlocale(locale
.LC_TIME
)[1]
516 s
= month_name
[themonth
]
517 if encoding
is not None:
518 s
= s
.decode(encoding
)
520 s
= "%s %r" % (s
, theyear
)
521 result
= s
.center(width
)
523 locale
.setlocale(locale
.LC_TIME
, oldlocale
)
527 class LocaleHTMLCalendar(HTMLCalendar
):
529 This class can be passed a locale name in the constructor and will return
530 month and weekday names in the specified locale. If this locale includes
531 an encoding all strings containing month and weekday names will be returned
534 def __init__(self
, firstweekday
=0, locale
=None):
535 HTMLCalendar
.__init
__(self
, firstweekday
)
537 locale
= locale
.getdefaultlocale()
540 def formatweekday(self
, day
):
541 oldlocale
= locale
.setlocale(locale
.LC_TIME
, self
.locale
)
543 encoding
= locale
.getlocale(locale
.LC_TIME
)[1]
545 if encoding
is not None:
546 s
= s
.decode(encoding
)
547 result
= '<th class="%s">%s</th>' % (self
.cssclasses
[day
], s
)
549 locale
.setlocale(locale
.LC_TIME
, oldlocale
)
552 def formatmonthname(self
, theyear
, themonth
, withyear
=True):
553 oldlocale
= locale
.setlocale(locale
.LC_TIME
, self
.locale
)
555 encoding
= locale
.getlocale(locale
.LC_TIME
)[1]
556 s
= month_name
[themonth
]
557 if encoding
is not None:
558 s
= s
.decode(encoding
)
560 s
= '%s %s' % (s
, theyear
)
561 result
= '<tr><th colspan="7" class="month">%s</th></tr>' % s
563 locale
.setlocale(locale
.LC_TIME
, oldlocale
)
567 # Support for old module level interface
570 firstweekday
= c
.getfirstweekday
572 def setfirstweekday(firstweekday
):
573 if not MONDAY
<= firstweekday
<= SUNDAY
:
574 raise IllegalWeekdayError(firstweekday
)
575 c
.firstweekday
= firstweekday
577 monthcalendar
= c
.monthdayscalendar
580 weekheader
= c
.formatweekheader
582 month
= c
.formatmonth
583 calendar
= c
.formatyear
587 # Spacing of month columns for multi-column year calendar
588 _colwidth
= 7*3 - 1 # Amount printed by prweek()
589 _spacing
= 6 # Number of spaces between columns
592 def format(cols
, colwidth
=_colwidth
, spacing
=_spacing
):
593 """Prints multi-column formatting for year calendars"""
594 print formatstring(cols
, colwidth
, spacing
)
597 def formatstring(cols
, colwidth
=_colwidth
, spacing
=_spacing
):
598 """Returns a string formatted from n strings, centered within n columns."""
600 return spacing
.join(c
.center(colwidth
) for c
in cols
)
604 _EPOCH_ORD
= datetime
.date(EPOCH
, 1, 1).toordinal()
608 """Unrelated but handy function to calculate Unix timestamp from GMT."""
609 year
, month
, day
, hour
, minute
, second
= tuple[:6]
610 days
= datetime
.date(year
, month
, 1).toordinal() - _EPOCH_ORD
+ day
- 1
611 hours
= days
*24 + hour
612 minutes
= hours
*60 + minute
613 seconds
= minutes
*60 + second
619 parser
= optparse
.OptionParser(usage
="usage: %prog [options] [year [month]]")
622 dest
="width", type="int", default
=2,
623 help="width of date column (default 2, text only)"
627 dest
="lines", type="int", default
=1,
628 help="number of lines for each week (default 1, text only)"
632 dest
="spacing", type="int", default
=6,
633 help="spacing between months (default 6, text only)"
637 dest
="months", type="int", default
=3,
638 help="months per row (default 3, text only)"
642 dest
="css", default
="calendar.css",
643 help="CSS to use for page (html only)"
647 dest
="locale", default
=None,
648 help="locale to be used from month and weekday names"
652 dest
="encoding", default
=None,
653 help="Encoding to use for output"
657 dest
="type", default
="text",
658 choices
=("text", "html"),
659 help="output type (text or html)"
662 (options
, args
) = parser
.parse_args(args
)
664 if options
.locale
and not options
.encoding
:
665 parser
.error("if --locale is specified --encoding is required")
668 if options
.type == "html":
670 cal
= LocaleHTMLCalendar(locale
=options
.locale
)
673 encoding
= options
.encoding
675 encoding
= sys
.getdefaultencoding()
676 optdict
= dict(encoding
=encoding
, css
=options
.css
)
678 print cal
.formatyearpage(datetime
.date
.today().year
, **optdict
)
680 print cal
.formatyearpage(int(args
[1]), **optdict
)
682 parser
.error("incorrect number of arguments")
686 cal
= LocaleTextCalendar(locale
=options
.locale
)
689 optdict
= dict(w
=options
.width
, l
=options
.lines
)
691 optdict
["c"] = options
.spacing
692 optdict
["m"] = options
.months
694 result
= cal
.formatyear(datetime
.date
.today().year
, **optdict
)
696 result
= cal
.formatyear(int(args
[1]), **optdict
)
698 result
= cal
.formatmonth(int(args
[1]), int(args
[2]), **optdict
)
700 parser
.error("incorrect number of arguments")
703 result
= result
.encode(options
.encoding
)
707 if __name__
== "__main__":