blob: 0218e2d39770facfafe9a394dd423ce9eb7b59df [file] [log] [blame]
Skip Montanaroad3bc442000-08-30 14:01:28 +00001"""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)."""
Guido van Rossumc6360141990-10-13 19:23:40 +00007
Christian Heimes96f31632007-11-12 01:32:03 +00008import sys
9import datetime
10import locale as _locale
Alexander Belopolsky957b7562016-09-27 20:26:39 -040011from itertools import repeat
Guido van Rossumc6360141990-10-13 19:23:40 +000012
Thomas Wouters49fd7fa2006-04-21 10:40:58 +000013__all__ = ["IllegalMonthError", "IllegalWeekdayError", "setfirstweekday",
14 "firstweekday", "isleap", "leapdays", "weekday", "monthrange",
15 "monthcalendar", "prmonth", "month", "prcal", "calendar",
Martin Panter4eb376c2016-01-16 06:49:30 +000016 "timegm", "month_name", "month_abbr", "day_name", "day_abbr",
17 "Calendar", "TextCalendar", "HTMLCalendar", "LocaleTextCalendar",
18 "LocaleHTMLCalendar", "weekheader"]
Skip Montanaroe99d5ea2001-01-20 19:54:20 +000019
Guido van Rossumc6360141990-10-13 19:23:40 +000020# Exception raised for bad input (with string parameter for details)
Guido van Rossum00245cf1999-05-03 18:07:40 +000021error = ValueError
Guido van Rossumc6360141990-10-13 19:23:40 +000022
Thomas Wouters49fd7fa2006-04-21 10:40:58 +000023# Exceptions raised for bad input
24class IllegalMonthError(ValueError):
25 def __init__(self, month):
26 self.month = month
27 def __str__(self):
28 return "bad month number %r; must be 1-12" % self.month
29
30
31class IllegalWeekdayError(ValueError):
32 def __init__(self, weekday):
33 self.weekday = weekday
34 def __str__(self):
35 return "bad weekday number %r; must be 0 (Monday) to 6 (Sunday)" % self.weekday
36
37
Guido van Rossum9b3bc711993-06-20 21:02:22 +000038# Constants for months referenced later
39January = 1
40February = 2
41
42# Number of days per month (except for February in leap years)
43mdays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
44
Tim Peters0c2c8e72002-03-23 03:26:53 +000045# This module used to have hard-coded lists of day and month names, as
46# English strings. The classes following emulate a read-only version of
47# that, but supply localized names. Note that the values are computed
48# fresh on each call, in case the user changes locale between calls.
49
Raymond Hettinger9c051d72002-06-20 03:38:12 +000050class _localized_month:
Tim Petersbbc0d442004-11-13 16:18:32 +000051
Guido van Rossum805365e2007-05-07 22:24:25 +000052 _months = [datetime.date(2001, i+1, 1).strftime for i in range(12)]
Tim Petersbbc0d442004-11-13 16:18:32 +000053 _months.insert(0, lambda x: "")
54
Tim Peters0c2c8e72002-03-23 03:26:53 +000055 def __init__(self, format):
Barry Warsaw1d099102001-05-22 15:58:30 +000056 self.format = format
Tim Peters0c2c8e72002-03-23 03:26:53 +000057
58 def __getitem__(self, i):
Tim Petersbbc0d442004-11-13 16:18:32 +000059 funcs = self._months[i]
60 if isinstance(i, slice):
61 return [f(self.format) for f in funcs]
62 else:
63 return funcs(self.format)
Tim Peters0c2c8e72002-03-23 03:26:53 +000064
Skip Montanaro4c834952002-03-15 04:08:38 +000065 def __len__(self):
Tim Peters0c2c8e72002-03-23 03:26:53 +000066 return 13
67
Thomas Wouters49fd7fa2006-04-21 10:40:58 +000068
Raymond Hettinger9c051d72002-06-20 03:38:12 +000069class _localized_day:
Tim Petersbbc0d442004-11-13 16:18:32 +000070
71 # January 1, 2001, was a Monday.
Guido van Rossum805365e2007-05-07 22:24:25 +000072 _days = [datetime.date(2001, 1, i+1).strftime for i in range(7)]
Tim Petersbbc0d442004-11-13 16:18:32 +000073
Tim Peters0c2c8e72002-03-23 03:26:53 +000074 def __init__(self, format):
75 self.format = format
76
77 def __getitem__(self, i):
Tim Petersbbc0d442004-11-13 16:18:32 +000078 funcs = self._days[i]
79 if isinstance(i, slice):
80 return [f(self.format) for f in funcs]
81 else:
82 return funcs(self.format)
Tim Peters0c2c8e72002-03-23 03:26:53 +000083
Neal Norwitz492faa52004-06-07 03:47:06 +000084 def __len__(self):
Tim Peters0c2c8e72002-03-23 03:26:53 +000085 return 7
Barry Warsaw1d099102001-05-22 15:58:30 +000086
Thomas Wouters49fd7fa2006-04-21 10:40:58 +000087
Guido van Rossum9b3bc711993-06-20 21:02:22 +000088# Full and abbreviated names of weekdays
Tim Peters0c2c8e72002-03-23 03:26:53 +000089day_name = _localized_day('%A')
90day_abbr = _localized_day('%a')
Guido van Rossum9b3bc711993-06-20 21:02:22 +000091
Guido van Rossum5cfa5df1993-06-23 09:30:50 +000092# Full and abbreviated names of months (1-based arrays!!!)
Tim Peters0c2c8e72002-03-23 03:26:53 +000093month_name = _localized_month('%B')
94month_abbr = _localized_month('%b')
Guido van Rossum9b3bc711993-06-20 21:02:22 +000095
Skip Montanaroad3bc442000-08-30 14:01:28 +000096# Constants for weekdays
97(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7)
98
Skip Montanaroad3bc442000-08-30 14:01:28 +000099
Guido van Rossum9b3bc711993-06-20 21:02:22 +0000100def isleap(year):
Alexander Belopolskyf87cc042010-10-19 17:43:50 +0000101 """Return True for leap years, False for non-leap years."""
Fred Drake8152d322000-12-12 23:20:45 +0000102 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
Guido van Rossum9b3bc711993-06-20 21:02:22 +0000103
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000104
Guido van Rossum9b3bc711993-06-20 21:02:22 +0000105def leapdays(y1, y2):
Guido van Rossum4acc25b2000-02-02 15:10:15 +0000106 """Return number of leap years in range [y1, y2).
Guido van Rossum46735ad2000-10-09 12:42:04 +0000107 Assume y1 <= y2."""
108 y1 -= 1
109 y2 -= 1
Raymond Hettingere11b5102002-12-25 16:37:19 +0000110 return (y2//4 - y1//4) - (y2//100 - y1//100) + (y2//400 - y1//400)
Guido van Rossum9b3bc711993-06-20 21:02:22 +0000111
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000112
Guido van Rossumc6360141990-10-13 19:23:40 +0000113def weekday(year, month, day):
Skip Montanaroad3bc442000-08-30 14:01:28 +0000114 """Return weekday (0-6 ~ Mon-Sun) for year (1970-...), month (1-12),
115 day (1-31)."""
Raymond Hettingere11b5102002-12-25 16:37:19 +0000116 return datetime.date(year, month, day).weekday()
Guido van Rossumc6360141990-10-13 19:23:40 +0000117
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000118
Guido van Rossumc6360141990-10-13 19:23:40 +0000119def monthrange(year, month):
Skip Montanaroad3bc442000-08-30 14:01:28 +0000120 """Return weekday (0-6 ~ Mon-Sun) and number of days (28-31) for
121 year, month."""
122 if not 1 <= month <= 12:
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000123 raise IllegalMonthError(month)
Guido van Rossum4acc25b2000-02-02 15:10:15 +0000124 day1 = weekday(year, month, 1)
125 ndays = mdays[month] + (month == February and isleap(year))
126 return day1, ndays
Guido van Rossumc6360141990-10-13 19:23:40 +0000127
Guido van Rossumc6360141990-10-13 19:23:40 +0000128
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000129class Calendar(object):
130 """
131 Base calendar class. This class doesn't do any formatting. It simply
132 provides data to subclasses.
133 """
Skip Montanaroad3bc442000-08-30 14:01:28 +0000134
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000135 def __init__(self, firstweekday=0):
136 self.firstweekday = firstweekday # 0 = Monday, 6 = Sunday
137
138 def getfirstweekday(self):
139 return self._firstweekday % 7
140
141 def setfirstweekday(self, firstweekday):
142 self._firstweekday = firstweekday
143
144 firstweekday = property(getfirstweekday, setfirstweekday)
145
146 def iterweekdays(self):
147 """
Martin Panter7462b6492015-11-02 03:37:02 +0000148 Return an iterator for one week of weekday numbers starting with the
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000149 configured first one.
150 """
Guido van Rossum805365e2007-05-07 22:24:25 +0000151 for i in range(self.firstweekday, self.firstweekday + 7):
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000152 yield i%7
153
154 def itermonthdates(self, year, month):
155 """
156 Return an iterator for one month. The iterator will yield datetime.date
157 values and will always iterate through complete weeks, so it will yield
158 dates outside the specified month.
159 """
160 date = datetime.date(year, month, 1)
161 # Go back to the beginning of the week
162 days = (date.weekday() - self.firstweekday) % 7
163 date -= datetime.timedelta(days=days)
164 oneday = datetime.timedelta(days=1)
165 while True:
166 yield date
Ezio Melotti85710a42012-09-21 17:26:35 +0300167 try:
168 date += oneday
169 except OverflowError:
170 # Adding one day could fail after datetime.MAXYEAR
171 break
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000172 if date.month != month and date.weekday() == self.firstweekday:
173 break
174
175 def itermonthdays2(self, year, month):
176 """
177 Like itermonthdates(), but will yield (day number, weekday number)
178 tuples. For days outside the specified month the day number is 0.
179 """
Alexander Belopolsky957b7562016-09-27 20:26:39 -0400180 for i, d in enumerate(self.itermonthdays(year, month), self.firstweekday):
181 yield d, i % 7
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000182
183 def itermonthdays(self, year, month):
184 """
Christian Heimes77c02eb2008-02-09 02:18:51 +0000185 Like itermonthdates(), but will yield day numbers. For days outside
186 the specified month the day number is 0.
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000187 """
Alexander Belopolsky957b7562016-09-27 20:26:39 -0400188 day1, ndays = monthrange(year, month)
189 days_before = (day1 - self.firstweekday) % 7
190 yield from repeat(0, days_before)
191 yield from range(1, ndays + 1)
192 days_after = (self.firstweekday - day1 - ndays) % 7
193 yield from repeat(0, days_after)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000194
195 def monthdatescalendar(self, year, month):
196 """
197 Return a matrix (list of lists) representing a month's calendar.
198 Each row represents a week; week entries are datetime.date values.
199 """
200 dates = list(self.itermonthdates(year, month))
Guido van Rossum805365e2007-05-07 22:24:25 +0000201 return [ dates[i:i+7] for i in range(0, len(dates), 7) ]
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000202
203 def monthdays2calendar(self, year, month):
204 """
205 Return a matrix representing a month's calendar.
206 Each row represents a week; week entries are
207 (day number, weekday number) tuples. Day numbers outside this month
208 are zero.
209 """
210 days = list(self.itermonthdays2(year, month))
Guido van Rossum805365e2007-05-07 22:24:25 +0000211 return [ days[i:i+7] for i in range(0, len(days), 7) ]
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000212
213 def monthdayscalendar(self, year, month):
214 """
215 Return a matrix representing a month's calendar.
216 Each row represents a week; days outside this month are zero.
217 """
218 days = list(self.itermonthdays(year, month))
Guido van Rossum805365e2007-05-07 22:24:25 +0000219 return [ days[i:i+7] for i in range(0, len(days), 7) ]
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000220
221 def yeardatescalendar(self, year, width=3):
222 """
223 Return the data for the specified year ready for formatting. The return
Ezio Melotti30b9d5d2013-08-17 15:50:46 +0300224 value is a list of month rows. Each month row contains up to width months.
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000225 Each month contains between 4 and 6 weeks and each week contains 1-7
226 days. Days are datetime.date objects.
227 """
228 months = [
229 self.monthdatescalendar(year, i)
Guido van Rossum805365e2007-05-07 22:24:25 +0000230 for i in range(January, January+12)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000231 ]
Guido van Rossum805365e2007-05-07 22:24:25 +0000232 return [months[i:i+width] for i in range(0, len(months), width) ]
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000233
234 def yeardays2calendar(self, year, width=3):
235 """
236 Return the data for the specified year ready for formatting (similar to
237 yeardatescalendar()). Entries in the week lists are
238 (day number, weekday number) tuples. Day numbers outside this month are
239 zero.
240 """
241 months = [
242 self.monthdays2calendar(year, i)
Guido van Rossum805365e2007-05-07 22:24:25 +0000243 for i in range(January, January+12)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000244 ]
Guido van Rossum805365e2007-05-07 22:24:25 +0000245 return [months[i:i+width] for i in range(0, len(months), width) ]
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000246
247 def yeardayscalendar(self, year, width=3):
248 """
249 Return the data for the specified year ready for formatting (similar to
250 yeardatescalendar()). Entries in the week lists are day numbers.
251 Day numbers outside this month are zero.
252 """
253 months = [
254 self.monthdayscalendar(year, i)
Guido van Rossum805365e2007-05-07 22:24:25 +0000255 for i in range(January, January+12)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000256 ]
Guido van Rossum805365e2007-05-07 22:24:25 +0000257 return [months[i:i+width] for i in range(0, len(months), width) ]
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000258
259
260class TextCalendar(Calendar):
261 """
262 Subclass of Calendar that outputs a calendar as a simple plain text
263 similar to the UNIX program cal.
264 """
265
266 def prweek(self, theweek, width):
267 """
268 Print a single week (no newline).
269 """
Serhiy Storchaka0595ed22016-10-25 15:20:58 +0300270 print(self.formatweek(theweek, width), end='')
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000271
272 def formatday(self, day, weekday, width):
273 """
274 Returns a formatted day.
275 """
Skip Montanaroad3bc442000-08-30 14:01:28 +0000276 if day == 0:
277 s = ''
278 else:
279 s = '%2i' % day # right-align single-digit days
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000280 return s.center(width)
Guido van Rossumc6360141990-10-13 19:23:40 +0000281
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000282 def formatweek(self, theweek, width):
283 """
284 Returns a single week in a string (no newline).
285 """
286 return ' '.join(self.formatday(d, wd, width) for (d, wd) in theweek)
Guido van Rossumc6360141990-10-13 19:23:40 +0000287
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000288 def formatweekday(self, day, width):
289 """
290 Returns a formatted week day name.
291 """
292 if width >= 9:
293 names = day_name
294 else:
295 names = day_abbr
296 return names[day][:width].center(width)
Skip Montanaroad3bc442000-08-30 14:01:28 +0000297
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000298 def formatweekheader(self, width):
299 """
300 Return a header for a week.
301 """
302 return ' '.join(self.formatweekday(i, width) for i in self.iterweekdays())
Guido van Rossumc6360141990-10-13 19:23:40 +0000303
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000304 def formatmonthname(self, theyear, themonth, width, withyear=True):
305 """
306 Return a formatted month name.
307 """
308 s = month_name[themonth]
309 if withyear:
310 s = "%s %r" % (s, theyear)
311 return s.center(width)
312
313 def prmonth(self, theyear, themonth, w=0, l=0):
314 """
315 Print a month's calendar.
316 """
Serhiy Storchaka7ff51bd2016-10-25 15:00:52 +0300317 print(self.formatmonth(theyear, themonth, w, l), end='')
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000318
319 def formatmonth(self, theyear, themonth, w=0, l=0):
320 """
321 Return a month's calendar string (multi-line).
322 """
323 w = max(2, w)
324 l = max(1, l)
325 s = self.formatmonthname(theyear, themonth, 7 * (w + 1) - 1)
326 s = s.rstrip()
327 s += '\n' * l
328 s += self.formatweekheader(w).rstrip()
329 s += '\n' * l
330 for week in self.monthdays2calendar(theyear, themonth):
331 s += self.formatweek(week, w).rstrip()
332 s += '\n' * l
333 return s
334
335 def formatyear(self, theyear, w=2, l=1, c=6, m=3):
336 """
337 Returns a year's calendar as a multi-line string.
338 """
339 w = max(2, w)
340 l = max(1, l)
341 c = max(2, c)
342 colwidth = (w + 1) * 7 - 1
343 v = []
344 a = v.append
345 a(repr(theyear).center(colwidth*m+c*(m-1)).rstrip())
346 a('\n'*l)
347 header = self.formatweekheader(w)
348 for (i, row) in enumerate(self.yeardays2calendar(theyear, m)):
349 # months in this row
Guido van Rossum805365e2007-05-07 22:24:25 +0000350 months = range(m*i+1, min(m*(i+1)+1, 13))
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000351 a('\n'*l)
352 names = (self.formatmonthname(theyear, k, colwidth, False)
353 for k in months)
354 a(formatstring(names, colwidth, c).rstrip())
355 a('\n'*l)
356 headers = (header for k in months)
357 a(formatstring(headers, colwidth, c).rstrip())
358 a('\n'*l)
359 # max number of weeks for this row
360 height = max(len(cal) for cal in row)
Guido van Rossum805365e2007-05-07 22:24:25 +0000361 for j in range(height):
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000362 weeks = []
363 for cal in row:
364 if j >= len(cal):
365 weeks.append('')
366 else:
367 weeks.append(self.formatweek(cal[j], w))
368 a(formatstring(weeks, colwidth, c).rstrip())
369 a('\n' * l)
370 return ''.join(v)
371
372 def pryear(self, theyear, w=0, l=0, c=6, m=3):
373 """Print a year's calendar."""
Serhiy Storchaka0595ed22016-10-25 15:20:58 +0300374 print(self.formatyear(theyear, w, l, c, m), end='')
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000375
376
377class HTMLCalendar(Calendar):
378 """
379 This calendar returns complete HTML pages.
380 """
381
382 # CSS classes for the day <td>s
383 cssclasses = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
384
Oz N Tiram8b7a4cc2017-06-06 11:35:59 +0200385 # CSS classes for the day <th>s
386 cssclasses_weekday_head = cssclasses
387
388 # CSS class for the days before and after current month
389 cssclass_noday = "noday"
390
391 # CSS class for the month's head
392 cssclass_month_head = "month"
393
394 # CSS class for the month
395 cssclass_month = "month"
396
397 # CSS class for the year's table head
398 cssclass_year_head = "year"
399
400 # CSS class for the whole year table
401 cssclass_year = "year"
402
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000403 def formatday(self, day, weekday):
404 """
405 Return a day as a table cell.
406 """
407 if day == 0:
Oz N Tiram8b7a4cc2017-06-06 11:35:59 +0200408 # day outside month
409 return '<td class="%s">&nbsp;</td>' % self.cssclass_noday
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000410 else:
411 return '<td class="%s">%d</td>' % (self.cssclasses[weekday], day)
412
413 def formatweek(self, theweek):
414 """
415 Return a complete week as a table row.
416 """
417 s = ''.join(self.formatday(d, wd) for (d, wd) in theweek)
418 return '<tr>%s</tr>' % s
419
420 def formatweekday(self, day):
421 """
422 Return a weekday name as a table header.
423 """
Oz N Tiram8b7a4cc2017-06-06 11:35:59 +0200424 return '<th class="%s">%s</th>' % (
425 self.cssclasses_weekday_head[day], day_abbr[day])
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000426
427 def formatweekheader(self):
428 """
429 Return a header for a week as a table row.
430 """
431 s = ''.join(self.formatweekday(i) for i in self.iterweekdays())
432 return '<tr>%s</tr>' % s
433
434 def formatmonthname(self, theyear, themonth, withyear=True):
435 """
436 Return a month name as a table row.
437 """
438 if withyear:
439 s = '%s %s' % (month_name[themonth], theyear)
440 else:
441 s = '%s' % month_name[themonth]
Oz N Tiram8b7a4cc2017-06-06 11:35:59 +0200442 return '<tr><th colspan="7" class="%s">%s</th></tr>' % (
443 self.cssclass_month_head, s)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000444
445 def formatmonth(self, theyear, themonth, withyear=True):
446 """
447 Return a formatted month as a table.
448 """
449 v = []
450 a = v.append
Oz N Tiram8b7a4cc2017-06-06 11:35:59 +0200451 a('<table border="0" cellpadding="0" cellspacing="0" class="%s">' % (
452 self.cssclass_month))
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000453 a('\n')
454 a(self.formatmonthname(theyear, themonth, withyear=withyear))
455 a('\n')
456 a(self.formatweekheader())
457 a('\n')
458 for week in self.monthdays2calendar(theyear, themonth):
459 a(self.formatweek(week))
460 a('\n')
461 a('</table>')
462 a('\n')
463 return ''.join(v)
464
465 def formatyear(self, theyear, width=3):
466 """
467 Return a formatted year as a table of tables.
468 """
469 v = []
470 a = v.append
471 width = max(width, 1)
Oz N Tiram8b7a4cc2017-06-06 11:35:59 +0200472 a('<table border="0" cellpadding="0" cellspacing="0" class="%s">' %
473 self.cssclass_year)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000474 a('\n')
Oz N Tiram8b7a4cc2017-06-06 11:35:59 +0200475 a('<tr><th colspan="%d" class="%s">%s</th></tr>' % (
476 width, self.cssclass_year_head, theyear))
Guido van Rossum805365e2007-05-07 22:24:25 +0000477 for i in range(January, January+12, width):
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000478 # months in this row
Guido van Rossum805365e2007-05-07 22:24:25 +0000479 months = range(i, min(i+width, 13))
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000480 a('<tr>')
481 for m in months:
482 a('<td>')
483 a(self.formatmonth(theyear, m, withyear=False))
484 a('</td>')
485 a('</tr>')
486 a('</table>')
487 return ''.join(v)
488
489 def formatyearpage(self, theyear, width=3, css='calendar.css', encoding=None):
490 """
491 Return a formatted year as a complete HTML page.
492 """
493 if encoding is None:
494 encoding = sys.getdefaultencoding()
495 v = []
496 a = v.append
497 a('<?xml version="1.0" encoding="%s"?>\n' % encoding)
498 a('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n')
499 a('<html>\n')
500 a('<head>\n')
501 a('<meta http-equiv="Content-Type" content="text/html; charset=%s" />\n' % encoding)
502 if css is not None:
503 a('<link rel="stylesheet" type="text/css" href="%s" />\n' % css)
Thomas Wouters47b49bf2007-08-30 22:15:33 +0000504 a('<title>Calendar for %d</title>\n' % theyear)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000505 a('</head>\n')
506 a('<body>\n')
507 a(self.formatyear(theyear, width))
508 a('</body>\n')
509 a('</html>\n')
510 return ''.join(v).encode(encoding, "xmlcharrefreplace")
511
512
Georg Brandlfd680872008-06-08 08:40:05 +0000513class different_locale:
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000514 def __init__(self, locale):
515 self.locale = locale
516
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000517 def __enter__(self):
Georg Brandl7004bd12010-10-19 18:54:25 +0000518 self.oldlocale = _locale.getlocale(_locale.LC_TIME)
519 _locale.setlocale(_locale.LC_TIME, self.locale)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000520
521 def __exit__(self, *args):
Christian Heimes96f31632007-11-12 01:32:03 +0000522 _locale.setlocale(_locale.LC_TIME, self.oldlocale)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000523
524
525class LocaleTextCalendar(TextCalendar):
526 """
527 This class can be passed a locale name in the constructor and will return
528 month and weekday names in the specified locale. If this locale includes
529 an encoding all strings containing month and weekday names will be returned
530 as unicode.
531 """
532
533 def __init__(self, firstweekday=0, locale=None):
534 TextCalendar.__init__(self, firstweekday)
535 if locale is None:
Christian Heimes96f31632007-11-12 01:32:03 +0000536 locale = _locale.getdefaultlocale()
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000537 self.locale = locale
538
539 def formatweekday(self, day, width):
Georg Brandlfd680872008-06-08 08:40:05 +0000540 with different_locale(self.locale):
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000541 if width >= 9:
542 names = day_name
543 else:
544 names = day_abbr
545 name = names[day]
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000546 return name[:width].center(width)
547
548 def formatmonthname(self, theyear, themonth, width, withyear=True):
Georg Brandlfd680872008-06-08 08:40:05 +0000549 with different_locale(self.locale):
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000550 s = month_name[themonth]
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000551 if withyear:
552 s = "%s %r" % (s, theyear)
553 return s.center(width)
554
555
556class LocaleHTMLCalendar(HTMLCalendar):
557 """
558 This class can be passed a locale name in the constructor and will return
559 month and weekday names in the specified locale. If this locale includes
560 an encoding all strings containing month and weekday names will be returned
561 as unicode.
562 """
563 def __init__(self, firstweekday=0, locale=None):
564 HTMLCalendar.__init__(self, firstweekday)
565 if locale is None:
Christian Heimes96f31632007-11-12 01:32:03 +0000566 locale = _locale.getdefaultlocale()
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000567 self.locale = locale
568
569 def formatweekday(self, day):
Georg Brandlfd680872008-06-08 08:40:05 +0000570 with different_locale(self.locale):
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000571 s = day_abbr[day]
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000572 return '<th class="%s">%s</th>' % (self.cssclasses[day], s)
573
574 def formatmonthname(self, theyear, themonth, withyear=True):
Georg Brandlfd680872008-06-08 08:40:05 +0000575 with different_locale(self.locale):
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000576 s = month_name[themonth]
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000577 if withyear:
578 s = '%s %s' % (s, theyear)
579 return '<tr><th colspan="7" class="month">%s</th></tr>' % s
580
581
582# Support for old module level interface
583c = TextCalendar()
584
585firstweekday = c.getfirstweekday
586
587def setfirstweekday(firstweekday):
588 if not MONDAY <= firstweekday <= SUNDAY:
589 raise IllegalWeekdayError(firstweekday)
590 c.firstweekday = firstweekday
591
592monthcalendar = c.monthdayscalendar
593prweek = c.prweek
594week = c.formatweek
595weekheader = c.formatweekheader
596prmonth = c.prmonth
597month = c.formatmonth
598calendar = c.formatyear
599prcal = c.pryear
600
601
602# Spacing of month columns for multi-column year calendar
Guido van Rossum4acc25b2000-02-02 15:10:15 +0000603_colwidth = 7*3 - 1 # Amount printed by prweek()
Skip Montanaroad3bc442000-08-30 14:01:28 +0000604_spacing = 6 # Number of spaces between columns
Guido van Rossumc6360141990-10-13 19:23:40 +0000605
Guido van Rossumc6360141990-10-13 19:23:40 +0000606
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000607def format(cols, colwidth=_colwidth, spacing=_spacing):
608 """Prints multi-column formatting for year calendars"""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000609 print(formatstring(cols, colwidth, spacing))
Skip Montanaroad3bc442000-08-30 14:01:28 +0000610
Skip Montanaroad3bc442000-08-30 14:01:28 +0000611
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000612def formatstring(cols, colwidth=_colwidth, spacing=_spacing):
613 """Returns a string formatted from n strings, centered within n columns."""
614 spacing *= ' '
615 return spacing.join(c.center(colwidth) for c in cols)
616
Guido van Rossumb39aff81999-06-09 15:07:38 +0000617
Guido van Rossumb39aff81999-06-09 15:07:38 +0000618EPOCH = 1970
Alexander Belopolsky97958cf2010-06-14 18:33:19 +0000619_EPOCH_ORD = datetime.date(EPOCH, 1, 1).toordinal()
620
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000621
Guido van Rossumb39aff81999-06-09 15:07:38 +0000622def timegm(tuple):
Guido van Rossum4acc25b2000-02-02 15:10:15 +0000623 """Unrelated but handy function to calculate Unix timestamp from GMT."""
Alexander Belopolsky97958cf2010-06-14 18:33:19 +0000624 year, month, day, hour, minute, second = tuple[:6]
625 days = datetime.date(year, month, 1).toordinal() - _EPOCH_ORD + day - 1
626 hours = days*24 + hour
627 minutes = hours*60 + minute
628 seconds = minutes*60 + second
629 return seconds
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000630
631
632def main(args):
Serhiy Storchaka97852612015-11-01 17:14:27 +0200633 import argparse
634 parser = argparse.ArgumentParser()
635 textgroup = parser.add_argument_group('text only arguments')
636 htmlgroup = parser.add_argument_group('html only arguments')
637 textgroup.add_argument(
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000638 "-w", "--width",
Serhiy Storchaka97852612015-11-01 17:14:27 +0200639 type=int, default=2,
640 help="width of date column (default 2)"
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000641 )
Serhiy Storchaka97852612015-11-01 17:14:27 +0200642 textgroup.add_argument(
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000643 "-l", "--lines",
Serhiy Storchaka97852612015-11-01 17:14:27 +0200644 type=int, default=1,
645 help="number of lines for each week (default 1)"
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000646 )
Serhiy Storchaka97852612015-11-01 17:14:27 +0200647 textgroup.add_argument(
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000648 "-s", "--spacing",
Serhiy Storchaka97852612015-11-01 17:14:27 +0200649 type=int, default=6,
650 help="spacing between months (default 6)"
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000651 )
Serhiy Storchaka97852612015-11-01 17:14:27 +0200652 textgroup.add_argument(
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000653 "-m", "--months",
Serhiy Storchaka97852612015-11-01 17:14:27 +0200654 type=int, default=3,
655 help="months per row (default 3)"
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000656 )
Serhiy Storchaka97852612015-11-01 17:14:27 +0200657 htmlgroup.add_argument(
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000658 "-c", "--css",
Serhiy Storchaka97852612015-11-01 17:14:27 +0200659 default="calendar.css",
660 help="CSS to use for page"
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000661 )
Serhiy Storchaka97852612015-11-01 17:14:27 +0200662 parser.add_argument(
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000663 "-L", "--locale",
Serhiy Storchaka97852612015-11-01 17:14:27 +0200664 default=None,
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000665 help="locale to be used from month and weekday names"
666 )
Serhiy Storchaka97852612015-11-01 17:14:27 +0200667 parser.add_argument(
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000668 "-e", "--encoding",
Serhiy Storchaka97852612015-11-01 17:14:27 +0200669 default=None,
670 help="encoding to use for output"
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000671 )
Serhiy Storchaka97852612015-11-01 17:14:27 +0200672 parser.add_argument(
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000673 "-t", "--type",
Serhiy Storchaka97852612015-11-01 17:14:27 +0200674 default="text",
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000675 choices=("text", "html"),
676 help="output type (text or html)"
677 )
Serhiy Storchaka97852612015-11-01 17:14:27 +0200678 parser.add_argument(
679 "year",
680 nargs='?', type=int,
681 help="year number (1-9999)"
682 )
683 parser.add_argument(
684 "month",
685 nargs='?', type=int,
686 help="month number (1-12, text only)"
687 )
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000688
Serhiy Storchaka97852612015-11-01 17:14:27 +0200689 options = parser.parse_args(args[1:])
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000690
691 if options.locale and not options.encoding:
692 parser.error("if --locale is specified --encoding is required")
693 sys.exit(1)
694
Christian Heimes96f31632007-11-12 01:32:03 +0000695 locale = options.locale, options.encoding
696
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000697 if options.type == "html":
698 if options.locale:
Christian Heimes96f31632007-11-12 01:32:03 +0000699 cal = LocaleHTMLCalendar(locale=locale)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000700 else:
701 cal = HTMLCalendar()
702 encoding = options.encoding
703 if encoding is None:
704 encoding = sys.getdefaultencoding()
705 optdict = dict(encoding=encoding, css=options.css)
Senthil Kumaran962fed92011-08-11 09:22:52 +0800706 write = sys.stdout.buffer.write
Serhiy Storchaka97852612015-11-01 17:14:27 +0200707 if options.year is None:
Senthil Kumaran962fed92011-08-11 09:22:52 +0800708 write(cal.formatyearpage(datetime.date.today().year, **optdict))
Serhiy Storchaka97852612015-11-01 17:14:27 +0200709 elif options.month is None:
710 write(cal.formatyearpage(options.year, **optdict))
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000711 else:
712 parser.error("incorrect number of arguments")
713 sys.exit(1)
714 else:
715 if options.locale:
Christian Heimes96f31632007-11-12 01:32:03 +0000716 cal = LocaleTextCalendar(locale=locale)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000717 else:
718 cal = TextCalendar()
719 optdict = dict(w=options.width, l=options.lines)
Serhiy Storchaka97852612015-11-01 17:14:27 +0200720 if options.month is None:
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000721 optdict["c"] = options.spacing
722 optdict["m"] = options.months
Serhiy Storchaka97852612015-11-01 17:14:27 +0200723 if options.year is None:
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000724 result = cal.formatyear(datetime.date.today().year, **optdict)
Serhiy Storchaka97852612015-11-01 17:14:27 +0200725 elif options.month is None:
726 result = cal.formatyear(options.year, **optdict)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000727 else:
Serhiy Storchaka97852612015-11-01 17:14:27 +0200728 result = cal.formatmonth(options.year, options.month, **optdict)
Senthil Kumaran962fed92011-08-11 09:22:52 +0800729 write = sys.stdout.write
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000730 if options.encoding:
731 result = result.encode(options.encoding)
Senthil Kumaran962fed92011-08-11 09:22:52 +0800732 write = sys.stdout.buffer.write
733 write(result)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +0000734
735
736if __name__ == "__main__":
737 main(sys.argv)