Package lib :: Module holiday
[hide private]
[frames] | no frames]

Source Code for Module lib.holiday

  1  # -*- coding: utf-8 -*- 
  2   
  3  #    callirhoe - high quality calendar rendering 
  4  #    Copyright (C) 2012-2013 George M. Tzoumas 
  5   
  6  #    This program is free software: you can redistribute it and/or modify 
  7  #    it under the terms of the GNU General Public License as published by 
  8  #    the Free Software Foundation, either version 3 of the License, or 
  9  #    (at your option) any later version. 
 10  # 
 11  #    This program is distributed in the hope that it will be useful, 
 12  #    but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  #    GNU General Public License for more details. 
 15  # 
 16  #    You should have received a copy of the GNU General Public License 
 17  #    along with this program.  If not, see http://www.gnu.org/licenses/ 
 18   
 19  # ***************************************** 
 20  #                                         # 
 21  """       holiday support routines       """ 
 22  #                                         # 
 23  # ***************************************** 
 24   
 25  from datetime import date, timedelta 
 26   
27 -def _get_orthodox_easter(year):
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 # res = date(year, 5, r - 30) if r > 30 else date(year, 4, r) 39 # return res 40
41 -def _get_catholic_easter(year):
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
57 -def _strip_empty(sl):
58 """strip empty strings from list I{sl} 59 60 @rtype: [str,...] 61 """ 62 return filter(lambda z: z, sl) if sl else []
63
64 -def _flatten(sl):
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
72 -class Holiday(object):
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):
96 self.header_list = _strip_empty(header) 97 self.footer_list = _strip_empty(footer) 98 self.flags = self._parse_flags(flags_str)
99
100 - def merge_with(self, hol_list):
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
107 - def header(self):
108 """return a comma-separated string for L{header_list} 109 110 @rtype: str 111 """ 112 return _flatten(self.header_list)
113
114 - def footer(self):
115 """return a comma-separated string for L{footer_list} 116 117 @rtype: str 118 """ 119 return _flatten(self.footer_list)
120
121 - def __str__(self):
122 """string representation for debugging purposes 123 124 @rtype: str 125 """ 126 return str(self.footer()) + ':' + str(self.header()) + ':' + str(self.flags)
127
128 - def _parse_flags(self, fstr):
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 # allow for prefix abbrev. 140 elif 'reminder'.startswith(s): val |= Holiday.REMINDER 141 return val
142
143 -def _decode_date_str(ddef):
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
167 -class HolidayProvider(object):
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() # key = (d,m) 199 self.monthly = dict() # key = d 200 self.fixed = dict() # key = date() 201 self.orth_easter = dict() # key = daysdelta 202 self.george = [] # key = n/a 203 self.cath_easter = dict() # key = daysdelta 204 self.cache = dict() # key = date() 205 self.ycache = set() # key = year 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
214 - def _parse_day_record(self, fields):
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
262 - def _multi_holiday_tuple(self, header, footer, flags):
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
287 - def load_holiday_file(self, filename):
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: # annual event 333 if (d,m) not in self.annual: self.annual[(d,m)] = [] 334 self.annual[(d,m)].append(hol) 335 else: # monthly event 336 if d not in self.monthly: self.monthly[d] = [] 337 self.monthly[d].append(hol) 338 else: # fixed date event 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 # properly annotate multi-day events 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
368 - def get_holiday(self, y, m, d):
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 # fill-in events for year y 377 # annual 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 # monthly 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 # fixed 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 # orthodox easter 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 # Georgios day 399 if self.george: 400 dt = date(y,4,23) 401 if edt >= dt: dt = edt + timedelta(1) # >= or > ?? 402 if not dt in self.cache: self.cache[dt] = Holiday() 403 self.cache[dt].merge_with(self.george) 404 # catholic easter 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
416 - def get_style(self, flags, dow):
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