1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """ holiday support routines """
22
23
24
25 from datetime import date, timedelta
26
28 """compute date of orthodox easter
29 @rtype: datetime.date
30 """
31 y1, y2, y3 = year % 4 , year % 7, year % 19
32 a = 19*y3 + 15
33 y4 = a % 30
34 b = 2*y1 + 4*y2 + 6*(y4 + 1)
35 y5 = b % 7
36 r = 1 + 3 + y4 + y5
37 return date(year, 3, 31) + timedelta(r)
38
39
40
42 """compute date of catholic easter
43
44 @rtype: datetime.date
45 """
46 a, b, c = year % 19, year // 100, year % 100
47 d, e = divmod(b,4)
48 f = (b + 8) // 25
49 g = (b - f + 1) // 3
50 h = (19*a + b - d - g + 15) % 30
51 i, k = divmod(c,4)
52 l = (32 + 2*e + 2*i - h - k) % 7
53 m = (a + 11*h + 22*l) // 451
54 emonth,edate = divmod(h + l - 7*m + 114,31)
55 return date(year, emonth, edate+1)
56
58 """strip empty strings from list I{sl}
59
60 @rtype: [str,...]
61 """
62 return filter(lambda z: z, sl) if sl else []
63
65 """join list I{sl} into a comma-separated string
66
67 @rtype: str
68 """
69 if not sl: return None
70 return ', '.join(sl)
71
73 """class holding a Holiday object (date is I{not} stored, use L{HolidayProvider} for that)
74
75 @ivar header_list: string list for header (primary text)
76 @ivar footer_list: string list for footer (secondary text)
77 @ivar flags: bit combination of {OFF=1, MULTI=2, REMINDER=4}
78
79 I{OFF}: day off (real holiday)
80
81 I{MULTI}: multi-day event (used to mark long day ranges,
82 not necessarily holidays)
83
84 I{REMINDER}: do not mark the day as holiday
85
86 @note: Rendering style is determined considering L{flags} in this order:
87 1. OFF
88 2. MULTI
89
90 First flag that matches determines the style.
91 """
92 OFF = 1
93 MULTI = 2
94 REMINDER = 4
95 - def __init__(self, header = [], footer = [], flags_str = None):
99
101 """merge a list of holiday objects into this object"""
102 for hol in hol_list:
103 self.header_list.extend(hol.header_list)
104 self.footer_list.extend(hol.footer_list)
105 self.flags |= hol.flags
106
108 """return a comma-separated string for L{header_list}
109
110 @rtype: str
111 """
112 return _flatten(self.header_list)
113
120
122 """string representation for debugging purposes
123
124 @rtype: str
125 """
126 return str(self.footer()) + ':' + str(self.header()) + ':' + str(self.flags)
127
129 """return a bit combination of flags, from a comma-separated string list
130
131 @rtype: int
132 """
133 if not fstr: return 0
134 fs = fstr.split(',')
135 val = 0
136 for s in fs:
137 if s == 'off': val |= Holiday.OFF
138 elif s == 'multi': val |= Holiday.MULTI
139
140 elif 'reminder'.startswith(s): val |= Holiday.REMINDER
141 return val
142
144 """decode a date definition string into a I{(year,month,day)} tuple
145
146 @param ddef: date definition string of length 2, 4 or 8
147
148 If C{ddef} is of the form "DD" then tuple (0,0,DD) is returned, which
149 stands for any year - any month - day DD.
150
151 If C{ddef} is of the form "MMDD" then tuple (0,MM,DD) is returned, which
152 stands for any year - month MM - day DD.
153
154 If C{ddef} is of the form "YYYYMMDD" then tuple (YYYY,MM,DD) is returned, which
155 stands for year YYYY - month MM - day DD.
156
157 @rtype: (int,int,int)
158 """
159 if len(ddef) == 2:
160 return (0,0,int(ddef))
161 if len(ddef) == 4:
162 return (0,int(ddef[:2]),int(ddef[-2:]))
163 if len(ddef) == 8:
164 return (int(ddef[:4]),int(ddef[4:6]),int(ddef[-2:]))
165 raise ValueError("invalid date definition '%s'" % ddef)
166
168 """class holding the holidays throught the year(s)
169
170 @ivar annual: dict of events occuring annually, indexed by tuple I{(day,month)}. Note
171 each dict entry is actually a list of L{Holiday} objects. This is also true for the other
172 instance variables: L{monthly}, L{fixed}, L{orth_easter}, L{george}, L{cath_easter}.
173 @ivar monthly: event occuring monthly, indexed by int I{day}
174 @ivar fixed: fixed date events, indexed by a C{date()} object
175 @ivar orth_easter: dict of events relative to the orthodox easter Sunday, indexed by
176 an integer days offset
177 @ivar george: events occuring on St George's day (orthodox calendar special computation)
178 @ivar cath_easter: dict of events relative to the catholic easter Sunday, indexed by
179 an integer days offset
180 @ivar cache: for each year requested, all holidays occuring
181 within that year (annual, monthly, easter-based etc.) are precomputed and stored into
182 dict C{cache}, indexed by a C{date()} object
183 @ivar ycache: set holding cached years; each new year requested, triggers a cache-fill
184 operation
185 """
186 - def __init__(self, s_normal, s_weekend, s_holiday, s_weekend_holiday, s_multi, s_weekend_multi, multiday_markers=True):
187 """initialize a C{HolidayProvider} object
188
189 @param s_normal: style class object for normal (weekday) day cells
190 @param s_weekend: style for weekend day cells
191 @param s_holiday: style for holiday day cells
192 @param s_weekend_holiday: style for holiday cells on weekends
193 @param s_multi: style for multi-day holiday weekday cells
194 @param s_weekend_multi: style for multi-day holiday weekend cells
195 @param multiday_markers: if C{True}, then use end-of-multiday-holiday markers and range markers (with dots),
196 otherwise only first day and first-day-of-month are marked
197 """
198 self.annual = dict()
199 self.monthly = dict()
200 self.fixed = dict()
201 self.orth_easter = dict()
202 self.george = []
203 self.cath_easter = dict()
204 self.cache = dict()
205 self.ycache = set()
206 self.s_normal = s_normal
207 self.s_weekend = s_weekend
208 self.s_holiday = s_holiday
209 self.s_weekend_holiday = s_weekend_holiday
210 self.s_multi = s_multi
211 self.s_weekend_multi = s_weekend_multi
212 self.multiday_markers = multiday_markers
213
215 """return tuple (etype,ddef,footer,header,flags)
216
217 @rtype: (char,type(ddef),str,str,int)
218 @note: I{ddef} is one of the following:
219 - None
220 - int
221 - ((y,m,d),)
222 - ((y,m,d),(y,m,d))
223 """
224 if len(fields) != 5:
225 raise ValueError("Too many fields: " + str(fields))
226 for i in range(len(fields)):
227 if len(fields[i]) == 0: fields[i] = None
228 if fields[0] == 'd':
229 if fields[1]:
230 if '*' in fields[1]:
231 if fields[0] != 'd':
232 raise ValueError("multi-day events not allowed with event type '%s'" % fields[0])
233 dstr,spanstr = fields[1].split('*')
234 if len(dstr) != 8:
235 raise ValueError("multi-day events allowed only with full date, not '%s'" % dstr)
236 span = int(spanstr)
237 y,m,d = _decode_date_str(dstr)
238 dt1 = date(y,m,d)
239 dt2 = dt1 + timedelta(span-1)
240 res = ((y,m,d),(dt2.year,dt2.month,dt2.day))
241 elif '-' in fields[1]:
242 if fields[0] != 'd':
243 raise ValueError("multi-day events not allowed with event type '%s'" % fields[0])
244 dstr,dstr2 = fields[1].split('-')
245 if len(dstr) != 8:
246 raise ValueError("multi-day events allowed only with full date, not '%s'" % dstr)
247 y,m,d = _decode_date_str(dstr)
248 y2,m2,d2 = _decode_date_str(dstr2)
249 res = ((y,m,d),(y2,m2,d2))
250 else:
251 y,m,d = _decode_date_str(fields[1])
252 if len(fields[1]) == 8:
253 res = ((y,m,d),(y,m,d))
254 else:
255 res = ((y,m,d),)
256 else:
257 res = None
258 else:
259 res = int(fields[1])
260 return (fields[0],res,fields[2],fields[3],fields[4])
261
263 """returns a 4-tuple of L{Holiday} objects representing (beginning, end, first-day-of-month, rest)
264
265 @param header: passed as C{[header]} of the generated L{Holiday} object
266 @param footer: passed as C{[footer]} of the generated L{Holiday} object
267 @param flags: C{flags} of the generated L{Holiday} object
268 @rtype: (Holiday,Holiday,Holiday,Holiday)
269 """
270 if header:
271 if self.multiday_markers:
272 header_tuple = (header+'..', '..'+header, '..'+header+'..', None)
273 else:
274 header_tuple = (header, None, header, None)
275 else:
276 header_tuple = (None, None, None, None)
277 if footer:
278 if self.multiday_markers:
279 footer_tuple = (footer+'..', '..'+footer, '..'+footer+'..', None)
280 else:
281 footer_tuple = (footer, None, footer, None)
282 else:
283 footer_tuple = (None, None, None, None)
284 return tuple(map(lambda k: Holiday([header_tuple[k]], [footer_tuple[k]], flags),
285 range(4)))
286
288 """load a holiday file into the C{HolidayProvider} object
289
290 B{File Format:}
291 - C{type|date*span|footer|header|flags}
292 - C{type|date1-date2|footer|header|flags}
293 - C{type|date|footer|header|flags}
294
295 I{type:}
296 - C{d}: event occurs annually fixed day/month; I{date}=MMDD
297 - C{d}: event occurs monthly, fixed day; I{date}=DD
298 - C{d}: fixed day/month/year combination (e.g. deadline, trip, etc.); I{date}=YYYYMMDD
299 - C{oe}: Orthodox Easter-dependent holiday, annually; I{date}=integer offset in days
300 - C{ge}: Georgios' name day, Orthodox Easter dependent holiday, annually; I{date} field is ignored
301 - C{ce}: Catholic Easter holiday; I{date}=integer offset in days
302
303 I{date*span} and range I{date1-date2} supported only for I{date}=YYYYMMDD (fixed) events
304
305 I{flags:} comma-separated list of the following:
306 1. off
307 2. multi
308 3. reminder (or any prefix of it)
309
310 B{Example}::
311
312 d|0101||New year's|off
313 d|0501||Labour day|off
314 ce|-2||Good Friday|
315 ce|0||Easter|off
316 ce|1||Easter Monday|off
317 d|20130223-20130310|winter vacations (B)||multi
318
319 @param filename: file to be loaded
320 """
321 with open(filename, 'r') as f:
322 for line in f:
323 line = line.strip()
324 if not line: continue
325 if line[0] == '#': continue
326 fields = line.split('|')
327 etype,ddef,footer,header,flags = self._parse_day_record(fields)
328 hol = Holiday([header], [footer], flags)
329 if etype == 'd':
330 if len(ddef) == 1:
331 y,m,d = ddef[0]
332 if m > 0:
333 if (d,m) not in self.annual: self.annual[(d,m)] = []
334 self.annual[(d,m)].append(hol)
335 else:
336 if d not in self.monthly: self.monthly[d] = []
337 self.monthly[d].append(hol)
338 else:
339 dt1,dt2 = date(*ddef[0]),date(*ddef[1])
340 span = (dt2-dt1).days + 1
341 if span == 1:
342 if dt1 not in self.fixed: self.fixed[dt1] = []
343 self.fixed[dt1].append(hol)
344 else:
345
346 hols = self._multi_holiday_tuple(header, footer, flags)
347 dt = dt1
348 while dt <= dt2:
349 if dt not in self.fixed: self.fixed[dt] = []
350 if dt == dt1: hol = hols[0]
351 elif dt == dt2: hol = hols[1]
352 elif dt.day == 1: hol = hols[2]
353 else: hol = hols[3]
354 self.fixed[dt].append(hol)
355 dt += timedelta(1)
356
357 elif etype == 'oe':
358 d = ddef
359 if d not in self.orth_easter: self.orth_easter[d] = []
360 self.orth_easter[d].append(hol)
361 elif etype == 'ge':
362 self.george.append(hol)
363 elif etype == 'ce':
364 d = ddef
365 if d not in self.cath_easter: self.cath_easter[d] = []
366 self.cath_easter[d].append(hol)
367
369 """return a L{Holiday} object for the specified date (y,m,d) or C{None} if no holiday is defined
370
371 @rtype: Holiday
372 @note: If year I{y} has not been requested before, the cache is updated first
373 with all holidays that belong in I{y}, indexed by C{date()} objects.
374 """
375 if y not in self.ycache:
376
377
378 for d0,m0 in self.annual:
379 dt = date(y,m0,d0)
380 if not dt in self.cache: self.cache[dt] = Holiday()
381 self.cache[dt].merge_with(self.annual[(d0,m0)])
382
383 for d0 in self.monthly:
384 for m0 in range(1,13):
385 dt = date(y,m0,d0)
386 if not dt in self.cache: self.cache[dt] = Holiday()
387 self.cache[dt].merge_with(self.monthly[m0])
388
389 for dt in filter(lambda z: z.year == y, self.fixed):
390 if not dt in self.cache: self.cache[dt] = Holiday()
391 self.cache[dt].merge_with(self.fixed[dt])
392
393 edt = _get_orthodox_easter(y)
394 for delta in self.orth_easter:
395 dt = edt + timedelta(delta)
396 if not dt in self.cache: self.cache[dt] = Holiday()
397 self.cache[dt].merge_with(self.orth_easter[delta])
398
399 if self.george:
400 dt = date(y,4,23)
401 if edt >= dt: dt = edt + timedelta(1)
402 if not dt in self.cache: self.cache[dt] = Holiday()
403 self.cache[dt].merge_with(self.george)
404
405 edt = _get_catholic_easter(y)
406 for delta in self.cath_easter:
407 dt = edt + timedelta(delta)
408 if not dt in self.cache: self.cache[dt] = Holiday()
409 self.cache[dt].merge_with(self.cath_easter[delta])
410
411 self.ycache.add(y)
412
413 dt = date(y,m,d)
414 return self.cache[dt] if dt in self.cache else None
415
417 """return appropriate style object, depending on I{flags} and I{dow}
418
419 @rtype: Style
420 @param flags: bit combination of holiday flags
421 @param dow: day of week
422 """
423 if flags & Holiday.OFF:
424 return self.s_weekend_holiday if dow >= 5 else self.s_holiday
425 if flags & Holiday.MULTI:
426 return self.s_weekend_multi if dow >= 5 else self.s_multi
427 return self.s_weekend if dow >= 5 else self.s_normal
428
429 - def __call__(self, year, month, dom, dow):
430 """returns (header,footer,day_style)
431
432 @rtype: (str,str,Style)
433 @param month: month (0-12)
434 @param dom: day of month (1-31)
435 @param dow: day of week (0-6)
436 """
437 hol = self.get_holiday(year,month,dom)
438 if hol:
439 return (hol.header(),hol.footer(),self.get_style(hol.flags,dow))
440 else:
441 return (None,None,self.get_style(0,dow))
442
443 if __name__ == '__main__':
444 import sys
445 hp = HolidayProvider('n', 'w', 'h', 'wh', 'm', 'wm')
446 if len(sys.argv) < 3:
447 raise SystemExit("Usage: %s YEAR holiday_file ..." % sys.argv[0]);
448 y = int(sys.argv[1])
449 for f in sys.argv[2:]:
450 hp.load_holiday_file(f)
451 if y == 0: y = date.today().year
452 cur = date(y,1,1)
453 d2 = date(y,12,31)
454 while cur <= d2:
455 y,m,d = cur.year, cur.month, cur.day
456 hol = hp.get_holiday(y,m,d)
457 if hol: print cur.strftime("%a %b %d %Y"),hol
458 cur += timedelta(1)
459