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
11 import locale
as _locale
13 __all__
= ["IllegalMonthError", "IllegalWeekdayError", "setfirstweekday",
14 "firstweekday", "isleap", "leapdays", "weekday", "monthrange",
15 "monthcalendar", "prmonth", "month", "prcal", "calendar",
16 "timegm", "month_name", "month_abbr", "day_name", "day_abbr"]
18 # Exception raised for bad input (with string parameter for details)
21 # Exceptions raised for bad input
22 class IllegalMonthError(ValueError):
23 def __init__(self
, month
):
26 return "bad month number %r; must be 1-12" % self
.month
29 class IllegalWeekdayError(ValueError):
30 def __init__(self
, weekday
):
31 self
.weekday
= weekday
33 return "bad weekday number %r; must be 0 (Monday) to 6 (Sunday)" % self
.weekday
36 # Constants for months referenced later
40 # Number of days per month (except for February in leap years)
41 mdays
= [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
43 # This module used to have hard-coded lists of day and month names, as
44 # English strings. The classes following emulate a read-only version of
45 # that, but supply localized names. Note that the values are computed
46 # fresh on each call, in case the user changes locale between calls.
48 class _localized_month
:
50 _months
= [datetime
.date(2001, i
+1, 1).strftime
for i
in range(12)]
51 _months
.insert(0, lambda x
: "")
53 def __init__(self
, format
):
56 def __getitem__(self
, i
):
57 funcs
= self
._months
[i
]
58 if isinstance(i
, slice):
59 return [f(self
.format
) for f
in funcs
]
61 return funcs(self
.format
)
69 # January 1, 2001, was a Monday.
70 _days
= [datetime
.date(2001, 1, i
+1).strftime
for i
in range(7)]
72 def __init__(self
, format
):
75 def __getitem__(self
, i
):
77 if isinstance(i
, slice):
78 return [f(self
.format
) for f
in funcs
]
80 return funcs(self
.format
)
86 # Full and abbreviated names of weekdays
87 day_name
= _localized_day('%A')
88 day_abbr
= _localized_day('%a')
90 # Full and abbreviated names of months (1-based arrays!!!)
91 month_name
= _localized_month('%B')
92 month_abbr
= _localized_month('%b')
94 # Constants for weekdays
95 (MONDAY
, TUESDAY
, WEDNESDAY
, THURSDAY
, FRIDAY
, SATURDAY
, SUNDAY
) = range(7)
99 """Return 1 for leap years, 0 for non-leap years."""
100 return year
% 4 == 0 and (year
% 100 != 0 or year
% 400 == 0)
103 def leapdays(y1
, y2
):
104 """Return number of leap years in range [y1, y2).
108 return (y2
//4 - y1
//4) - (y2
//100 - y1
//100) + (y2
//400 - y1
//400)
111 def weekday(year
, month
, day
):
112 """Return weekday (0-6 ~ Mon-Sun) for year (1970-...), month (1-12),
114 return datetime
.date(year
, month
, day
).weekday()
117 def monthrange(year
, month
):
118 """Return weekday (0-6 ~ Mon-Sun) and number of days (28-31) for
120 if not 1 <= month
<= 12:
121 raise IllegalMonthError(month
)
122 day1
= weekday(year
, month
, 1)
123 ndays
= mdays
[month
] + (month
== February
and isleap(year
))
127 class Calendar(object):
129 Base calendar class. This class doesn't do any formatting. It simply
130 provides data to subclasses.
133 def __init__(self
, firstweekday
=0):
134 self
.firstweekday
= firstweekday
# 0 = Monday, 6 = Sunday
136 def getfirstweekday(self
):
137 return self
._firstweekday
% 7
139 def setfirstweekday(self
, firstweekday
):
140 self
._firstweekday
= firstweekday
142 firstweekday
= property(getfirstweekday
, setfirstweekday
)
144 def iterweekdays(self
):
146 Return a iterator for one week of weekday numbers starting with the
147 configured first one.
149 for i
in range(self
.firstweekday
, self
.firstweekday
+ 7):
152 def itermonthdates(self
, year
, month
):
154 Return an iterator for one month. The iterator will yield datetime.date
155 values and will always iterate through complete weeks, so it will yield
156 dates outside the specified month.
158 date
= datetime
.date(year
, month
, 1)
159 # Go back to the beginning of the week
160 days
= (date
.weekday() - self
.firstweekday
) % 7
161 date
-= datetime
.timedelta(days
=days
)
162 oneday
= datetime
.timedelta(days
=1)
166 if date
.month
!= month
and date
.weekday() == self
.firstweekday
:
169 def itermonthdays2(self
, year
, month
):
171 Like itermonthdates(), but will yield (day number, weekday number)
172 tuples. For days outside the specified month the day number is 0.
174 for date
in self
.itermonthdates(year
, month
):
175 if date
.month
!= month
:
176 yield (0, date
.weekday())
178 yield (date
.day
, date
.weekday())
180 def itermonthdays(self
, year
, month
):
182 Like itermonthdates(), but will yield day numbers tuples. For days
183 outside the specified month the day number is 0.
185 for date
in self
.itermonthdates(year
, month
):
186 if date
.month
!= month
:
191 def monthdatescalendar(self
, year
, month
):
193 Return a matrix (list of lists) representing a month's calendar.
194 Each row represents a week; week entries are datetime.date values.
196 dates
= list(self
.itermonthdates(year
, month
))
197 return [ dates
[i
:i
+7] for i
in range(0, len(dates
), 7) ]
199 def monthdays2calendar(self
, year
, month
):
201 Return a matrix representing a month's calendar.
202 Each row represents a week; week entries are
203 (day number, weekday number) tuples. Day numbers outside this month
206 days
= list(self
.itermonthdays2(year
, month
))
207 return [ days
[i
:i
+7] for i
in range(0, len(days
), 7) ]
209 def monthdayscalendar(self
, year
, month
):
211 Return a matrix representing a month's calendar.
212 Each row represents a week; days outside this month are zero.
214 days
= list(self
.itermonthdays(year
, month
))
215 return [ days
[i
:i
+7] for i
in range(0, len(days
), 7) ]
217 def yeardatescalendar(self
, year
, width
=3):
219 Return the data for the specified year ready for formatting. The return
220 value is a list of month rows. Each month row contains upto width months.
221 Each month contains between 4 and 6 weeks and each week contains 1-7
222 days. Days are datetime.date objects.
225 self
.monthdatescalendar(year
, i
)
226 for i
in range(January
, January
+12)
228 return [months
[i
:i
+width
] for i
in range(0, len(months
), width
) ]
230 def yeardays2calendar(self
, year
, width
=3):
232 Return the data for the specified year ready for formatting (similar to
233 yeardatescalendar()). Entries in the week lists are
234 (day number, weekday number) tuples. Day numbers outside this month are
238 self
.monthdays2calendar(year
, i
)
239 for i
in range(January
, January
+12)
241 return [months
[i
:i
+width
] for i
in range(0, len(months
), width
) ]
243 def yeardayscalendar(self
, year
, width
=3):
245 Return the data for the specified year ready for formatting (similar to
246 yeardatescalendar()). Entries in the week lists are day numbers.
247 Day numbers outside this month are zero.
250 self
.monthdayscalendar(year
, i
)
251 for i
in range(January
, January
+12)
253 return [months
[i
:i
+width
] for i
in range(0, len(months
), width
) ]
256 class TextCalendar(Calendar
):
258 Subclass of Calendar that outputs a calendar as a simple plain text
259 similar to the UNIX program cal.
262 def prweek(self
, theweek
, width
):
264 Print a single week (no newline).
266 print self
.formatweek(theweek
, width
),
268 def formatday(self
, day
, weekday
, width
):
270 Returns a formatted day.
275 s
= '%2i' % day
# right-align single-digit days
276 return s
.center(width
)
278 def formatweek(self
, theweek
, width
):
280 Returns a single week in a string (no newline).
282 return ' '.join(self
.formatday(d
, wd
, width
) for (d
, wd
) in theweek
)
284 def formatweekday(self
, day
, width
):
286 Returns a formatted week day name.
292 return names
[day
][:width
].center(width
)
294 def formatweekheader(self
, width
):
296 Return a header for a week.
298 return ' '.join(self
.formatweekday(i
, width
) for i
in self
.iterweekdays())
300 def formatmonthname(self
, theyear
, themonth
, width
, withyear
=True):
302 Return a formatted month name.
304 s
= month_name
[themonth
]
306 s
= "%s %r" % (s
, theyear
)
307 return s
.center(width
)
309 def prmonth(self
, theyear
, themonth
, w
=0, l
=0):
311 Print a month's calendar.
313 print self
.formatmonth(theyear
, themonth
, w
, l
),
315 def formatmonth(self
, theyear
, themonth
, w
=0, l
=0):
317 Return a month's calendar string (multi-line).
321 s
= self
.formatmonthname(theyear
, themonth
, 7 * (w
+ 1) - 1)
324 s
+= self
.formatweekheader(w
).rstrip()
326 for week
in self
.monthdays2calendar(theyear
, themonth
):
327 s
+= self
.formatweek(week
, w
).rstrip()
331 def formatyear(self
, theyear
, w
=2, l
=1, c
=6, m
=3):
333 Returns a year's calendar as a multi-line string.
338 colwidth
= (w
+ 1) * 7 - 1
341 a(repr(theyear
).center(colwidth
*m
+c
*(m
-1)).rstrip())
343 header
= self
.formatweekheader(w
)
344 for (i
, row
) in enumerate(self
.yeardays2calendar(theyear
, m
)):
346 months
= range(m
*i
+1, min(m
*(i
+1)+1, 13))
348 names
= (self
.formatmonthname(theyear
, k
, colwidth
, False)
350 a(formatstring(names
, colwidth
, c
).rstrip())
352 headers
= (header
for k
in months
)
353 a(formatstring(headers
, colwidth
, c
).rstrip())
355 # max number of weeks for this row
356 height
= max(len(cal
) for cal
in row
)
357 for j
in range(height
):
363 weeks
.append(self
.formatweek(cal
[j
], w
))
364 a(formatstring(weeks
, colwidth
, c
).rstrip())
368 def pryear(self
, theyear
, w
=0, l
=0, c
=6, m
=3):
369 """Print a year's calendar."""
370 print self
.formatyear(theyear
, w
, l
, c
, m
)
373 class HTMLCalendar(Calendar
):
375 This calendar returns complete HTML pages.
378 # CSS classes for the day <td>s
379 cssclasses
= ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
381 def formatday(self
, day
, weekday
):
383 Return a day as a table cell.
386 return '<td class="noday"> </td>' # day outside month
388 return '<td class="%s">%d</td>' % (self
.cssclasses
[weekday
], day
)
390 def formatweek(self
, theweek
):
392 Return a complete week as a table row.
394 s
= ''.join(self
.formatday(d
, wd
) for (d
, wd
) in theweek
)
395 return '<tr>%s</tr>' % s
397 def formatweekday(self
, day
):
399 Return a weekday name as a table header.
401 return '<th class="%s">%s</th>' % (self
.cssclasses
[day
], day_abbr
[day
])
403 def formatweekheader(self
):
405 Return a header for a week as a table row.
407 s
= ''.join(self
.formatweekday(i
) for i
in self
.iterweekdays())
408 return '<tr>%s</tr>' % s
410 def formatmonthname(self
, theyear
, themonth
, withyear
=True):
412 Return a month name as a table row.
415 s
= '%s %s' % (month_name
[themonth
], theyear
)
417 s
= '%s' % month_name
[themonth
]
418 return '<tr><th colspan="7" class="month">%s</th></tr>' % s
420 def formatmonth(self
, theyear
, themonth
, withyear
=True):
422 Return a formatted month as a table.
426 a('<table border="0" cellpadding="0" cellspacing="0" class="month">')
428 a(self
.formatmonthname(theyear
, themonth
, withyear
=withyear
))
430 a(self
.formatweekheader())
432 for week
in self
.monthdays2calendar(theyear
, themonth
):
433 a(self
.formatweek(week
))
439 def formatyear(self
, theyear
, width
=3):
441 Return a formatted year as a table of tables.
445 width
= max(width
, 1)
446 a('<table border="0" cellpadding="0" cellspacing="0" class="year">')
448 a('<tr><th colspan="%d" class="year">%s</th></tr>' % (width
, theyear
))
449 for i
in range(January
, January
+12, width
):
451 months
= range(i
, min(i
+width
, 13))
455 a(self
.formatmonth(theyear
, m
, withyear
=False))
461 def formatyearpage(self
, theyear
, width
=3, css
='calendar.css', encoding
=None):
463 Return a formatted year as a complete HTML page.
466 encoding
= sys
.getdefaultencoding()
469 a('<?xml version="1.0" encoding="%s"?>\n' % encoding
)
470 a('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n')
473 a('<meta http-equiv="Content-Type" content="text/html; charset=%s" />\n' % encoding
)
475 a('<link rel="stylesheet" type="text/css" href="%s" />\n' % css
)
476 a('<title>Calendar for %d</title>\n' % theyear
)
479 a(self
.formatyear(theyear
, width
))
482 return ''.join(v
).encode(encoding
, "xmlcharrefreplace")
486 def __init__(self
, locale
):
490 self
.oldlocale
= _locale
.setlocale(_locale
.LC_TIME
, self
.locale
)
491 return _locale
.getlocale(_locale
.LC_TIME
)[1]
493 def __exit__(self
, *args
):
494 _locale
.setlocale(_locale
.LC_TIME
, self
.oldlocale
)
497 class LocaleTextCalendar(TextCalendar
):
499 This class can be passed a locale name in the constructor and will return
500 month and weekday names in the specified locale. If this locale includes
501 an encoding all strings containing month and weekday names will be returned
505 def __init__(self
, firstweekday
=0, locale
=None):
506 TextCalendar
.__init
__(self
, firstweekday
)
508 locale
= _locale
.getdefaultlocale()
511 def formatweekday(self
, day
, width
):
512 with
TimeEncoding(self
.locale
) as encoding
:
518 if encoding
is not None:
519 name
= name
.decode(encoding
)
520 return name
[:width
].center(width
)
522 def formatmonthname(self
, theyear
, themonth
, width
, withyear
=True):
523 with
TimeEncoding(self
.locale
) as encoding
:
524 s
= month_name
[themonth
]
525 if encoding
is not None:
526 s
= s
.decode(encoding
)
528 s
= "%s %r" % (s
, theyear
)
529 return s
.center(width
)
532 class LocaleHTMLCalendar(HTMLCalendar
):
534 This class can be passed a locale name in the constructor and will return
535 month and weekday names in the specified locale. If this locale includes
536 an encoding all strings containing month and weekday names will be returned
539 def __init__(self
, firstweekday
=0, locale
=None):
540 HTMLCalendar
.__init
__(self
, firstweekday
)
542 locale
= _locale
.getdefaultlocale()
545 def formatweekday(self
, day
):
546 with
TimeEncoding(self
.locale
) as encoding
:
548 if encoding
is not None:
549 s
= s
.decode(encoding
)
550 return '<th class="%s">%s</th>' % (self
.cssclasses
[day
], s
)
552 def formatmonthname(self
, theyear
, themonth
, withyear
=True):
553 with
TimeEncoding(self
.locale
) as encoding
:
554 s
= month_name
[themonth
]
555 if encoding
is not None:
556 s
= s
.decode(encoding
)
558 s
= '%s %s' % (s
, theyear
)
559 return '<tr><th colspan="7" class="month">%s</th></tr>' % s
562 # Support for old module level interface
565 firstweekday
= c
.getfirstweekday
567 def setfirstweekday(firstweekday
):
568 if not MONDAY
<= firstweekday
<= SUNDAY
:
569 raise IllegalWeekdayError(firstweekday
)
570 c
.firstweekday
= firstweekday
572 monthcalendar
= c
.monthdayscalendar
575 weekheader
= c
.formatweekheader
577 month
= c
.formatmonth
578 calendar
= c
.formatyear
582 # Spacing of month columns for multi-column year calendar
583 _colwidth
= 7*3 - 1 # Amount printed by prweek()
584 _spacing
= 6 # Number of spaces between columns
587 def format(cols
, colwidth
=_colwidth
, spacing
=_spacing
):
588 """Prints multi-column formatting for year calendars"""
589 print formatstring(cols
, colwidth
, spacing
)
592 def formatstring(cols
, colwidth
=_colwidth
, spacing
=_spacing
):
593 """Returns a string formatted from n strings, centered within n columns."""
595 return spacing
.join(c
.center(colwidth
) for c
in cols
)
599 _EPOCH_ORD
= datetime
.date(EPOCH
, 1, 1).toordinal()
603 """Unrelated but handy function to calculate Unix timestamp from GMT."""
604 year
, month
, day
, hour
, minute
, second
= tuple[:6]
605 days
= datetime
.date(year
, month
, 1).toordinal() - _EPOCH_ORD
+ day
- 1
606 hours
= days
*24 + hour
607 minutes
= hours
*60 + minute
608 seconds
= minutes
*60 + second
614 parser
= optparse
.OptionParser(usage
="usage: %prog [options] [year [month]]")
617 dest
="width", type="int", default
=2,
618 help="width of date column (default 2, text only)"
622 dest
="lines", type="int", default
=1,
623 help="number of lines for each week (default 1, text only)"
627 dest
="spacing", type="int", default
=6,
628 help="spacing between months (default 6, text only)"
632 dest
="months", type="int", default
=3,
633 help="months per row (default 3, text only)"
637 dest
="css", default
="calendar.css",
638 help="CSS to use for page (html only)"
642 dest
="locale", default
=None,
643 help="locale to be used from month and weekday names"
647 dest
="encoding", default
=None,
648 help="Encoding to use for output"
652 dest
="type", default
="text",
653 choices
=("text", "html"),
654 help="output type (text or html)"
657 (options
, args
) = parser
.parse_args(args
)
659 if options
.locale
and not options
.encoding
:
660 parser
.error("if --locale is specified --encoding is required")
663 locale
= options
.locale
, options
.encoding
665 if options
.type == "html":
667 cal
= LocaleHTMLCalendar(locale
=locale
)
670 encoding
= options
.encoding
672 encoding
= sys
.getdefaultencoding()
673 optdict
= dict(encoding
=encoding
, css
=options
.css
)
675 print cal
.formatyearpage(datetime
.date
.today().year
, **optdict
)
677 print cal
.formatyearpage(int(args
[1]), **optdict
)
679 parser
.error("incorrect number of arguments")
683 cal
= LocaleTextCalendar(locale
=locale
)
686 optdict
= dict(w
=options
.width
, l
=options
.lines
)
688 optdict
["c"] = options
.spacing
689 optdict
["m"] = options
.months
691 result
= cal
.formatyear(datetime
.date
.today().year
, **optdict
)
693 result
= cal
.formatyear(int(args
[1]), **optdict
)
695 result
= cal
.formatmonth(int(args
[1]), int(args
[2]), **optdict
)
697 parser
.error("incorrect number of arguments")
700 result
= result
.encode(options
.encoding
)
704 if __name__
== "__main__":