1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/RecurrenceSupport.py Sat Jul 13 01:33:22 2013 +0200
1.3 @@ -0,0 +1,262 @@
1.4 +# -*- coding: iso-8859-1 -*-
1.5 +"""
1.6 + MoinMoin - Calendar recurrence support
1.7 +
1.8 + @copyright: 2013 by Paul Boddie <paul@boddie.org.uk>
1.9 + @license: GNU GPL (v2 or later), see COPYING.txt for details.
1.10 +
1.11 +Supported grammar:
1.12 +
1.13 + <recurrence> [ of <recurrence> ]...
1.14 +
1.15 + recurrence = <specific-recurrence> | <repeating-recurrence>
1.16 +
1.17 + specific-recurrence = ( ( the <qualifier> <interval> ) | <datetime> | <month> | <year> )
1.18 + [ in <specific-recurrence> ]
1.19 +
1.20 + repeating-recurrence = every [ <qualifier> ] <interval>
1.21 + [ from <specific-recurrence> ]
1.22 + [ until <specific-recurrence> ]
1.23 +"""
1.24 +
1.25 +from DateSupport import weekday_labels, month_labels
1.26 +
1.27 +qualifiers = {
1.28 + "2nd" : 2,
1.29 + "third" : 3,
1.30 + "3rd" : 3,
1.31 + "fourth" : 4,
1.32 + "fifth" : 5,
1.33 + # stop at this point, supporting also "nth" and "n"
1.34 + }
1.35 +
1.36 +def isQualifier(qualifier, qualifiers):
1.37 + if qualifiers.has_key(qualifier):
1.38 + return qualifiers[qualifier]
1.39 + if qualifier.endswith("th") or qualifier.endswith("nd") or qualifier.endswith("st"):
1.40 + qualifier = qualifier[:-2]
1.41 + try:
1.42 + return int(qualifier)
1.43 + except ValueError:
1.44 + return False
1.45 +
1.46 +# Specific qualifiers refer to specific entities such as...
1.47 +# "the second Monday of every other month"
1.48 +# -> month 1 (Monday #2), month 3 (Monday #2), month 5 (Monday #2)
1.49 +
1.50 +specific_qualifiers = {
1.51 + "first" : 1,
1.52 + "1st" : 1,
1.53 + "second" : 2, # other is not permitted
1.54 + "last" : -1,
1.55 + "final" : -1,
1.56 + }
1.57 +
1.58 +specific_qualifiers.update(qualifiers)
1.59 +
1.60 +def isSpecificQualifier(qualifier):
1.61 + return isQualifier(qualifier, specific_qualifiers)
1.62 +
1.63 +# Repeating qualifiers refer to repeating entities such as...
1.64 +# "every other Monday of every other month"
1.65 +# -> month 1 (Monday #2, #4), month 3 (Monday #2, #4)
1.66 +
1.67 +repeating_qualifiers = {
1.68 + "single" : 1,
1.69 + "other" : 2, # second is not permitted (it clashes with the interval)
1.70 + }
1.71 +
1.72 +repeating_qualifiers.update(qualifiers)
1.73 +
1.74 +def isRepeatingQualifier(qualifier):
1.75 + return isQualifier(qualifier, repeating_qualifiers)
1.76 +
1.77 +intervals = {
1.78 + "second" : 1,
1.79 + "minute" : 2,
1.80 + "hour" : 3,
1.81 + "day" : 4,
1.82 + "week" : 5,
1.83 + "month" : 6,
1.84 + "year" : 7
1.85 + }
1.86 +
1.87 +# NOTE: Support day and month abbreviations in the input.
1.88 +
1.89 +for day in weekday_labels:
1.90 + intervals[day] = intervals["day"]
1.91 +
1.92 +for month in month_labels:
1.93 + intervals[month] = intervals["month"]
1.94 +
1.95 +# Parsing-related classes.
1.96 +
1.97 +class ParseError(Exception):
1.98 + pass
1.99 +
1.100 +class ParseIterator:
1.101 + def __init__(self, iterator):
1.102 + self.iterator = iterator
1.103 + self.tokens = []
1.104 + self.end = False
1.105 +
1.106 + def want(self):
1.107 + try:
1.108 + return self._next()
1.109 + except StopIteration:
1.110 + self.end = True
1.111 + return None
1.112 +
1.113 + def next(self):
1.114 + try:
1.115 + return self._next()
1.116 + except StopIteration:
1.117 + self.end = True
1.118 + raise ParseError, self.tokens
1.119 +
1.120 + def need(self, token):
1.121 + t = self._next()
1.122 + if t != token:
1.123 + raise ParseError, self.tokens
1.124 +
1.125 + def have(self, token):
1.126 + if self.end:
1.127 + return False
1.128 + t = self.tokens[-1]
1.129 + return t == token
1.130 +
1.131 + def _next(self):
1.132 + t = self.iterator.next()
1.133 + self.tokens.append(t)
1.134 + return t
1.135 +
1.136 +class Selector:
1.137 + def __init__(self, qualified_by=None):
1.138 + self.recurrence_type = None
1.139 + self.qualifier = None
1.140 + self.qualifier_level = None
1.141 + self.interval = None
1.142 + self.interval_level = None
1.143 + self.qualified_by = qualified_by
1.144 + self.from_datetime = None
1.145 + self.until_datetime = None
1.146 +
1.147 + def add_details(self, recurrence_type, qualifier, qualifier_level, interval, interval_level):
1.148 + self.recurrence_type = recurrence_type
1.149 + self.qualifier = qualifier
1.150 + self.qualifier_level = qualifier_level
1.151 + self.interval = interval
1.152 + self.interval_level = interval_level
1.153 +
1.154 + def set_from(self, from_datetime):
1.155 + self.from_datetime = from_datetime
1.156 +
1.157 + def set_until(self, until_datetime):
1.158 + self.until_datetime = until_datetime
1.159 +
1.160 + def __str__(self):
1.161 + return "%s %s %s%s%s%s" % (
1.162 + self.recurrence_type, self.qualifier, self.interval,
1.163 + self.from_datetime and " from {%s}" % self.from_datetime or "",
1.164 + self.until_datetime and " until {%s}" % self.until_datetime or "",
1.165 + self.qualified_by and ", in %s" % self.qualified_by or "")
1.166 +
1.167 + def __repr__(self):
1.168 + return "Selector(%s, %s, %s%s%s%s>" % (
1.169 + self.recurrence_type, self.qualifier, self.interval,
1.170 + self.from_datetime and ", from_datetime=%r" % self.from_datetime or "",
1.171 + self.until_datetime and ", until_datetime=%r" % self.until_datetime or "",
1.172 + self.qualified_by and ", qualified_by=%r" % self.qualified_by or "")
1.173 +
1.174 +# Parsing functions.
1.175 +
1.176 +def getRecurrence(s):
1.177 +
1.178 + "Interpret the given string 's', returning a recurrence description."
1.179 +
1.180 + words = ParseIterator(iter([w.strip() for w in s.split()]))
1.181 +
1.182 + current = Selector()
1.183 +
1.184 + current = parseRecurrence(words, current)
1.185 + parseOptionalLimits(words, current)
1.186 +
1.187 + # Obtain qualifications to the recurrence.
1.188 +
1.189 + while words.have("of"):
1.190 + current = Selector(current)
1.191 + current = parseRecurrence(words, current)
1.192 + parseOptionalLimits(words, current)
1.193 +
1.194 + if not words.end:
1.195 + raise ParseError, words.tokens
1.196 +
1.197 + return current
1.198 +
1.199 +def parseRecurrence(words, current):
1.200 + words.next()
1.201 + if words.have("every"):
1.202 + parseRepeatingRecurrence(words, current)
1.203 + words.want()
1.204 + return current
1.205 + elif words.have("the"):
1.206 + return parseSpecificRecurrence(words, current)
1.207 + else:
1.208 + raise ParseError, words.tokens
1.209 +
1.210 +def parseSpecificRecurrence(words, current):
1.211 + qualifier = words.next()
1.212 + qualifier_level = isSpecificQualifier(qualifier)
1.213 + if not qualifier_level:
1.214 + raise ParseError, words.tokens
1.215 + interval = words.next()
1.216 + interval_level = intervals.has_key(interval)
1.217 + if not interval_level:
1.218 + raise ParseError, words.tokens
1.219 +
1.220 + current.add_details("the", qualifier, qualifier_level, interval, interval_level)
1.221 +
1.222 + words.want()
1.223 + if words.have("in"):
1.224 + current = Selector(current)
1.225 + words.need("the")
1.226 + parseSpecificRecurrence(words, current)
1.227 +
1.228 + return current
1.229 +
1.230 +def parseRepeatingRecurrence(words, current):
1.231 + qualifier = words.next()
1.232 + if intervals.has_key(qualifier):
1.233 + interval = qualifier
1.234 + interval_level = intervals.has_key(interval)
1.235 + qualifier = "single"
1.236 + qualifier_level = isRepeatingQualifier(qualifier)
1.237 + else:
1.238 + qualifier_level = isRepeatingQualifier(qualifier)
1.239 + if not qualifier_level:
1.240 + raise ParseError, words.tokens
1.241 + interval = words.next()
1.242 + interval_level = intervals.has_key(interval)
1.243 + if not interval_level:
1.244 + raise ParseError, words.tokens
1.245 +
1.246 + current.add_details("every", qualifier, qualifier_level, interval, interval_level)
1.247 +
1.248 +def parseOptionalLimits(words, current):
1.249 + if current.recurrence_type == "every":
1.250 + parseLimits(words, current)
1.251 +
1.252 +def parseLimits(words, current):
1.253 + if words.have("from"):
1.254 + words.need("the")
1.255 + from_datetime = Selector()
1.256 + from_datetime = parseSpecificRecurrence(words, from_datetime)
1.257 + current.set_from(from_datetime)
1.258 +
1.259 + if words.have("until"):
1.260 + words.need("the")
1.261 + until_datetime = Selector()
1.262 + until_datetime = parseSpecificRecurrence(words, until_datetime)
1.263 + current.set_until(until_datetime)
1.264 +
1.265 +# vim: tabstop=4 expandtab shiftwidth=4