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> ) | <datetime> | <month> | <year> ) 15 [ in <specific-recurrence> ] 16 17 repeating-recurrence = every [ <qualifier> ] <interval> 18 [ from <specific-recurrence> ] 19 [ until <specific-recurrence> ] 20 21 Constraints: 22 23 repeating-recurrence: if <qualifier> is not "single": 24 from and/or until must be specified 25 """ 26 27 from DateSupport import weekday_labels, month_labels 28 29 qualifiers = { 30 "2nd" : 2, 31 "third" : 3, 32 "3rd" : 3, 33 "fourth" : 4, 34 "fifth" : 5, 35 # stop at this point, supporting also "nth" and "n" 36 } 37 38 def isQualifier(qualifier, qualifiers): 39 40 """ 41 Return whether the 'qualifier' is one of the given 'qualifiers', returning 42 the level of the qualifier. 43 """ 44 45 if qualifiers.has_key(qualifier): 46 return qualifiers[qualifier] 47 if qualifier.endswith("th") or qualifier.endswith("nd") or qualifier.endswith("st"): 48 qualifier = qualifier[:-2] 49 try: 50 return int(qualifier) 51 except ValueError: 52 return False 53 54 # Specific qualifiers refer to specific entities such as... 55 # "the second Monday of every other month" 56 # -> month 1 (Monday #2), month 3 (Monday #2), month 5 (Monday #2) 57 58 specific_qualifiers = { 59 "first" : 1, 60 "1st" : 1, 61 "second" : 2, # other is not permitted 62 "last" : -1, 63 "final" : -1, 64 } 65 66 specific_qualifiers.update(qualifiers) 67 68 def isSpecificQualifier(qualifier): 69 return isQualifier(qualifier, specific_qualifiers) 70 71 # Repeating qualifiers refer to repeating entities such as... 72 # "every other Monday of every other month" 73 # -> month 1 (Monday #2, #4), month 3 (Monday #2, #4) 74 75 repeating_qualifiers = { 76 "single" : 1, 77 "other" : 2, # second is not permitted (it clashes with the interval) 78 } 79 80 repeating_qualifiers.update(qualifiers) 81 82 def isRepeatingQualifier(qualifier): 83 return isQualifier(qualifier, repeating_qualifiers) 84 85 intervals = { 86 "second" : 1, 87 "minute" : 2, 88 "hour" : 3, 89 "day" : 4, 90 "week" : 5, 91 "month" : 6, 92 "year" : 7 93 } 94 95 # NOTE: Support day and month abbreviations in the input. 96 97 for day in weekday_labels: 98 intervals[day] = intervals["day"] 99 100 for month in month_labels: 101 intervals[month] = intervals["month"] 102 103 # Parsing-related classes. 104 105 class ParseError(Exception): 106 pass 107 108 class VerifyError(Exception): 109 pass 110 111 class ParseIterator: 112 def __init__(self, iterator): 113 self.iterator = iterator 114 self.tokens = [] 115 self.end = False 116 117 def want(self): 118 try: 119 return self._next() 120 except StopIteration: 121 self.end = True 122 return None 123 124 def next(self): 125 try: 126 return self._next() 127 except StopIteration: 128 self.end = True 129 raise ParseError, self.tokens 130 131 def need(self, token): 132 t = self._next() 133 if t != token: 134 raise ParseError, self.tokens 135 136 def have(self, token): 137 if self.end: 138 return False 139 t = self.tokens[-1] 140 return t == token 141 142 def _next(self): 143 t = self.iterator.next() 144 self.tokens.append(t) 145 return t 146 147 class Selector: 148 def __init__(self, qualified_by=None): 149 self.recurrence_type = None 150 self.qualifier = None 151 self.qualifier_level = None 152 self.interval = None 153 self.interval_level = None 154 self.qualified_by = qualified_by 155 self.from_datetime = None 156 self.until_datetime = None 157 158 def add_details(self, recurrence_type, qualifier, qualifier_level, interval, interval_level): 159 self.recurrence_type = recurrence_type 160 self.qualifier = qualifier 161 self.qualifier_level = qualifier_level 162 self.interval = interval 163 self.interval_level = interval_level 164 165 def set_from(self, from_datetime): 166 self.from_datetime = from_datetime 167 168 def set_until(self, until_datetime): 169 self.until_datetime = until_datetime 170 171 def __str__(self): 172 return "%s %s %s%s%s%s" % ( 173 self.recurrence_type, self.qualifier, self.interval, 174 self.from_datetime and " from {%s}" % self.from_datetime or "", 175 self.until_datetime and " until {%s}" % self.until_datetime or "", 176 self.qualified_by and ", selecting %s" % self.qualified_by or "") 177 178 def __repr__(self): 179 return "Selector(%s, %s, %s%s%s%s>" % ( 180 self.recurrence_type, self.qualifier, self.interval, 181 self.from_datetime and ", from_datetime=%r" % self.from_datetime or "", 182 self.until_datetime and ", until_datetime=%r" % self.until_datetime or "", 183 self.qualified_by and ", qualified_by=%r" % self.qualified_by or "") 184 185 # Parsing functions. 186 187 def getRecurrence(s): 188 189 "Interpret the given string 's', returning a recurrence description." 190 191 words = ParseIterator(iter([w.strip() for w in s.split()])) 192 193 current = Selector() 194 195 current = parseRecurrence(words, current) 196 parseOptionalLimits(words, current) 197 198 # Obtain qualifications to the recurrence. 199 200 while words.have("of"): 201 current = Selector(current) 202 current = parseRecurrence(words, current) 203 parseOptionalLimits(words, current) 204 205 if not words.end: 206 raise ParseError, words.tokens 207 208 return current 209 210 def parseRecurrence(words, current): 211 212 """ 213 Using the incoming 'words' and given the 'current' selector, parse a 214 recurrence that can be either a repeating recurrence (starting with "every") 215 or a specific recurrence (starting with "the"). 216 217 The active selector is returned as a result of parsing the recurrence. 218 """ 219 220 words.next() 221 if words.have("every"): 222 parseRepeatingRecurrence(words, current) 223 words.want() 224 return current 225 elif words.have("the"): 226 return parseSpecificRecurrence(words, current) 227 else: 228 raise ParseError, words.tokens 229 230 def parseSpecificRecurrence(words, current): 231 232 """ 233 Using the incoming 'words' and given the 'current' selector, parse and 234 return a specific recurrence. 235 """ 236 237 # Handle the qualifier and interval. 238 239 qualifier = words.next() 240 qualifier_level = isSpecificQualifier(qualifier) 241 if not qualifier_level: 242 raise ParseError, words.tokens 243 interval = words.next() 244 interval_level = intervals.get(interval) 245 if not interval_level: 246 raise ParseError, words.tokens 247 248 current.add_details("the", qualifier, qualifier_level, interval, interval_level) 249 250 # Detect the intervals becoming smaller. 251 252 if current.qualified_by and current.interval_level < current.qualified_by.interval_level: 253 raise ParseError, words.tokens 254 255 # Define selectors that are being qualified by the above qualifier and 256 # interval, returning the least specific selector. 257 258 words.want() 259 if words.have("in"): 260 current = Selector(current) 261 words.need("the") 262 current = parseSpecificRecurrence(words, current) 263 264 return current 265 266 def parseRepeatingRecurrence(words, current): 267 268 """ 269 Using the incoming 'words' and given the 'current' selector, parse and 270 return a repeating recurrence. 271 """ 272 273 qualifier = words.next() 274 if intervals.has_key(qualifier): 275 interval = qualifier 276 interval_level = intervals.get(interval) 277 qualifier = "single" 278 qualifier_level = isRepeatingQualifier(qualifier) 279 else: 280 qualifier_level = isRepeatingQualifier(qualifier) 281 if not qualifier_level: 282 raise ParseError, words.tokens 283 interval = words.next() 284 interval_level = intervals.get(interval) 285 if not interval_level: 286 raise ParseError, words.tokens 287 288 current.add_details("every", qualifier, qualifier_level, interval, interval_level) 289 290 # Detect the intervals becoming smaller. 291 292 if current.qualified_by and current.interval_level < current.qualified_by.interval_level: 293 raise ParseError, words.tokens 294 295 def parseOptionalLimits(words, current): 296 297 """ 298 Using the incoming 'words' and given the 'current' selector, parse any 299 optional limits, where these only apply to repeating occurrences. 300 """ 301 302 if current.recurrence_type == "every": 303 parseLimits(words, current) 304 305 def parseLimits(words, current): 306 307 """ 308 Using the incoming 'words', parse any limits applying to the 'current' 309 selector. 310 """ 311 312 from_datetime = until_datetime = None 313 314 if words.have("from"): 315 words.need("the") 316 from_datetime = Selector() 317 from_datetime = parseSpecificRecurrence(words, from_datetime) 318 current.set_from(from_datetime) 319 320 if words.have("until"): 321 words.need("the") 322 until_datetime = Selector() 323 until_datetime = parseSpecificRecurrence(words, until_datetime) 324 current.set_until(until_datetime) 325 326 # Where the selector refers to a interval repeating at a frequency greater 327 # than one, some limit must be specified to "anchor" the occurrences. 328 329 elif not from_datetime and current.qualifier_level != 1: 330 raise ParseError, words.tokens 331 332 # vim: tabstop=4 expandtab shiftwidth=4