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