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 314 # Handle the qualifier and interval. 315 316 elif word == "the": 317 qualifier = words.next() 318 qualifier_level = isSpecificQualifier(qualifier) 319 if not qualifier_level: 320 raise ParseError, words.tokens 321 interval = words.next() 322 interval_level = intervals.get(interval) 323 if not interval_level: 324 raise ParseError, words.tokens 325 326 current.add_details("the", qualifier, qualifier_level, interval, interval_level) 327 328 # Detect the intervals becoming smaller. 329 330 if current.qualified_by and current.interval_level < current.qualified_by.interval_level: 331 raise ParseError, words.tokens 332 333 # Define selectors that are being qualified by the above qualifier and 334 # interval, returning the least specific selector. 335 336 words.want() 337 if words.have("in"): 338 current = Selector(current) 339 words.want() 340 current = parseSpecificRecurrence(words, current) 341 342 # Handle concrete datetime details. 343 344 else: 345 current = parseConcreteDateTime(words, current) 346 347 return current 348 349 def parseRepeatingRecurrence(words, current): 350 351 """ 352 Using the incoming 'words' and given the 'current' selector, parse and 353 return a repeating recurrence. 354 """ 355 356 qualifier = words.next() 357 358 # Handle intervals without qualifiers. 359 360 if intervals.has_key(qualifier): 361 interval = qualifier 362 interval_level = intervals.get(interval) 363 qualifier = "single" 364 qualifier_level = isRepeatingQualifier(qualifier) 365 366 # Handle qualified intervals. 367 368 else: 369 qualifier_level = isRepeatingQualifier(qualifier) 370 if not qualifier_level: 371 raise ParseError, words.tokens 372 interval = words.next() 373 interval_level = intervals.get(interval) 374 if not interval_level: 375 raise ParseError, words.tokens 376 377 current.add_details("every", qualifier, qualifier_level, interval, interval_level) 378 379 # Detect the intervals becoming smaller. 380 381 if current.qualified_by and current.interval_level < current.qualified_by.interval_level: 382 raise ParseError, words.tokens 383 384 def parseOptionalLimits(words, current): 385 386 """ 387 Using the incoming 'words' and given the 'current' selector, parse any 388 optional limits, where these only apply to repeating occurrences. 389 """ 390 391 if current.recurrence_type == "every": 392 parseLimits(words, current) 393 394 def parseLimits(words, current): 395 396 """ 397 Using the incoming 'words', parse any limits applying to the 'current' 398 selector. 399 """ 400 401 from_datetime = until_datetime = None 402 403 if words.have("from"): 404 words.want() 405 from_datetime = Selector() 406 from_datetime = parseSpecificRecurrence(words, from_datetime) 407 408 # Detect the limit interval differing from the selector. 409 410 if current.interval_level != from_datetime.get_resolution(): 411 raise ParseError, words.tokens 412 413 current.set_from(from_datetime) 414 415 if words.have("until"): 416 words.want() 417 until_datetime = Selector() 418 until_datetime = parseSpecificRecurrence(words, until_datetime) 419 420 # Detect the limit interval differing from the selector. 421 422 if current.interval_level != until_datetime.get_resolution(): 423 raise ParseError, words.tokens 424 425 current.set_until(until_datetime) 426 427 # Where the selector refers to a interval repeating at a frequency greater 428 # than one, some limit must be specified to "anchor" the occurrences. 429 430 elif not from_datetime and current.qualifier_level != 1: 431 raise ParseError, words.tokens 432 433 # vim: tabstop=4 expandtab shiftwidth=4