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 22 from DateSupport import weekday_labels, month_labels 23 24 qualifiers = { 25 "2nd" : 2, 26 "third" : 3, 27 "3rd" : 3, 28 "fourth" : 4, 29 "fifth" : 5, 30 # stop at this point, supporting also "nth" and "n" 31 } 32 33 def isQualifier(qualifier, qualifiers): 34 if qualifiers.has_key(qualifier): 35 return qualifiers[qualifier] 36 if qualifier.endswith("th") or qualifier.endswith("nd") or qualifier.endswith("st"): 37 qualifier = qualifier[:-2] 38 try: 39 return int(qualifier) 40 except ValueError: 41 return False 42 43 # Specific qualifiers refer to specific entities such as... 44 # "the second Monday of every other month" 45 # -> month 1 (Monday #2), month 3 (Monday #2), month 5 (Monday #2) 46 47 specific_qualifiers = { 48 "first" : 1, 49 "1st" : 1, 50 "second" : 2, # other is not permitted 51 "last" : -1, 52 "final" : -1, 53 } 54 55 specific_qualifiers.update(qualifiers) 56 57 def isSpecificQualifier(qualifier): 58 return isQualifier(qualifier, specific_qualifiers) 59 60 # Repeating qualifiers refer to repeating entities such as... 61 # "every other Monday of every other month" 62 # -> month 1 (Monday #2, #4), month 3 (Monday #2, #4) 63 64 repeating_qualifiers = { 65 "single" : 1, 66 "other" : 2, # second is not permitted (it clashes with the interval) 67 } 68 69 repeating_qualifiers.update(qualifiers) 70 71 def isRepeatingQualifier(qualifier): 72 return isQualifier(qualifier, repeating_qualifiers) 73 74 intervals = { 75 "second" : 1, 76 "minute" : 2, 77 "hour" : 3, 78 "day" : 4, 79 "week" : 5, 80 "month" : 6, 81 "year" : 7 82 } 83 84 # NOTE: Support day and month abbreviations in the input. 85 86 for day in weekday_labels: 87 intervals[day] = intervals["day"] 88 89 for month in month_labels: 90 intervals[month] = intervals["month"] 91 92 # Parsing-related classes. 93 94 class ParseError(Exception): 95 pass 96 97 class ParseIterator: 98 def __init__(self, iterator): 99 self.iterator = iterator 100 self.tokens = [] 101 self.end = False 102 103 def want(self): 104 try: 105 return self._next() 106 except StopIteration: 107 self.end = True 108 return None 109 110 def next(self): 111 try: 112 return self._next() 113 except StopIteration: 114 self.end = True 115 raise ParseError, self.tokens 116 117 def need(self, token): 118 t = self._next() 119 if t != token: 120 raise ParseError, self.tokens 121 122 def have(self, token): 123 if self.end: 124 return False 125 t = self.tokens[-1] 126 return t == token 127 128 def _next(self): 129 t = self.iterator.next() 130 self.tokens.append(t) 131 return t 132 133 class Selector: 134 def __init__(self, qualified_by=None): 135 self.recurrence_type = None 136 self.qualifier = None 137 self.qualifier_level = None 138 self.interval = None 139 self.interval_level = None 140 self.qualified_by = qualified_by 141 self.from_datetime = None 142 self.until_datetime = None 143 144 def add_details(self, recurrence_type, qualifier, qualifier_level, interval, interval_level): 145 self.recurrence_type = recurrence_type 146 self.qualifier = qualifier 147 self.qualifier_level = qualifier_level 148 self.interval = interval 149 self.interval_level = interval_level 150 151 def set_from(self, from_datetime): 152 self.from_datetime = from_datetime 153 154 def set_until(self, until_datetime): 155 self.until_datetime = until_datetime 156 157 def __str__(self): 158 return "%s %s %s%s%s%s" % ( 159 self.recurrence_type, self.qualifier, self.interval, 160 self.from_datetime and " from {%s}" % self.from_datetime or "", 161 self.until_datetime and " until {%s}" % self.until_datetime or "", 162 self.qualified_by and ", in %s" % self.qualified_by or "") 163 164 def __repr__(self): 165 return "Selector(%s, %s, %s%s%s%s>" % ( 166 self.recurrence_type, self.qualifier, self.interval, 167 self.from_datetime and ", from_datetime=%r" % self.from_datetime or "", 168 self.until_datetime and ", until_datetime=%r" % self.until_datetime or "", 169 self.qualified_by and ", qualified_by=%r" % self.qualified_by or "") 170 171 # Parsing functions. 172 173 def getRecurrence(s): 174 175 "Interpret the given string 's', returning a recurrence description." 176 177 words = ParseIterator(iter([w.strip() for w in s.split()])) 178 179 current = Selector() 180 181 current = parseRecurrence(words, current) 182 parseOptionalLimits(words, current) 183 184 # Obtain qualifications to the recurrence. 185 186 while words.have("of"): 187 current = Selector(current) 188 current = parseRecurrence(words, current) 189 parseOptionalLimits(words, current) 190 191 if not words.end: 192 raise ParseError, words.tokens 193 194 return current 195 196 def parseRecurrence(words, current): 197 words.next() 198 if words.have("every"): 199 parseRepeatingRecurrence(words, current) 200 words.want() 201 return current 202 elif words.have("the"): 203 return parseSpecificRecurrence(words, current) 204 else: 205 raise ParseError, words.tokens 206 207 def parseSpecificRecurrence(words, current): 208 qualifier = words.next() 209 qualifier_level = isSpecificQualifier(qualifier) 210 if not qualifier_level: 211 raise ParseError, words.tokens 212 interval = words.next() 213 interval_level = intervals.has_key(interval) 214 if not interval_level: 215 raise ParseError, words.tokens 216 217 current.add_details("the", qualifier, qualifier_level, interval, interval_level) 218 219 words.want() 220 if words.have("in"): 221 current = Selector(current) 222 words.need("the") 223 parseSpecificRecurrence(words, current) 224 225 return current 226 227 def parseRepeatingRecurrence(words, current): 228 qualifier = words.next() 229 if intervals.has_key(qualifier): 230 interval = qualifier 231 interval_level = intervals.has_key(interval) 232 qualifier = "single" 233 qualifier_level = isRepeatingQualifier(qualifier) 234 else: 235 qualifier_level = isRepeatingQualifier(qualifier) 236 if not qualifier_level: 237 raise ParseError, words.tokens 238 interval = words.next() 239 interval_level = intervals.has_key(interval) 240 if not interval_level: 241 raise ParseError, words.tokens 242 243 current.add_details("every", qualifier, qualifier_level, interval, interval_level) 244 245 def parseOptionalLimits(words, current): 246 if current.recurrence_type == "every": 247 parseLimits(words, current) 248 249 def parseLimits(words, current): 250 if words.have("from"): 251 words.need("the") 252 from_datetime = Selector() 253 from_datetime = parseSpecificRecurrence(words, from_datetime) 254 current.set_from(from_datetime) 255 256 if words.have("until"): 257 words.need("the") 258 until_datetime = Selector() 259 until_datetime = parseSpecificRecurrence(words, until_datetime) 260 current.set_until(until_datetime) 261 262 # vim: tabstop=4 expandtab shiftwidth=4