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 from __future__
import with_statement
9 import sys
, datetime
, locale
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"]
16 # Exception raised for bad input (with string parameter for details)
19 # Exceptions raised for bad input
20 class IllegalMonthError(ValueError):
21 def __init__(self
, month
):
24 return "bad month number %r; must be 1-12" % self
.month
27 class IllegalWeekdayError(ValueError):
28 def __init__(self
, weekday
):
29 self
.weekday
= weekday
31 return "bad weekday number %r; must be 0 (Monday) to 6 (Sunday)" % self
.weekday
34 # Constants for months referenced later
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]
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.
46 class _localized_month
:
48 _months
= [datetime
.date(2001, i
+1, 1).strftime
for i
in range(12)]
49 _months
.insert(0, lambda x
: "")
51 def __init__(self
, format
):
54 def __getitem__(self
, i
):
55 funcs
= self
._months
[i
]
56 if isinstance(i
, slice):
57 return [f(self
.format
) for f
in funcs
]
59 return funcs(self
.format
)
67 # January 1, 2001, was a Monday.
68 _days
= [datetime
.date(2001, 1, i
+1).strftime
for i
in range(7)]
70 def __init__(self
, format
):
73 def __getitem__(self
, i
):
75 if isinstance(i
, slice):
76 return [f(self
.format
) for f
in funcs
]
78 return funcs(self
.format
)
84 # Full and abbreviated names of weekdays
85 day_name
= _localized_day('%A')
86 day_abbr
= _localized_day('%a')
88 # Full and abbreviated names of months (1-based arrays!!!)
89 month_name
= _localized_month('%B')
90 month_abbr
= _localized_month('%b')
92 # Constants for weekdays
93 (MONDAY
, TUESDAY
, WEDNESDAY
, THURSDAY
, FRIDAY
, SATURDAY
, SUNDAY
) = range(7)
97 """Return 1 for leap years, 0 for non-leap years."""
98 return year
% 4 == 0 and (year
% 100 != 0 or year
% 400 == 0)
101 def leapdays(y1
, y2
):
102 """Return number of leap years in range [y1, y2).
106 return (y2
//4 - y1
//4) - (y2
//100 - y1
//100) + (y2
//400 - y1
//400)
109 def weekday(year
, month
, day
):
110 """Return weekday (0-6 ~ Mon-Sun) for year (1970-...), month (1-12),
112 return datetime
.date(year
, month
, day
).weekday()
115 def monthrange(year
, month
):
116 """Return weekday (0-6 ~ Mon-Sun) and number of days (28-31) for
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
))
125 class Calendar(object):
127 Base calendar class. This class doesn't do any formatting. It simply
128 provides data to subclasses.
131 def __init__(self
, firstweekday
=0):
132 self
.firstweekday
= firstweekday
# 0 = Monday, 6 = Sunday
134 def getfirstweekday(self
):
135 return self
._firstweekday
% 7
137 def setfirstweekday(self
, firstweekday
):
138 self
._firstweekday
= firstweekday
140 firstweekday
= property(getfirstweekday
, setfirstweekday
)
142 def iterweekdays(self
):
144 Return a iterator for one week of weekday numbers starting with the
145 configured first one.
147 for i
in range(self
.firstweekday
, self
.firstweekday
+ 7):
150 def itermonthdates(self
, year
, month
):
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.
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)
164 if date
.month
!= month
and date
.weekday() == self
.firstweekday
:
167 def itermonthdays2(self
, year
, month
):
169 Like itermonthdates(), but will yield (day number, weekday number)
170 tuples. For days outside the specified month the day number is 0.
172 for date
in self
.itermonthdates(year
, month
):
173 if date
.month
!= month
:
174 yield (0, date
.weekday())
176 yield (date
.day
, date
.weekday())
178 def itermonthdays(self
, year
, month
):
180 Like itermonthdates(), but will yield day numbers tuples. For days
181 outside the specified month the day number is 0.
183 for date
in self
.itermonthdates(year
, month
):
184 if date
.month
!= month
:
189 def monthdatescalendar(self
, year
, month
):
191 Return a matrix (list of lists) representing a month's calendar.
192 Each row represents a week; week entries are datetime.date values.
194 dates
= list(self
.itermonthdates(year
, month
))
195 return [ dates
[i
:i
+7] for i
in range(0, len(dates
), 7) ]
197 def monthdays2calendar(self
, year
, month
):
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
204 days
= list(self
.itermonthdays2(year
, month
))
205 return [ days
[i
:i
+7] for i
in range(0, len(days
), 7) ]
207 def monthdayscalendar(self
, year
, month
):
209 Return a matrix representing a month's calendar.
210 Each row represents a week; days outside this month are zero.
212 days
= list(self
.itermonthdays(year
, month
))
213 return [ days
[i
:i
+7] for i
in range(0, len(days
), 7) ]
215 def yeardatescalendar(self
, year
, width
=3):
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.
223 self
.monthdatescalendar(year
, i
)
224 for i
in range(January
, January
+12)
226 return [months
[i
:i
+width
] for i
in range(0, len(months
), width
) ]
228 def yeardays2calendar(self
, year
, width
=3):
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
236 self
.monthdays2calendar(year
, i
)
237 for i
in range(January
, January
+12)
239 return [months
[i
:i
+width
] for i
in range(0, len(months
), width
) ]
241 def yeardayscalendar(self
, year
, width
=3):
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.
248 self
.monthdayscalendar(year
, i
)
249 for i
in range(January
, January
+12)
251 return [months
[i
:i
+width
] for i
in range(0, len(months
), width
) ]
254 class TextCalendar(Calendar
):
256 Subclass of Calendar that outputs a calendar as a simple plain text
257 similar to the UNIX program cal.
260 def prweek(self
, theweek
, width
):
262 Print a single week (no newline).
264 print self
.week(theweek
, width
),
266 def formatday(self
, day
, weekday
, width
):
268 Returns a formatted day.
273 s
= '%2i' % day
# right-align single-digit days
274 return s
.center(width
)
276 def formatweek(self
, theweek
, width
):
278 Returns a single week in a string (no newline).
280 return ' '.join(self
.formatday(d
, wd
, width
) for (d
, wd
) in theweek
)
282 def formatweekday(self
, day
, width
):
284 Returns a formatted week day name.
290 return names
[day
][:width
].center(width
)
292 def formatweekheader(self
, width
):
294 Return a header for a week.
296 return ' '.join(self
.formatweekday(i
, width
) for i
in self
.iterweekdays())
298 def formatmonthname(self
, theyear
, themonth
, width
, withyear
=True):
300 Return a formatted month name.
302 s
= month_name
[themonth
]
304 s
= "%s %r" % (s
, theyear
)
305 return s
.center(width
)
307 def prmonth(self
, theyear
, themonth
, w
=0, l
=0):
309 Print a month's calendar.
311 print self
.formatmonth(theyear
, themonth
, w
, l
),
313 def formatmonth(self
, theyear
, themonth
, w
=0, l
=0):
315 Return a month's calendar string (multi-line).
319 s
= self
.formatmonthname(theyear
, themonth
, 7 * (w
+ 1) - 1)
322 s
+= self
.formatweekheader(w
).rstrip()
324 for week
in self
.monthdays2calendar(theyear
, themonth
):
325 s
+= self
.formatweek(week
, w
).rstrip()
329 def formatyear(self
, theyear
, w
=2, l
=1, c
=6, m
=3):
331 Returns a year's calendar as a multi-line string.
336 colwidth
= (w
+ 1) * 7 - 1
339 a(repr(theyear
).center(colwidth
*m
+c
*(m
-1)).rstrip())
341 header
= self
.formatweekheader(w
)
342 for (i
, row
) in enumerate(self
.yeardays2calendar(theyear
, m
)):
344 months
= range(m
*i
+1, min(m
*(i
+1)+1, 13))
346 names
= (self
.formatmonthname(theyear
, k
, colwidth
, False)
348 a(formatstring(names
, colwidth
, c
).rstrip())
350 headers
= (header
for k
in months
)
351 a(formatstring(headers
, colwidth
, c
).rstrip())
353 # max number of weeks for this row
354 height
= max(len(cal
) for cal
in row
)
355 for j
in range(height
):
361 weeks
.append(self
.formatweek(cal
[j
], w
))
362 a(formatstring(weeks
, colwidth
, c
).rstrip())
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
)
371 class HTMLCalendar(Calendar
):
373 This calendar returns complete HTML pages.
376 # CSS classes for the day <td>s
377 cssclasses
= ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
379 def formatday(self
, day
, weekday
):
381 Return a day as a table cell.
384 return '<td class="noday"> </td>' # day outside month
386 return '<td class="%s">%d</td>' % (self
.cssclasses
[weekday
], day
)
388 def formatweek(self
, theweek
):
390 Return a complete week as a table row.
392 s
= ''.join(self
.formatday(d
, wd
) for (d
, wd
) in theweek
)
393 return '<tr>%s</tr>' % s
395 def formatweekday(self
, day
):
397 Return a weekday name as a table header.
399 return '<th class="%s">%s</th>' % (self
.cssclasses
[day
], day_abbr
[day
])
401 def formatweekheader(self
):
403 Return a header for a week as a table row.
405 s
= ''.join(self
.formatweekday(i
) for i
in self
.iterweekdays())
406 return '<tr>%s</tr>' % s
408 def formatmonthname(self
, theyear
, themonth
, withyear
=True):
410 Return a month name as a table row.
413 s
= '%s %s' % (month_name
[themonth
], theyear
)
415 s
= '%s' % month_name
[themonth
]
416 return '<tr><th colspan="7" class="month">%s</th></tr>' % s
418 def formatmonth(self
, theyear
, themonth
, withyear
=True):
420 Return a formatted month as a table.
424 a('<table border="0" cellpadding="0" cellspacing="0" class="month">')
426 a(self
.formatmonthname(theyear
, themonth
, withyear
=withyear
))
428 a(self
.formatweekheader())
430 for week
in self
.monthdays2calendar(theyear
, themonth
):
431 a(self
.formatweek(week
))
437 def formatyear(self
, theyear
, width
=3):
439 Return a formatted year as a table of tables.
443 width
= max(width
, 1)
444 a('<table border="0" cellpadding="0" cellspacing="0" class="year">')
446 a('<tr><th colspan="%d" class="year">%s</th></tr>' % (width
, theyear
))
447 for i
in range(January
, January
+12, width
):
449 months
= range(i
, min(i
+width
, 13))
453 a(self
.formatmonth(theyear
, m
, withyear
=False))
459 def formatyearpage(self
, theyear
, width
=3, css
='calendar.css', encoding
=None):
461 Return a formatted year as a complete HTML page.
464 encoding
= sys
.getdefaultencoding()
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')
471 a('<meta http-equiv="Content-Type" content="text/html; charset=%s" />\n' % encoding
)
473 a('<link rel="stylesheet" type="text/css" href="%s" />\n' % css
)
474 a('<title>Calendar for %d</title>\n' % theyear
)
477 a(self
.formatyear(theyear
, width
))
480 return ''.join(v
).encode(encoding
, "xmlcharrefreplace")
484 def __init__(self
, locale
):
488 self
.oldlocale
= locale
.setlocale(locale
.LC_TIME
, self
.locale
)
489 return locale
.getlocale(locale
.LC_TIME
)[1]
491 def __exit__(self
, *args
):
492 locale
.setlocale(locale
.LC_TIME
, self
.oldlocale
)
495 class LocaleTextCalendar(TextCalendar
):
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
503 def __init__(self
, firstweekday
=0, locale
=None):
504 TextCalendar
.__init
__(self
, firstweekday
)
506 locale
= locale
.getdefaultlocale()
509 def formatweekday(self
, day
, width
):
510 with
TimeEncoding(self
.locale
) as encoding
:
516 if encoding
is not None:
517 name
= name
.decode(encoding
)
518 return name
[:width
].center(width
)
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
)
526 s
= "%s %r" % (s
, theyear
)
527 return s
.center(width
)
530 class LocaleHTMLCalendar(HTMLCalendar
):
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
537 def __init__(self
, firstweekday
=0, locale
=None):
538 HTMLCalendar
.__init
__(self
, firstweekday
)
540 locale
= locale
.getdefaultlocale()
543 def formatweekday(self
, day
):
544 with
TimeEncoding(self
.locale
) as encoding
:
546 if encoding
is not None:
547 s
= s
.decode(encoding
)
548 return '<th class="%s">%s</th>' % (self
.cssclasses
[day
], s
)
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
)
556 s
= '%s %s' % (s
, theyear
)
557 return '<tr><th colspan="7" class="month">%s</th></tr>' % s
560 # Support for old module level interface
563 firstweekday
= c
.getfirstweekday
565 def setfirstweekday(firstweekday
):
566 if not MONDAY
<= firstweekday
<= SUNDAY
:
567 raise IllegalWeekdayError(firstweekday
)
568 c
.firstweekday
= firstweekday
570 monthcalendar
= c
.monthdayscalendar
573 weekheader
= c
.formatweekheader
575 month
= c
.formatmonth
576 calendar
= c
.formatyear
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
585 def format(cols
, colwidth
=_colwidth
, spacing
=_spacing
):
586 """Prints multi-column formatting for year calendars"""
587 print formatstring(cols
, colwidth
, spacing
)
590 def formatstring(cols
, colwidth
=_colwidth
, spacing
=_spacing
):
591 """Returns a string formatted from n strings, centered within n columns."""
593 return spacing
.join(c
.center(colwidth
) for c
in cols
)
597 _EPOCH_ORD
= datetime
.date(EPOCH
, 1, 1).toordinal()
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
612 parser
= optparse
.OptionParser(usage
="usage: %prog [options] [year [month]]")
615 dest
="width", type="int", default
=2,
616 help="width of date column (default 2, text only)"
620 dest
="lines", type="int", default
=1,
621 help="number of lines for each week (default 1, text only)"
625 dest
="spacing", type="int", default
=6,
626 help="spacing between months (default 6, text only)"
630 dest
="months", type="int", default
=3,
631 help="months per row (default 3, text only)"
635 dest
="css", default
="calendar.css",
636 help="CSS to use for page (html only)"
640 dest
="locale", default
=None,
641 help="locale to be used from month and weekday names"
645 dest
="encoding", default
=None,
646 help="Encoding to use for output"
650 dest
="type", default
="text",
651 choices
=("text", "html"),
652 help="output type (text or html)"
655 (options
, args
) = parser
.parse_args(args
)
657 if options
.locale
and not options
.encoding
:
658 parser
.error("if --locale is specified --encoding is required")
661 if options
.type == "html":
663 cal
= LocaleHTMLCalendar(locale
=options
.locale
)
666 encoding
= options
.encoding
668 encoding
= sys
.getdefaultencoding()
669 optdict
= dict(encoding
=encoding
, css
=options
.css
)
671 print cal
.formatyearpage(datetime
.date
.today().year
, **optdict
)
673 print cal
.formatyearpage(int(args
[1]), **optdict
)
675 parser
.error("incorrect number of arguments")
679 cal
= LocaleTextCalendar(locale
=options
.locale
)
682 optdict
= dict(w
=options
.width
, l
=options
.lines
)
684 optdict
["c"] = options
.spacing
685 optdict
["m"] = options
.months
687 result
= cal
.formatyear(datetime
.date
.today().year
, **optdict
)
689 result
= cal
.formatyear(int(args
[1]), **optdict
)
691 result
= cal
.formatmonth(int(args
[1]), int(args
[2]), **optdict
)
693 parser
.error("incorrect number of arguments")
696 result
= result
.encode(options
.encoding
)
700 if __name__
== "__main__":