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)."""
10 import locale
as _locale
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"]
17 # Exception raised for bad input (with string parameter for details)
20 # Exceptions raised for bad input
21 class IllegalMonthError(ValueError):
22 def __init__(self
, month
):
25 return "bad month number %r; must be 1-12" % self
.month
28 class IllegalWeekdayError(ValueError):
29 def __init__(self
, weekday
):
30 self
.weekday
= weekday
32 return "bad weekday number %r; must be 0 (Monday) to 6 (Sunday)" % self
.weekday
35 # Constants for months referenced later
39 # Number of days per month (except for February in leap years)
40 mdays
= [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
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.
47 class _localized_month
:
49 _months
= [datetime
.date(2001, i
+1, 1).strftime
for i
in range(12)]
50 _months
.insert(0, lambda x
: "")
52 def __init__(self
, format
):
55 def __getitem__(self
, i
):
56 funcs
= self
._months
[i
]
57 if isinstance(i
, slice):
58 return [f(self
.format
) for f
in funcs
]
60 return funcs(self
.format
)
68 # January 1, 2001, was a Monday.
69 _days
= [datetime
.date(2001, 1, i
+1).strftime
for i
in range(7)]
71 def __init__(self
, format
):
74 def __getitem__(self
, i
):
76 if isinstance(i
, slice):
77 return [f(self
.format
) for f
in funcs
]
79 return funcs(self
.format
)
85 # Full and abbreviated names of weekdays
86 day_name
= _localized_day('%A')
87 day_abbr
= _localized_day('%a')
89 # Full and abbreviated names of months (1-based arrays!!!)
90 month_name
= _localized_month('%B')
91 month_abbr
= _localized_month('%b')
93 # Constants for weekdays
94 (MONDAY
, TUESDAY
, WEDNESDAY
, THURSDAY
, FRIDAY
, SATURDAY
, SUNDAY
) = range(7)
98 """Return 1 for leap years, 0 for non-leap years."""
99 return year
% 4 == 0 and (year
% 100 != 0 or year
% 400 == 0)
102 def leapdays(y1
, y2
):
103 """Return number of leap years in range [y1, y2).
107 return (y2
//4 - y1
//4) - (y2
//100 - y1
//100) + (y2
//400 - y1
//400)
110 def weekday(year
, month
, day
):
111 """Return weekday (0-6 ~ Mon-Sun) for year (1970-...), month (1-12),
113 return datetime
.date(year
, month
, day
).weekday()
116 def monthrange(year
, month
):
117 """Return weekday (0-6 ~ Mon-Sun) and number of days (28-31) for
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
))
126 class Calendar(object):
128 Base calendar class. This class doesn't do any formatting. It simply
129 provides data to subclasses.
132 def __init__(self
, firstweekday
=0):
133 self
.firstweekday
= firstweekday
# 0 = Monday, 6 = Sunday
135 def getfirstweekday(self
):
136 return self
._firstweekday
% 7
138 def setfirstweekday(self
, firstweekday
):
139 self
._firstweekday
= firstweekday
141 firstweekday
= property(getfirstweekday
, setfirstweekday
)
143 def iterweekdays(self
):
145 Return a iterator for one week of weekday numbers starting with the
146 configured first one.
148 for i
in range(self
.firstweekday
, self
.firstweekday
+ 7):
151 def itermonthdates(self
, year
, month
):
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.
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)
165 if date
.month
!= month
and date
.weekday() == self
.firstweekday
:
168 def itermonthdays2(self
, year
, month
):
170 Like itermonthdates(), but will yield (day number, weekday number)
171 tuples. For days outside the specified month the day number is 0.
173 for date
in self
.itermonthdates(year
, month
):
174 if date
.month
!= month
:
175 yield (0, date
.weekday())
177 yield (date
.day
, date
.weekday())
179 def itermonthdays(self
, year
, month
):
181 Like itermonthdates(), but will yield day numbers. For days outside
182 the specified month the day number is 0.
184 for date
in self
.itermonthdates(year
, month
):
185 if date
.month
!= month
:
190 def monthdatescalendar(self
, year
, month
):
192 Return a matrix (list of lists) representing a month's calendar.
193 Each row represents a week; week entries are datetime.date values.
195 dates
= list(self
.itermonthdates(year
, month
))
196 return [ dates
[i
:i
+7] for i
in range(0, len(dates
), 7) ]
198 def monthdays2calendar(self
, year
, month
):
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
205 days
= list(self
.itermonthdays2(year
, month
))
206 return [ days
[i
:i
+7] for i
in range(0, len(days
), 7) ]
208 def monthdayscalendar(self
, year
, month
):
210 Return a matrix representing a month's calendar.
211 Each row represents a week; days outside this month are zero.
213 days
= list(self
.itermonthdays(year
, month
))
214 return [ days
[i
:i
+7] for i
in range(0, len(days
), 7) ]
216 def yeardatescalendar(self
, year
, width
=3):
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.
224 self
.monthdatescalendar(year
, i
)
225 for i
in range(January
, January
+12)
227 return [months
[i
:i
+width
] for i
in range(0, len(months
), width
) ]
229 def yeardays2calendar(self
, year
, width
=3):
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
237 self
.monthdays2calendar(year
, i
)
238 for i
in range(January
, January
+12)
240 return [months
[i
:i
+width
] for i
in range(0, len(months
), width
) ]
242 def yeardayscalendar(self
, year
, width
=3):
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.
249 self
.monthdayscalendar(year
, i
)
250 for i
in range(January
, January
+12)
252 return [months
[i
:i
+width
] for i
in range(0, len(months
), width
) ]
255 class TextCalendar(Calendar
):
257 Subclass of Calendar that outputs a calendar as a simple plain text
258 similar to the UNIX program cal.
261 def prweek(self
, theweek
, width
):
263 Print a single week (no newline).
265 print self
.formatweek(theweek
, width
),
267 def formatday(self
, day
, weekday
, width
):
269 Returns a formatted day.
274 s
= '%2i' % day
# right-align single-digit days
275 return s
.center(width
)
277 def formatweek(self
, theweek
, width
):
279 Returns a single week in a string (no newline).
281 return ' '.join(self
.formatday(d
, wd
, width
) for (d
, wd
) in theweek
)
283 def formatweekday(self
, day
, width
):
285 Returns a formatted week day name.
291 return names
[day
][:width
].center(width
)
293 def formatweekheader(self
, width
):
295 Return a header for a week.
297 return ' '.join(self
.formatweekday(i
, width
) for i
in self
.iterweekdays())
299 def formatmonthname(self
, theyear
, themonth
, width
, withyear
=True):
301 Return a formatted month name.
303 s
= month_name
[themonth
]
305 s
= "%s %r" % (s
, theyear
)
306 return s
.center(width
)
308 def prmonth(self
, theyear
, themonth
, w
=0, l
=0):
310 Print a month's calendar.
312 print self
.formatmonth(theyear
, themonth
, w
, l
),
314 def formatmonth(self
, theyear
, themonth
, w
=0, l
=0):
316 Return a month's calendar string (multi-line).
320 s
= self
.formatmonthname(theyear
, themonth
, 7 * (w
+ 1) - 1)
323 s
+= self
.formatweekheader(w
).rstrip()
325 for week
in self
.monthdays2calendar(theyear
, themonth
):
326 s
+= self
.formatweek(week
, w
).rstrip()
330 def formatyear(self
, theyear
, w
=2, l
=1, c
=6, m
=3):
332 Returns a year's calendar as a multi-line string.
337 colwidth
= (w
+ 1) * 7 - 1
340 a(repr(theyear
).center(colwidth
*m
+c
*(m
-1)).rstrip())
342 header
= self
.formatweekheader(w
)
343 for (i
, row
) in enumerate(self
.yeardays2calendar(theyear
, m
)):
345 months
= range(m
*i
+1, min(m
*(i
+1)+1, 13))
347 names
= (self
.formatmonthname(theyear
, k
, colwidth
, False)
349 a(formatstring(names
, colwidth
, c
).rstrip())
351 headers
= (header
for k
in months
)
352 a(formatstring(headers
, colwidth
, c
).rstrip())
354 # max number of weeks for this row
355 height
= max(len(cal
) for cal
in row
)
356 for j
in range(height
):
362 weeks
.append(self
.formatweek(cal
[j
], w
))
363 a(formatstring(weeks
, colwidth
, c
).rstrip())
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
)
372 class HTMLCalendar(Calendar
):
374 This calendar returns complete HTML pages.
377 # CSS classes for the day <td>s
378 cssclasses
= ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
380 def formatday(self
, day
, weekday
):
382 Return a day as a table cell.
385 return '<td class="noday"> </td>' # day outside month
387 return '<td class="%s">%d</td>' % (self
.cssclasses
[weekday
], day
)
389 def formatweek(self
, theweek
):
391 Return a complete week as a table row.
393 s
= ''.join(self
.formatday(d
, wd
) for (d
, wd
) in theweek
)
394 return '<tr>%s</tr>' % s
396 def formatweekday(self
, day
):
398 Return a weekday name as a table header.
400 return '<th class="%s">%s</th>' % (self
.cssclasses
[day
], day_abbr
[day
])
402 def formatweekheader(self
):
404 Return a header for a week as a table row.
406 s
= ''.join(self
.formatweekday(i
) for i
in self
.iterweekdays())
407 return '<tr>%s</tr>' % s
409 def formatmonthname(self
, theyear
, themonth
, withyear
=True):
411 Return a month name as a table row.
414 s
= '%s %s' % (month_name
[themonth
], theyear
)
416 s
= '%s' % month_name
[themonth
]
417 return '<tr><th colspan="7" class="month">%s</th></tr>' % s
419 def formatmonth(self
, theyear
, themonth
, withyear
=True):
421 Return a formatted month as a table.
425 a('<table border="0" cellpadding="0" cellspacing="0" class="month">')
427 a(self
.formatmonthname(theyear
, themonth
, withyear
=withyear
))
429 a(self
.formatweekheader())
431 for week
in self
.monthdays2calendar(theyear
, themonth
):
432 a(self
.formatweek(week
))
438 def formatyear(self
, theyear
, width
=3):
440 Return a formatted year as a table of tables.
444 width
= max(width
, 1)
445 a('<table border="0" cellpadding="0" cellspacing="0" class="year">')
447 a('<tr><th colspan="%d" class="year">%s</th></tr>' % (width
, theyear
))
448 for i
in range(January
, January
+12, width
):
450 months
= range(i
, min(i
+width
, 13))
454 a(self
.formatmonth(theyear
, m
, withyear
=False))
460 def formatyearpage(self
, theyear
, width
=3, css
='calendar.css', encoding
=None):
462 Return a formatted year as a complete HTML page.
465 encoding
= sys
.getdefaultencoding()
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')
472 a('<meta http-equiv="Content-Type" content="text/html; charset=%s" />\n' % encoding
)
474 a('<link rel="stylesheet" type="text/css" href="%s" />\n' % css
)
475 a('<title>Calendar for %d</title>\n' % theyear
)
478 a(self
.formatyear(theyear
, width
))
481 return ''.join(v
).encode(encoding
, "xmlcharrefreplace")
485 def __init__(self
, locale
):
489 self
.oldlocale
= _locale
.setlocale(_locale
.LC_TIME
, self
.locale
)
490 return _locale
.getlocale(_locale
.LC_TIME
)[1]
492 def __exit__(self
, *args
):
493 _locale
.setlocale(_locale
.LC_TIME
, self
.oldlocale
)
496 class LocaleTextCalendar(TextCalendar
):
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
504 def __init__(self
, firstweekday
=0, locale
=None):
505 TextCalendar
.__init
__(self
, firstweekday
)
507 locale
= _locale
.getdefaultlocale()
510 def formatweekday(self
, day
, width
):
511 with
TimeEncoding(self
.locale
) as encoding
:
517 if encoding
is not None:
518 name
= name
.decode(encoding
)
519 return name
[:width
].center(width
)
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
)
527 s
= "%s %r" % (s
, theyear
)
528 return s
.center(width
)
531 class LocaleHTMLCalendar(HTMLCalendar
):
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
538 def __init__(self
, firstweekday
=0, locale
=None):
539 HTMLCalendar
.__init
__(self
, firstweekday
)
541 locale
= _locale
.getdefaultlocale()
544 def formatweekday(self
, day
):
545 with
TimeEncoding(self
.locale
) as encoding
:
547 if encoding
is not None:
548 s
= s
.decode(encoding
)
549 return '<th class="%s">%s</th>' % (self
.cssclasses
[day
], s
)
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
)
557 s
= '%s %s' % (s
, theyear
)
558 return '<tr><th colspan="7" class="month">%s</th></tr>' % s
561 # Support for old module level interface
564 firstweekday
= c
.getfirstweekday
566 def setfirstweekday(firstweekday
):
567 if not MONDAY
<= firstweekday
<= SUNDAY
:
568 raise IllegalWeekdayError(firstweekday
)
569 c
.firstweekday
= firstweekday
571 monthcalendar
= c
.monthdayscalendar
574 weekheader
= c
.formatweekheader
576 month
= c
.formatmonth
577 calendar
= c
.formatyear
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
586 def format(cols
, colwidth
=_colwidth
, spacing
=_spacing
):
587 """Prints multi-column formatting for year calendars"""
588 print formatstring(cols
, colwidth
, spacing
)
591 def formatstring(cols
, colwidth
=_colwidth
, spacing
=_spacing
):
592 """Returns a string formatted from n strings, centered within n columns."""
594 return spacing
.join(c
.center(colwidth
) for c
in cols
)
598 _EPOCH_ORD
= datetime
.date(EPOCH
, 1, 1).toordinal()
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
613 parser
= optparse
.OptionParser(usage
="usage: %prog [options] [year [month]]")
616 dest
="width", type="int", default
=2,
617 help="width of date column (default 2, text only)"
621 dest
="lines", type="int", default
=1,
622 help="number of lines for each week (default 1, text only)"
626 dest
="spacing", type="int", default
=6,
627 help="spacing between months (default 6, text only)"
631 dest
="months", type="int", default
=3,
632 help="months per row (default 3, text only)"
636 dest
="css", default
="calendar.css",
637 help="CSS to use for page (html only)"
641 dest
="locale", default
=None,
642 help="locale to be used from month and weekday names"
646 dest
="encoding", default
=None,
647 help="Encoding to use for output"
651 dest
="type", default
="text",
652 choices
=("text", "html"),
653 help="output type (text or html)"
656 (options
, args
) = parser
.parse_args(args
)
658 if options
.locale
and not options
.encoding
:
659 parser
.error("if --locale is specified --encoding is required")
662 locale
= options
.locale
, options
.encoding
664 if options
.type == "html":
666 cal
= LocaleHTMLCalendar(locale
=locale
)
669 encoding
= options
.encoding
671 encoding
= sys
.getdefaultencoding()
672 optdict
= dict(encoding
=encoding
, css
=options
.css
)
674 print cal
.formatyearpage(datetime
.date
.today().year
, **optdict
)
676 print cal
.formatyearpage(int(args
[1]), **optdict
)
678 parser
.error("incorrect number of arguments")
682 cal
= LocaleTextCalendar(locale
=locale
)
685 optdict
= dict(w
=options
.width
, l
=options
.lines
)
687 optdict
["c"] = options
.spacing
688 optdict
["m"] = options
.months
690 result
= cal
.formatyear(datetime
.date
.today().year
, **optdict
)
692 result
= cal
.formatyear(int(args
[1]), **optdict
)
694 result
= cal
.formatmonth(int(args
[1]), int(args
[2]), **optdict
)
696 parser
.error("incorrect number of arguments")
699 result
= result
.encode(options
.encoding
)
703 if __name__
== "__main__":