# HG changeset patch # User Paul Boddie # Date 1373672002 -7200 # Node ID 66479525a9fc3c0144974f64eed6a15add31d06c # Parent 0b6ab9f187e357a766dc15e0d276ca3a37516ee1 Added basic datetime recurrence description support. diff -r 0b6ab9f187e3 -r 66479525a9fc RecurrenceSupport.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/RecurrenceSupport.py Sat Jul 13 01:33:22 2013 +0200 @@ -0,0 +1,262 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - Calendar recurrence support + + @copyright: 2013 by Paul Boddie + @license: GNU GPL (v2 or later), see COPYING.txt for details. + +Supported grammar: + + [ of ]... + + recurrence = | + + specific-recurrence = ( ( the ) | | | ) + [ in ] + + repeating-recurrence = every [ ] + [ from ] + [ until ] +""" + +from DateSupport import weekday_labels, month_labels + +qualifiers = { + "2nd" : 2, + "third" : 3, + "3rd" : 3, + "fourth" : 4, + "fifth" : 5, + # stop at this point, supporting also "nth" and "n" + } + +def isQualifier(qualifier, qualifiers): + if qualifiers.has_key(qualifier): + return qualifiers[qualifier] + if qualifier.endswith("th") or qualifier.endswith("nd") or qualifier.endswith("st"): + qualifier = qualifier[:-2] + try: + return int(qualifier) + except ValueError: + return False + +# Specific qualifiers refer to specific entities such as... +# "the second Monday of every other month" +# -> month 1 (Monday #2), month 3 (Monday #2), month 5 (Monday #2) + +specific_qualifiers = { + "first" : 1, + "1st" : 1, + "second" : 2, # other is not permitted + "last" : -1, + "final" : -1, + } + +specific_qualifiers.update(qualifiers) + +def isSpecificQualifier(qualifier): + return isQualifier(qualifier, specific_qualifiers) + +# Repeating qualifiers refer to repeating entities such as... +# "every other Monday of every other month" +# -> month 1 (Monday #2, #4), month 3 (Monday #2, #4) + +repeating_qualifiers = { + "single" : 1, + "other" : 2, # second is not permitted (it clashes with the interval) + } + +repeating_qualifiers.update(qualifiers) + +def isRepeatingQualifier(qualifier): + return isQualifier(qualifier, repeating_qualifiers) + +intervals = { + "second" : 1, + "minute" : 2, + "hour" : 3, + "day" : 4, + "week" : 5, + "month" : 6, + "year" : 7 + } + +# NOTE: Support day and month abbreviations in the input. + +for day in weekday_labels: + intervals[day] = intervals["day"] + +for month in month_labels: + intervals[month] = intervals["month"] + +# Parsing-related classes. + +class ParseError(Exception): + pass + +class ParseIterator: + def __init__(self, iterator): + self.iterator = iterator + self.tokens = [] + self.end = False + + def want(self): + try: + return self._next() + except StopIteration: + self.end = True + return None + + def next(self): + try: + return self._next() + except StopIteration: + self.end = True + raise ParseError, self.tokens + + def need(self, token): + t = self._next() + if t != token: + raise ParseError, self.tokens + + def have(self, token): + if self.end: + return False + t = self.tokens[-1] + return t == token + + def _next(self): + t = self.iterator.next() + self.tokens.append(t) + return t + +class Selector: + def __init__(self, qualified_by=None): + self.recurrence_type = None + self.qualifier = None + self.qualifier_level = None + self.interval = None + self.interval_level = None + self.qualified_by = qualified_by + self.from_datetime = None + self.until_datetime = None + + def add_details(self, recurrence_type, qualifier, qualifier_level, interval, interval_level): + self.recurrence_type = recurrence_type + self.qualifier = qualifier + self.qualifier_level = qualifier_level + self.interval = interval + self.interval_level = interval_level + + def set_from(self, from_datetime): + self.from_datetime = from_datetime + + def set_until(self, until_datetime): + self.until_datetime = until_datetime + + def __str__(self): + return "%s %s %s%s%s%s" % ( + self.recurrence_type, self.qualifier, self.interval, + self.from_datetime and " from {%s}" % self.from_datetime or "", + self.until_datetime and " until {%s}" % self.until_datetime or "", + self.qualified_by and ", in %s" % self.qualified_by or "") + + def __repr__(self): + return "Selector(%s, %s, %s%s%s%s>" % ( + self.recurrence_type, self.qualifier, self.interval, + self.from_datetime and ", from_datetime=%r" % self.from_datetime or "", + self.until_datetime and ", until_datetime=%r" % self.until_datetime or "", + self.qualified_by and ", qualified_by=%r" % self.qualified_by or "") + +# Parsing functions. + +def getRecurrence(s): + + "Interpret the given string 's', returning a recurrence description." + + words = ParseIterator(iter([w.strip() for w in s.split()])) + + current = Selector() + + current = parseRecurrence(words, current) + parseOptionalLimits(words, current) + + # Obtain qualifications to the recurrence. + + while words.have("of"): + current = Selector(current) + current = parseRecurrence(words, current) + parseOptionalLimits(words, current) + + if not words.end: + raise ParseError, words.tokens + + return current + +def parseRecurrence(words, current): + words.next() + if words.have("every"): + parseRepeatingRecurrence(words, current) + words.want() + return current + elif words.have("the"): + return parseSpecificRecurrence(words, current) + else: + raise ParseError, words.tokens + +def parseSpecificRecurrence(words, current): + qualifier = words.next() + qualifier_level = isSpecificQualifier(qualifier) + if not qualifier_level: + raise ParseError, words.tokens + interval = words.next() + interval_level = intervals.has_key(interval) + if not interval_level: + raise ParseError, words.tokens + + current.add_details("the", qualifier, qualifier_level, interval, interval_level) + + words.want() + if words.have("in"): + current = Selector(current) + words.need("the") + parseSpecificRecurrence(words, current) + + return current + +def parseRepeatingRecurrence(words, current): + qualifier = words.next() + if intervals.has_key(qualifier): + interval = qualifier + interval_level = intervals.has_key(interval) + qualifier = "single" + qualifier_level = isRepeatingQualifier(qualifier) + else: + qualifier_level = isRepeatingQualifier(qualifier) + if not qualifier_level: + raise ParseError, words.tokens + interval = words.next() + interval_level = intervals.has_key(interval) + if not interval_level: + raise ParseError, words.tokens + + current.add_details("every", qualifier, qualifier_level, interval, interval_level) + +def parseOptionalLimits(words, current): + if current.recurrence_type == "every": + parseLimits(words, current) + +def parseLimits(words, current): + if words.have("from"): + words.need("the") + from_datetime = Selector() + from_datetime = parseSpecificRecurrence(words, from_datetime) + current.set_from(from_datetime) + + if words.have("until"): + words.need("the") + until_datetime = Selector() + until_datetime = parseSpecificRecurrence(words, until_datetime) + current.set_until(until_datetime) + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 0b6ab9f187e3 -r 66479525a9fc setup.py --- a/setup.py Fri Jun 21 23:16:28 2013 +0200 +++ b/setup.py Sat Jul 13 01:33:22 2013 +0200 @@ -11,5 +11,6 @@ version = "0.4", py_modules = ["ContentTypeSupport", "DateSupport", "GeneralSupport", "ItemSupport", "LocationSupport", "MoinDateSupport", - "MoinRemoteSupport", "MoinSupport", "ViewSupport"] + "MoinRemoteSupport", "MoinSupport", "RecurrenceSupport", + "ViewSupport"] ) diff -r 0b6ab9f187e3 -r 66479525a9fc tests/test_recurrence.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_recurrence.py Sat Jul 13 01:33:22 2013 +0200 @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +from RecurrenceSupport import getRecurrence + +print getRecurrence("every other day of every other month") +print getRecurrence("the second day of every other month") +print getRecurrence("every other day of every other month from the third month of every year") +print getRecurrence("every day from the second day until the 10th day of every other month") +print getRecurrence("every day from the 10th day in the second month until the 10th day in the 10th month of every third month") +print getRecurrence("every day of every third month from the 10th day in the second month until the 10th day in the 10th month") + +# vim: tabstop=4 expandtab shiftwidth=4