1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - Calendar recurrence support 4 5 @copyright: 2013 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 8 Supported grammar: 9 10 <recurrence> [ of <recurrence> ]... 11 12 recurrence = <specific-recurrence> | <repeating-recurrence> 13 14 specific-recurrence = ( the <qualifier> <interval> | <month-label> ) 15 [ in <specific-recurrence> ] 16 | <concrete-datetime> 17 18 repeating-recurrence = every [ <qualifier> ] <interval> 19 [ from <specific-recurrence> ] 20 [ until <specific-recurrence> ] 21 22 concrete-datetime = <datetime> | <year> 23 24 Constraints: 25 26 repeating-recurrence: if <qualifier> is not "single": 27 from and/or until must be specified 28 29 from and/or until <specific-recurrence> resolutions 30 must be compatible with the repeating-recurrence 31 """ 32 33 from DateSupport import weekday_labels, weekday_labels_verbose, month_labels, \ 34 getDate, getMonth 35 36 qualifiers = { 37 "2nd" : 2, 38 "third" : 3, 39 "3rd" : 3, 40 "fourth" : 4, 41 "fifth" : 5, 42 # stop at this point, supporting also "nth" and "n" 43 } 44 45 def isQualifier(qualifier, qualifiers): 46 47 """ 48 Return whether the 'qualifier' is one of the given 'qualifiers', returning 49 the level of the qualifier. 50 """ 51 52 if qualifiers.has_key(qualifier): 53 return qualifiers[qualifier] 54 if qualifier.endswith("th") or qualifier.endswith("nd") or qualifier.endswith("st"): 55 qualifier = qualifier[:-2] 56 try: 57 return int(qualifier) 58 except ValueError: 59 return False 60 61 # Specific qualifiers refer to specific entities such as... 62 # "the second Monday of every other month" 63 # -> month 1 (Monday #2), month 3 (Monday #2), month 5 (Monday #2) 64 65 specific_qualifiers = { 66 "first" : 1, 67 "1st" : 1, 68 "second" : 2, # other is not permitted 69 "last" : -1, 70 "final" : -1, 71 } 72 73 specific_qualifiers.update(qualifiers) 74 75 def isSpecificQualifier(qualifier): 76 return isQualifier(qualifier, specific_qualifiers) 77 78 # Repeating qualifiers refer to repeating entities such as... 79 # "every other Monday of every other month" 80 # -> month 1 (Monday #2, #4), month 3 (Monday #2, #4) 81 82 repeating_qualifiers = { 83 "single" : 1, 84 "other" : 2, # second is not permitted (it clashes with the interval) 85 } 86 87 repeating_qualifiers.update(qualifiers) 88 89 def isRepeatingQualifier(qualifier): 90 return isQualifier(qualifier, repeating_qualifiers) 91 92 intervals = { 93 "second" : 1, 94 "minute" : 2, 95 "hour" : 3, 96 "day" : 4, 97 "week" : 5, 98 "month" : 6, 99 "year" : 7 100 } 101 102 # NOTE: Support day and month abbreviations in the input. 103 104 for day in weekday_labels: 105 intervals[day] = intervals["day"] 106 107 for day in weekday_labels_verbose: 108 intervals[day] = intervals["day"] 109 110 for month in month_labels: 111 intervals[month] = intervals["month"] 112 113 # Parsing-related classes. 114 115 class ParseError(Exception): 116 pass 117 118 class VerifyError(Exception): 119 pass 120 121 class ParseIterator: 122 def __init__(self, iterator): 123 self.iterator = iterator 124 self.tokens = [] 125 self.end = False 126 127 def want(self): 128 try: 129 return self._next() 130 except StopIteration: 131 self.end = True 132 return None 133 134 def next(self): 135 try: 136 return self._next() 137 except StopIteration: 138 self.end = True 139 raise ParseError, self.tokens 140 141 def need(self, token): 142 t = self._next() 143 if t != token: 144 raise ParseError, self.tokens 145 146 def have(self, token=None): 147 if self.end: 148 return False 149 t = self.tokens[-1] 150 if token: 151 return t == token 152 else: 153 return t 154 155 def have_one_of(self, tokens): 156 if self.end: 157 return False 158 t = self.tokens[-1] 159 return t in tokens 160 161 def _next(self): 162 t = self.iterator.next() 163 self.tokens.append(t) 164 return t 165 166 class Selector: 167 168 "A selector of datetime occurrences at a particular interval resolution." 169 170 def __init__(self, qualified_by=None): 171 self.recurrence_type = None 172 self.qualifier = None 173 self.qualifier_level = None 174 self.interval = None 175 self.interval_level = None 176 self.qualified_by = qualified_by 177 self.from_datetime = None 178 self.until_datetime = None 179 180 def add_details(self, recurrence_type, qualifier, qualifier_level, interval, interval_level): 181 self.recurrence_type = recurrence_type 182 self.qualifier = qualifier 183 self.qualifier_level = qualifier_level 184 self.interval = interval 185 self.interval_level = interval_level 186 187 def add_datetime(self, interval, interval_level, value): 188 self.recurrence_type = "the" 189 self.qualifier = str(value) 190 self.qualifier_level = value 191 self.interval = interval 192 self.interval_level = interval_level 193 194 def set_from(self, from_datetime): 195 self.from_datetime = from_datetime 196 197 def set_until(self, until_datetime): 198 self.until_datetime = until_datetime 199 200 def __str__(self): 201 return "%s%s%s%s%s%s" % ( 202 self.recurrence_type or "", 203 self.qualifier and " %s" % self.qualifier or "", 204 self.interval and " %s" % self.interval or "", 205 self.from_datetime and " from {%s}" % self.from_datetime or "", 206 self.until_datetime and " until {%s}" % self.until_datetime or "", 207 self.qualified_by and ", selecting %s" % self.qualified_by or "") 208 209 def __repr__(self): 210 return "Selector(%s, %s, %s%s%s%s>" % ( 211 self.recurrence_type, self.qualifier, self.interval, 212 self.from_datetime and ", from_datetime=%r" % self.from_datetime or "", 213 self.until_datetime and ", until_datetime=%r" % self.until_datetime or "", 214 self.qualified_by and ", qualified_by=%r" % self.qualified_by or "") 215 216 def get_resolution(self): 217 if self.qualified_by: 218 return self.qualified_by.get_resolution() 219 else: 220 return self.interval_level 221 222 # Parsing functions. 223 224 def getRecurrence(s): 225 226 "Interpret the given string 's', returning a recurrence description." 227 228 words = ParseIterator(iter([w.strip() for w in s.split()])) 229 230 current = Selector() 231 232 current = parseRecurrence(words, current) 233 parseOptionalLimits(words, current) 234 235 # Obtain qualifications to the recurrence. 236 237 while words.have("of"): 238 current = Selector(current) 239 current = parseRecurrence(words, current) 240 parseOptionalLimits(words, current) 241 242 if not words.end: 243 raise ParseError, words.tokens 244 245 return current 246 247 def parseRecurrence(words, current): 248 249 """ 250 Using the incoming 'words' and given the 'current' selector, parse a 251 recurrence that can be either a repeating recurrence (starting with "every") 252 or a specific recurrence (starting with "the") or a concrete datetime 253 reference. 254 255 The active selector is returned as a result of parsing the recurrence. 256 """ 257 258 words.next() 259 if words.have("every"): 260 parseRepeatingRecurrence(words, current) 261 words.want() 262 return current 263 else: 264 return parseSpecificRecurrence(words, current) 265 266 def parseConcreteDateTime(words, current): 267 268 """ 269 Using the incoming 'words' and given the 'current' selector, parse and 270 return a datetime acting as a specific recurrence. 271 """ 272 273 word = words.have() 274 275 # Detect dates. 276 277 date = getDate(word) 278 if date: 279 current.add_datetime("day", intervals["day"], date) 280 words.want() 281 return current 282 283 # Detect months. 284 285 month = getMonth(word) 286 if month: 287 current.add_datetime("month", intervals["month"], month) 288 words.want() 289 return current 290 291 # Detect years. 292 293 if word.isdigit(): 294 current.add_datetime("year", intervals["year"], int(word)) 295 words.want() 296 return current 297 298 raise ParseError, words.tokens 299 300 def parseSpecificRecurrence(words, current): 301 302 """ 303 Using the incoming 'words' and given the 'current' selector, parse and 304 return a specific recurrence. 305 """ 306 307 word = words.have() 308 309 # Detect month labels. 310 311 if word in month_labels: 312 current.add_details("the", word, month_labels.index(word) + 1, "month", intervals["month"]) 313 words.want() 314 315 # Handle the qualifier and interval. 316 317 elif word == "the": 318 qualifier = words.next() 319 qualifier_level = isSpecificQualifier(qualifier) 320 if not qualifier_level: 321 raise ParseError, words.tokens 322 interval = words.next() 323 interval_level = intervals.get(interval) 324 if not interval_level: 325 raise ParseError, words.tokens 326 327 current.add_details("the", qualifier, qualifier_level, interval, interval_level) 328 329 # Detect the intervals becoming smaller. 330 331 if current.qualified_by and current.interval_level < current.qualified_by.interval_level: 332 raise ParseError, words.tokens 333 334 # Define selectors that are being qualified by the above qualifier and 335 # interval, returning the least specific selector. 336 337 words.want() 338 if words.have("in"): 339 current = Selector(current) 340 words.want() 341 current = parseSpecificRecurrence(words, current) 342 343 # Handle concrete datetime details. 344 345 else: 346 current = parseConcreteDateTime(words, current) 347 348 return current 349 350 def parseRepeatingRecurrence(words, current): 351 352 """ 353 Using the incoming 'words' and given the 'current' selector, parse and 354 return a repeating recurrence. 355 """ 356 357 qualifier = words.next() 358 359 # Handle intervals without qualifiers. 360 361 if intervals.has_key(qualifier): 362 interval = qualifier 363 interval_level = intervals.get(interval) 364 qualifier = "single" 365 qualifier_level = isRepeatingQualifier(qualifier) 366 367 # Handle qualified intervals. 368 369 else: 370 qualifier_level = isRepeatingQualifier(qualifier) 371 if not qualifier_level: 372 raise ParseError, words.tokens 373 interval = words.next() 374 interval_level = intervals.get(interval) 375 if not interval_level: 376 raise ParseError, words.tokens 377 378 current.add_details("every", qualifier, qualifier_level, interval, interval_level) 379 380 # Detect the intervals becoming smaller. 381 382 if current.qualified_by and current.interval_level < current.qualified_by.interval_level: 383 raise ParseError, words.tokens 384 385 def parseOptionalLimits(words, current): 386 387 """ 388 Using the incoming 'words' and given the 'current' selector, parse any 389 optional limits, where these only apply to repeating occurrences. 390 """ 391 392 if current.recurrence_type == "every": 393 parseLimits(words, current) 394 395 def parseLimits(words, current): 396 397 """ 398 Using the incoming 'words', parse any limits applying to the 'current' 399 selector. 400 """ 401 402 from_datetime = until_datetime = None 403 404 if words.have("from"): 405 words.want() 406 from_datetime = Selector() 407 from_datetime = parseSpecificRecurrence(words, from_datetime) 408 409 # Detect the limit interval differing from the selector. 410 411 if current.interval_level != from_datetime.get_resolution(): 412 raise ParseError, words.tokens 413 414 current.set_from(from_datetime) 415 416 if words.have("until"): 417 words.want() 418 until_datetime = Selector() 419 until_datetime = parseSpecificRecurrence(words, until_datetime) 420 421 # Detect the limit interval differing from the selector. 422 423 if current.interval_level != until_datetime.get_resolution(): 424 raise ParseError, words.tokens 425 426 current.set_until(until_datetime) 427 428 # Where the selector refers to a interval repeating at a frequency greater 429 # than one, some limit must be specified to "anchor" the occurrences. 430 431 elif not from_datetime and current.qualifier_level != 1: 432 raise ParseError, words.tokens 433 434 # vim: tabstop=4 expandtab shiftwidth=4