changeset 1433:6b0ea72d7665

mtime search works, added MoinMoin.support.parsedatetime, small fixes
author Franz Pletz <fpletz AT franz-pletz DOT org>
date Mon, 21 Aug 2006 02:30:05 +0200
parents fa0b7d2d998b
children 7bfc51951aa5
files MoinMoin/action/fullsearch.py MoinMoin/macro/AdvancedSearch.py MoinMoin/search/Xapian.py MoinMoin/search/__init__.py MoinMoin/search/builtin.py MoinMoin/support/__init__.py MoinMoin/support/parsedatetime/__init__.py MoinMoin/support/parsedatetime/parsedatetime.py MoinMoin/support/parsedatetime/parsedatetime_consts.py
diffstat 9 files changed, 1451 insertions(+), 53 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/action/fullsearch.py	Sun Aug 20 02:05:35 2006 +0200
+++ b/MoinMoin/action/fullsearch.py	Mon Aug 21 02:30:05 2006 +0200
@@ -8,9 +8,10 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import re
+import re, time
 from MoinMoin.Page import Page
 from MoinMoin import wikiutil
+from MoinMoin.support.parsedatetime.parsedatetime import Calendar
 
 
 def isTitleSearch(request):
@@ -54,6 +55,8 @@
     case = int(request.form.get('case', [0])[0])
     regex = int(request.form.get('regex', [0])[0]) # no interface currently
     hitsFrom = int(request.form.get('from', [0])[0])
+    mtime = None
+    msg = ''
 
     max_context = 1 # only show first `max_context` contexts XXX still unused
 
@@ -69,7 +72,19 @@
         mimetype = request.form.get('mimetype', [0])[0]
         includeunderlay = request.form.get('includeunderlay', [0])[0]
         onlysystempages = request.form.get('onlysystempages', [0])[0]
+
         mtime = request.form.get('mtime', [''])[0]
+        if mtime:
+            cal = Calendar()
+            mtime_parsed = cal.parse(mtime)
+
+            if mtime_parsed[1] == 0 and mtime_parsed[0] <= time.localtime():
+                mtime = time.mktime(mtime_parsed[0])
+            else:
+                msg = _('The modification date you entered was not recognized '
+                        'and is therefore not considered for the search '
+                        'results!')
+                mtime = None
         
         word_re = re.compile(r'(\"[\w\s]+"|\w+)')
         needle = ''
@@ -81,8 +96,6 @@
             needle += '-domain:underlay '
         if onlysystempages:
             needle += 'domain:system '
-        if mtime:
-            needle += 'lastmodifiedsince:%s ' % mtime
         if categories:
             needle += '(%s) ' % ' or '.join(['category:%s' % cat
                 for cat in word_re.findall(categories)])
@@ -94,14 +107,14 @@
             needle += '(%s) ' % ' or '.join(word_re.findall(or_terms))
 
     # check for sensible search term
-    striped = needle.strip()
-    if len(striped) == 0:
+    stripped = needle.strip()
+    if len(stripped) == 0:
         err = _('Please use a more selective search term instead '
                 'of {{{"%s"}}}') % needle
         request.emit_http_headers()
         Page(request, pagename).send_page(request, msg=err)
         return
-    needle = striped
+    needle = stripped
 
     # Setup for type of search
     if titlesearch:
@@ -115,7 +128,7 @@
     from MoinMoin.search import searchPages, QueryParser
     query = QueryParser(case=case, regex=regex,
             titlesearch=titlesearch).parse_query(needle)
-    results = searchPages(request, query, sort)
+    results = searchPages(request, query, sort, mtime)
 
     # directly show a single hit
     # XXX won't work with attachment search
@@ -135,7 +148,8 @@
     # This action generate data using the user language
     request.setContentLanguage(request.lang)
 
-    request.theme.send_title(title % needle, form=request.form, pagename=pagename)
+    request.theme.send_title(title % needle, form=request.form,
+            pagename=pagename, msg=msg)
 
     # Start content (important for RTL support)
     request.write(request.formatter.startContent("content"))
--- a/MoinMoin/macro/AdvancedSearch.py	Sun Aug 20 02:05:35 2006 +0200
+++ b/MoinMoin/macro/AdvancedSearch.py	Mon Aug 21 02:30:05 2006 +0200
@@ -12,18 +12,12 @@
 
 from MoinMoin import config, wikiutil, search
 from MoinMoin.i18n import languages
+from MoinMoin.support import sorted
 
 import mimetypes
 
 Dependencies = ['pages']
 
-try:
-    sorted
-except NameError:
-    def sorted(l, *args, **kw):
-        l = l[:]
-        l.sort(*args, **kw)
-        return l
 
 def advanced_ui(macro):
     _ = macro._
@@ -55,7 +49,7 @@
             # TODO: dropdown-box?
             (_('belonging to one of the following categories'),
                 '<input type="text" name="categories" size="30">'),
-            (_('last modified since (XXX)'),
+            (_('last modified since'),
                 '<input type="text" name="mtime" size="30" value="">'),
         )])
     ])
--- a/MoinMoin/search/Xapian.py	Sun Aug 20 02:05:35 2006 +0200
+++ b/MoinMoin/search/Xapian.py	Mon Aug 21 02:30:05 2006 +0200
@@ -405,7 +405,7 @@
                     xapdoc.Keyword('full_title', pagename.lower()),
                     xapdoc.Keyword('revision', revision),
                     xapdoc.Keyword('author', author),
-                )]
+                ]
             for pagelink in page.getPageLinks(request):
                 xkeywords.append(xapdoc.Keyword('linkto', pagelink))
             for category in categories:
@@ -516,33 +516,3 @@
         finally:
             writer.__del__()
 
-def run_query(query, db):
-    enquire = xapian.Enquire(db)
-    parser = xapian.QueryParser()
-    query = parser.parse_query(query, xapian.QueryParser.FLAG_WILDCARD)
-    print query.get_description()
-    enquire.set_query(query)
-    return enquire.get_mset(0, 10)
-
-def run(request):
-    pass
-    #print "Begin"
-    #db = xapian.WritableDatabase(xapian.open('test.db',
-    #                                         xapian.DB_CREATE_OR_OPEN))
-    #
-    # index_data(db) ???
-    #del db
-    #mset = run_query(sys.argv[1], db)
-    #print mset.get_matches_estimated()
-    #iterator = mset.begin()
-    #while iterator != mset.end():
-    #    print iterator.get_document().get_data()
-    #    iterator.next()
-    #for i in xrange(1,170):
-    #    doc = db.get_document(i)
-    #    print doc.get_data()
-
-if __name__ == '__main__':
-    run()
-
-
--- a/MoinMoin/search/__init__.py	Sun Aug 20 02:05:35 2006 +0200
+++ b/MoinMoin/search/__init__.py	Mon Aug 21 02:30:05 2006 +0200
@@ -13,7 +13,7 @@
 from MoinMoin.search.queryparser import QueryParser
 from MoinMoin.search.builtin import Search
 
-def searchPages(request, query, sort='weight', **kw):
+def searchPages(request, query, sort='weight', mtime=None, **kw):
     """ Search the text of all pages for query.
     
     @param request: current request
@@ -23,5 +23,5 @@
     """
     if isinstance(query, str) or isinstance(query, unicode):
         query = QueryParser(**kw).parse_query(query)
-    return Search(request, query, sort).run()
+    return Search(request, query, sort, mtime=mtime).run()
 
--- a/MoinMoin/search/builtin.py	Sun Aug 20 02:05:35 2006 +0200
+++ b/MoinMoin/search/builtin.py	Mon Aug 21 02:30:05 2006 +0200
@@ -352,10 +352,11 @@
 class Search:
     """ A search run """
     
-    def __init__(self, request, query, sort='weight'):
+    def __init__(self, request, query, sort='weight', mtime=None):
         self.request = request
         self.query = query
         self.sort = sort
+        self.mtime = mtime
         self.filtered = False
         self.fs_rootpage = "FS" # XXX FS hardcoded
 
@@ -552,9 +553,12 @@
         userMayRead = self.request.user.may.read
         fs_rootpage = self.fs_rootpage + "/"
         thiswiki = (self.request.cfg.interwikiname, 'Self')
-        filtered = [(wikiname, page, attachment, match) for wikiname, page, attachment, match in hits
-                    if not wikiname in thiswiki or
+        filtered = [(wikiname, page, attachment, match)
+                for wikiname, page, attachment, match in hits
+                    if (not wikiname in thiswiki or
                        page.exists() and userMayRead(page.page_name) or
-                       page.page_name.startswith(fs_rootpage)]
+                       page.page_name.startswith(fs_rootpage)) and
+                       (not self.mtime or 
+                           self.mtime <= page.mtime_usecs()/1000000)]
         return filtered
 
--- a/MoinMoin/support/__init__.py	Sun Aug 20 02:05:35 2006 +0200
+++ b/MoinMoin/support/__init__.py	Mon Aug 21 02:30:05 2006 +0200
@@ -10,3 +10,12 @@
     @copyright: 2001-2004 by Jürgen Hermann <jh@web.de>
     @license: GNU GPL, see COPYING for details.
 """
+
+try:
+    sorted = sorted
+except NameError:
+    def sorted(l, *args, **kw):
+        l = l[:]
+        l.sort(*args, **kw)
+        return l
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/parsedatetime/__init__.py	Mon Aug 21 02:30:05 2006 +0200
@@ -0,0 +1,17 @@
+version = '0.6.4'
+author  = 'Mike Taylor <http://code-bear.com>'
+license = """Copyright (c) 2004-2006 Mike Taylor, All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/parsedatetime/parsedatetime.py	Mon Aug 21 02:30:05 2006 +0200
@@ -0,0 +1,1112 @@
+#!/usr/bin/env python
+
+"""
+Parse human-readable date/time text.
+"""
+
+__license__ = """Copyright (c) 2004-2006 Mike Taylor, All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+__author__       = 'Mike Taylor <http://code-bear.com>'
+__contributors__ = ['Darshana Chhajed <mailto://darshana@osafoundation.org>',
+                   ]
+
+_debug = False
+
+
+import string, re, time
+import datetime, calendar, rfc822
+import parsedatetime_consts
+
+
+# Copied from feedparser.py
+# Universal Feedparser, Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
+# Originally a def inside of _parse_date_w3dtf()
+def _extract_date(m):
+    year = int(m.group('year'))
+    if year < 100:
+        year = 100 * int(time.gmtime()[0] / 100) + int(year)
+    if year < 1000:
+        return 0, 0, 0
+    julian = m.group('julian')
+    if julian:
+        julian = int(julian)
+        month = julian / 30 + 1
+        day = julian % 30 + 1
+        jday = None
+        while jday != julian:
+            t = time.mktime((year, month, day, 0, 0, 0, 0, 0, 0))
+            jday = time.gmtime(t)[-2]
+            diff = abs(jday - julian)
+            if jday > julian:
+                if diff < day:
+                    day = day - diff
+                else:
+                    month = month - 1
+                    day = 31
+            elif jday < julian:
+                if day + diff < 28:
+                   day = day + diff
+                else:
+                    month = month + 1
+        return year, month, day
+    month = m.group('month')
+    day = 1
+    if month is None:
+        month = 1
+    else:
+        month = int(month)
+        day = m.group('day')
+        if day:
+            day = int(day)
+        else:
+            day = 1
+    return year, month, day
+
+# Copied from feedparser.py 
+# Universal Feedparser, Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
+# Originally a def inside of _parse_date_w3dtf()
+def _extract_time(m):
+    if not m:
+        return 0, 0, 0
+    hours = m.group('hours')
+    if not hours:
+        return 0, 0, 0
+    hours = int(hours)
+    minutes = int(m.group('minutes'))
+    seconds = m.group('seconds')
+    if seconds:
+        seconds = int(seconds)
+    else:
+        seconds = 0
+    return hours, minutes, seconds
+
+
+# Copied from feedparser.py
+# Universal Feedparser, Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
+# Modified to return a tuple instead of mktime
+#
+# Original comment:
+#       W3DTF-style date parsing adapted from PyXML xml.utils.iso8601, written by
+#       Drake and licensed under the Python license.  Removed all range checking
+#       for month, day, hour, minute, and second, since mktime will normalize
+#       these later
+def _parse_date_w3dtf(dateString):
+    # the __extract_date and __extract_time methods were
+    # copied-out so they could be used by my code --bear
+    def __extract_tzd(m):
+        '''Return the Time Zone Designator as an offset in seconds from UTC.'''
+        if not m:
+            return 0
+        tzd = m.group('tzd')
+        if not tzd:
+            return 0
+        if tzd == 'Z':
+            return 0
+        hours = int(m.group('tzdhours'))
+        minutes = m.group('tzdminutes')
+        if minutes:
+            minutes = int(minutes)
+        else:
+            minutes = 0
+        offset = (hours*60 + minutes) * 60
+        if tzd[0] == '+':
+            return -offset
+        return offset
+
+    __date_re = ('(?P<year>\d\d\d\d)'
+                 '(?:(?P<dsep>-|)'
+                 '(?:(?P<julian>\d\d\d)'
+                 '|(?P<month>\d\d)(?:(?P=dsep)(?P<day>\d\d))?))?')
+    __tzd_re = '(?P<tzd>[-+](?P<tzdhours>\d\d)(?::?(?P<tzdminutes>\d\d))|Z)'
+    __tzd_rx = re.compile(__tzd_re)
+    __time_re = ('(?P<hours>\d\d)(?P<tsep>:|)(?P<minutes>\d\d)'
+                 '(?:(?P=tsep)(?P<seconds>\d\d(?:[.,]\d+)?))?'
+                 + __tzd_re)
+    __datetime_re = '%s(?:T%s)?' % (__date_re, __time_re)
+    __datetime_rx = re.compile(__datetime_re)
+    m = __datetime_rx.match(dateString)
+    if (m is None) or (m.group() != dateString): return
+    return _extract_date(m) + _extract_time(m) + (0, 0, 0)
+
+
+# Copied from feedparser.py
+# Universal Feedparser, Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
+# Modified to return a tuple instead of mktime
+#
+def _parse_date_rfc822(dateString):
+    '''Parse an RFC822, RFC1123, RFC2822, or asctime-style date'''
+    data = dateString.split()
+    if data[0][-1] in (',', '.') or data[0].lower() in rfc822._daynames:
+        del data[0]
+    if len(data) == 4:
+        s = data[3]
+        i = s.find('+')
+        if i > 0:
+            data[3:] = [s[:i], s[i+1:]]
+        else:
+            data.append('')
+        dateString = " ".join(data)
+    if len(data) < 5:
+        dateString += ' 00:00:00 GMT'
+    return rfc822.parsedate_tz(dateString)
+
+# rfc822.py defines several time zones, but we define some extra ones.
+# 'ET' is equivalent to 'EST', etc.
+_additional_timezones = {'AT': -400, 'ET': -500, 'CT': -600, 'MT': -700, 'PT': -800}
+rfc822._timezones.update(_additional_timezones)
+
+
+class Calendar:
+    """
+    A collection of routines to input, parse and manipulate date and times.
+    The text can either be 'normal' date values or it can be human readable.
+    """
+
+    def __init__(self, constants=None):
+        """
+        Default constructor for the Calendar class.
+
+        @type  constants: object
+        @param constants: Instance of the class L{CalendarConstants}
+
+        @rtype:  object
+        @return: Calendar instance
+        """
+          # if a constants reference is not included, use default
+        if constants is None:
+            self.ptc = parsedatetime_consts.CalendarConstants()
+        else:
+            self.ptc = constants
+
+        self.CRE_SPECIAL   = re.compile(self.ptc.RE_SPECIAL,   re.IGNORECASE)
+        self.CRE_UNITS     = re.compile(self.ptc.RE_UNITS,     re.IGNORECASE)
+        self.CRE_QUNITS    = re.compile(self.ptc.RE_QUNITS,    re.IGNORECASE)
+        self.CRE_MODIFIER  = re.compile(self.ptc.RE_MODIFIER,  re.IGNORECASE)
+        self.CRE_MODIFIER2 = re.compile(self.ptc.RE_MODIFIER2, re.IGNORECASE)
+        self.CRE_TIMEHMS   = re.compile(self.ptc.RE_TIMEHMS,   re.IGNORECASE)
+        self.CRE_TIMEHMS2  = re.compile(self.ptc.RE_TIMEHMS2,  re.IGNORECASE)
+        self.CRE_DATE      = re.compile(self.ptc.RE_DATE,      re.IGNORECASE)
+        self.CRE_DATE2     = re.compile(self.ptc.RE_DATE2,     re.IGNORECASE)
+        self.CRE_DATE3     = re.compile(self.ptc.RE_DATE3,     re.IGNORECASE)
+        self.CRE_MONTH     = re.compile(self.ptc.RE_MONTH,     re.IGNORECASE)
+        self.CRE_WEEKDAY   = re.compile(self.ptc.RE_WEEKDAY,   re.IGNORECASE)
+        self.CRE_DAY       = re.compile(self.ptc.RE_DAY,       re.IGNORECASE)
+        self.CRE_TIME      = re.compile(self.ptc.RE_TIME,      re.IGNORECASE)
+        self.CRE_REMAINING = re.compile(self.ptc.RE_REMAINING, re.IGNORECASE)
+
+        self.invalidFlag   = 0  # Is set if the datetime string entered cannot be parsed at all
+        self.weekdyFlag    = 0  # monday/tuesday/...
+        self.dateStdFlag   = 0  # 07/21/06
+        self.dateStrFlag   = 0  # July 21st, 2006
+        self.timeFlag      = 0  # 5:50 
+        self.meridianFlag  = 0  # am/pm
+        self.dayStrFlag    = 0  # tomorrow/yesterday/today/..
+        self.timeStrFlag   = 0  # lunch/noon/breakfast/...
+        self.modifierFlag  = 0  # after/before/prev/next/..
+        self.modifier2Flag = 0  # after/before/prev/next/..
+        self.unitsFlag     = 0  # hrs/weeks/yrs/min/..
+        self.qunitsFlag    = 0  # h/m/t/d..
+
+
+    def _convertUnitAsWords(self, unitText):
+        """
+        Converts text units into their number value
+
+        Five = 5
+        Twenty Five = 25
+        Two hundred twenty five = 225
+        Two thousand and twenty five = 2025
+        Two thousand twenty five = 2025
+
+        @type  unitText: string
+        @param unitText: number string
+
+        @rtype:  integer
+        @return: numerical value of unitText
+        """
+        # TODO: implement this
+        pass
+
+
+    def _buildTime(self, source, quantity, modifier, units):
+        """
+        Take quantity, modifier and unit strings and convert them into values.
+        Then calcuate the time and return the adjusted sourceTime
+
+        @type  source:   time
+        @param source:   time to use as the base (or source)
+        @type  quantity: string
+        @param quantity: quantity string
+        @type  modifier: string
+        @param modifier: how quantity and units modify the source time
+        @type  units:    string
+        @param units:    unit of the quantity (i.e. hours, days, months, etc)
+
+        @rtype:  timetuple
+        @return: timetuple of the calculated time
+        """
+        if _debug:
+            print '_buildTime: [%s][%s][%s]' % (quantity, modifier, units)
+
+        if source is None:
+            source = time.localtime()
+
+        if quantity is None:
+            quantity = ''
+        else:
+            quantity = string.strip(quantity)
+
+        if len(quantity) == 0:
+            qty = 1
+        else:
+            try:
+                qty = int(quantity)
+            except ValueError:
+                qty = 0
+
+        if modifier in self.ptc.Modifiers:
+            qty = qty * self.ptc.Modifiers[modifier]
+
+            if units is None or units == '':
+                units = 'dy'
+
+        # plurals are handled by regex's (could be a bug tho)
+
+        if units in self.ptc.Units:
+            u = self.ptc.Units[units]
+        else:
+            u = 1
+
+        (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = source
+
+        start  = datetime.datetime(yr, mth, dy, hr, mn, sec)
+        target = start
+
+        if units.startswith('y'):
+            target = self.inc(start, year=qty)
+        elif units.endswith('th') or units.endswith('ths'):
+            target = self.inc(start, month=qty)
+        else:
+            if units.startswith('d'):
+                target = start + datetime.timedelta(days=qty)
+            elif units.startswith('h'):
+                target = start + datetime.timedelta(hours=qty)
+            elif units.startswith('m'):
+                target = start + datetime.timedelta(minutes=qty)
+            elif units.startswith('s'):
+                target = start + datetime.timedelta(seconds=qty)
+            elif units.startswith('w'):
+                target = start + datetime.timedelta(weeks=qty)
+
+        if target != start:
+            self.invalidFlag = 0
+
+        return target.timetuple()
+
+
+    def parseDate(self, dateString):
+        """
+        Parses strings like 05/28/200 or 04.21
+
+        @type  dateString: string
+        @param dateString: text to convert to a datetime
+
+        @rtype:  datetime
+        @return: calculated datetime value of dateString
+        """
+        yr, mth, dy, hr, mn, sec, wd, yd, isdst = time.localtime()
+
+        # XXX: Quick fix to ignore 'the'
+        dateString = dateString.replace('the', '')
+
+        s = dateString
+        m = self.CRE_DATE2.search(s)
+        if m is not None:
+            index = m.start()
+            mth   = int(s[:index])
+            s     = s[index + 1:]
+
+        m = self.CRE_DATE2.search(s)
+        if m is not None:
+            index = m.start()
+            dy    = int(s[:index])
+            yr    = int(s[index + 1:])
+            # TODO should this have a birthday epoch constraint?
+            if yr < 99:
+                yr += 2000
+        else:
+            dy = int(string.strip(s))
+
+        if mth <= 12 and dy <= self.ptc.DaysInMonthList[mth - 1]:
+            sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
+        else:
+            self.invalidFlag = 1
+            sourceTime       = time.localtime() #return current time if date string is invalid
+
+        return sourceTime
+
+
+    def parseDateText(self, dateString):
+        """
+        Parses strings like "May 31st, 2006" or "Jan 1st" or "July 2006"
+
+        @type  dateString: string
+        @param dateString: text to convert to a datetime
+
+        @rtype:  datetime
+        @return: calculated datetime value of dateString
+        """
+        yr, mth, dy, hr, mn, sec, wd, yd, isdst = time.localtime()
+
+        currentMth = mth
+        currentDy  = dy
+
+        s   = dateString.lower()
+        m   = self.CRE_DATE3.search(s)
+        mth = m.group('mthname')
+        mth = int(self.ptc.MthNames[mth])
+
+        if m.group('day') !=  None:
+            dy = int(m.group('day'))
+        else:
+            dy = 1
+
+        if m.group('year') !=  None:
+            yr = int(m.group('year'))
+        elif (mth < currentMth) or (mth == currentMth and dy < currentDy):
+            # if that day and month have already passed in this year,
+            # then increment the year by 1
+            yr += 1
+
+        if dy <= self.ptc.DaysInMonthList[mth - 1]:
+            sourceTime = (yr, mth, dy, 9, 0, 0, wd, yd, isdst)
+        else:
+              # Return current time if date string is invalid
+            self.invalidFlag = 1
+            sourceTime       = time.localtime()
+
+        return sourceTime
+
+
+    def _evalModifier(self, modifier, chunk1, chunk2, sourceTime):
+        """
+        Evaluate the modifier string and following text (passed in
+        as chunk1 and chunk2) and if they match any known modifiers
+        calculate the delta and apply it to sourceTime
+
+        @type  modifier: string
+        @param modifier: modifier text to apply to sourceTime
+        @type  chunk1:   string
+        @param chunk1:   first text chunk that followed modifier (if any)
+        @type  chunk2:   string
+        @param chunk2:   second text chunk that followed modifier (if any)
+        @type  sourceTime: datetime
+        @param sourceTime: datetime value to use as the base
+
+        @rtype:  tuple
+        @return: tuple of any remaining text and the modified sourceTime
+        """
+        offset = self.ptc.Modifiers[modifier]
+
+        if sourceTime is not None:
+            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
+        else:
+            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = time.localtime()
+
+        # capture the units after the modifier and the remaining string after the unit
+        m = self.CRE_REMAINING.search(chunk2)
+        if m is not None:
+            index  = m.start() + 1
+            unit   = chunk2[:m.start()]
+            chunk2 = chunk2[index:]
+        else:
+            unit   = chunk2
+            chunk2 = ''
+
+        flag = 0
+
+        if unit == self.ptc.Target_Text['month'] or \
+           unit == self.ptc.Target_Text['mth']:
+            if offset == 0:
+                dy        = self.ptc.DaysInMonthList[mth - 1]
+                sourceTime = (yr, mth, dy, 9, 0, 0, wd, yd, isdst)
+            elif offset == 2:
+                # if day is the last day of the month, calculate the last day of the next month
+                if dy == self.ptc.DaysInMonthList[mth - 1]:
+                    dy = self.ptc.DaysInMonthList[mth]
+
+                start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
+                target     = self.inc(start, month=1)
+                sourceTime = target.timetuple()
+            else:
+                start      = datetime.datetime(yr, mth, 1, 9, 0, 0)
+                target     = self.inc(start, month=offset)
+                sourceTime = target.timetuple()
+
+            flag = 1
+
+        if unit == self.ptc.Target_Text['week'] or \
+             unit == self.ptc.Target_Text['wk'] or \
+             unit == self.ptc.Target_Text['w']:
+            if offset == 0:
+                start      = datetime.datetime(yr, mth, dy, 17, 0, 0)
+                target     = start + datetime.timedelta(days=(4 - wd))
+                sourceTime = target.timetuple()
+            elif offset == 2:
+                start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
+                target     = start + datetime.timedelta(days=7)
+                sourceTime = target.timetuple()
+            else:
+                return self._evalModifier(modifier, chunk1, "monday " + chunk2, sourceTime)
+
+            flag = 1
+
+        if unit == self.ptc.Target_Text['day'] or \
+            unit == self.ptc.Target_Text['dy'] or \
+            unit == self.ptc.Target_Text['d']:
+            if offset == 0:
+                sourceTime = (yr, mth, dy, 17, 0, 0, wd, yd, isdst)
+            elif offset == 2:
+                start      = datetime.datetime(yr, mth, dy, hr, mn, sec)
+                target     = start + datetime.timedelta(days=1)
+                sourceTime = target.timetuple()
+            else:
+                start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
+                target     = start + datetime.timedelta(days=offset)
+                sourceTime = target.timetuple()
+
+            flag = 1
+
+        if unit == self.ptc.Target_Text['hour'] or \
+           unit == self.ptc.Target_Text['hr']:
+            if offset == 0:
+                sourceTime = (yr, mth, dy, hr, 0, 0, wd, yd, isdst)
+            else:
+                start      = datetime.datetime(yr, mth, dy, hr, 0, 0)
+                target     = start + datetime.timedelta(hours=offset)
+                sourceTime = target.timetuple()
+
+            flag = 1
+
+        if unit == self.ptc.Target_Text['year'] or \
+             unit == self.ptc.Target_Text['yr'] or \
+             unit == self.ptc.Target_Text['y']:
+            if offset == 0:
+                sourceTime = (yr, 12, 31, hr, mn, sec, wd, yd, isdst)
+            elif offset == 2:
+                sourceTime = (yr + 1, mth, dy, hr, mn, sec, wd, yd, isdst)
+            else:
+                sourceTime = (yr + offset, 1, 1, 9, 0, 0, wd, yd, isdst)
+
+            flag = 1
+
+        if flag == 0:
+            m = self.CRE_WEEKDAY.match(unit)
+            if m is not None:
+                wkdy = m.group()
+                wkdy = self.ptc.WeekDays[wkdy]
+
+                if offset == 0:
+                    diff       = wkdy - wd
+                    start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
+                    target     = start + datetime.timedelta(days=diff)
+                    sourceTime = target.timetuple()
+                else:
+                    diff       = wkdy - wd
+                    start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
+                    target     = start + datetime.timedelta(days=diff + 7 * offset)
+                    sourceTime = target.timetuple()
+
+                flag = 1
+
+        if flag == 0:
+            m = self.CRE_TIME.match(unit)
+            if m is not None:
+                (yr, mth, dy, hr, mn, sec, wd, yd, isdst), self.invalidFlag = self.parse(unit)
+                start      = datetime.datetime(yr, mth, dy, hr, mn, sec)
+                target     = start + datetime.timedelta(days=offset)
+                sourceTime = target.timetuple()
+
+                flag              = 1
+                self.modifierFlag = 0
+
+        # if the word after next is a number, the string is likely
+        # to be something like "next 4 hrs" for which we have to
+        # combine the units with the rest of the string
+        if flag == 0:
+            if offset < 0:
+                # if offset is negative, the unit has to be made negative
+                unit = '-%s' % unit
+
+            chunk2 = '%s %s' % (unit, chunk2)
+
+        self.modifierFlag = 0
+
+        return '%s %s' % (chunk1, chunk2), sourceTime
+
+
+    def _evalModifier2(self, modifier, chunk1 , chunk2, sourceTime):
+        """
+        Evaluate the modifier string and following text (passed in
+        as chunk1 and chunk2) and if they match any known modifiers
+        calculate the delta and apply it to sourceTime
+
+        @type  modifier:   string
+        @param modifier:   modifier text to apply to sourceTime
+        @type  chunk1:     string
+        @param chunk1:     first text chunk that followed modifier (if any)
+        @type  chunk2:     string
+        @param chunk2:     second text chunk that followed modifier (if any)
+        @type  sourceTime: datetime
+        @param sourceTime: datetime value to use as the base
+
+        @rtype:  tuple
+        @return: tuple of any remaining text and the modified sourceTime
+        """
+        offset = self.ptc.Modifiers[modifier]
+        digit  = r'\d+'
+
+        if sourceTime is not None:
+            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
+        else:
+            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = time.localtime()
+
+        self.modifier2Flag = 0
+
+        # If the string after the negative modifier starts with
+        # digits, then it is likely that the string is similar to
+        # " before 3 days" or 'evening prior to 3 days'.
+        # In this case, the total time is calculated by subtracting
+        # '3 days' from the current date.
+        # So, we have to identify the quantity and negate it before
+        # parsing the string.
+        # This is not required for strings not starting with digits
+        # since the string is enough to calculate the sourceTime
+        if offset < 0:
+            m = re.match(digit, string.strip(chunk2))
+            if m is not None:
+                qty    = int(m.group()) * -1
+                chunk2 = chunk2[m.end():]
+                chunk2 = '%d%s' % (qty, chunk2)
+
+        sourceTime, flag = self.parse(chunk2, sourceTime)
+
+        if chunk1 != '':
+            if offset < 0:
+                m = re.match(digit, string.strip(chunk1))
+                if m is not None:
+                    qty    = int(m.group()) * -1
+                    chunk1 = chunk1[m.end():]
+                    chunk1 = '%d%s' % (qty, chunk1)
+
+            sourceTime, flag = self.parse(chunk1, sourceTime)
+
+        return '', sourceTime
+
+
+    def _evalString(self, datetimeString, sourceTime=None):
+        """
+        Calculate the datetime based on flags set by the L{parse()} routine
+
+        Examples handled::
+            RFC822, W3CDTF formatted dates
+            HH:MM[:SS][ am/pm]
+            MM/DD/YYYY
+            DD MMMM YYYY
+
+        @type  datetimeString: string
+        @param datetimeString: text to try and parse as more "traditional" date/time text
+        @type  sourceTime:     datetime
+        @param sourceTime:     datetime value to use as the base
+
+        @rtype:  datetime
+        @return: calculated datetime value or current datetime if not parsed
+        """
+        s = string.strip(datetimeString)
+
+          # Given string date is a RFC822 date
+        if sourceTime is None:
+            sourceTime = _parse_date_rfc822(s)
+
+          # Given string date is a W3CDTF date
+        if sourceTime is None:
+            sourceTime = _parse_date_w3dtf(s)
+
+        if sourceTime is None:
+            s = s.lower()
+
+          # Given string is in the format HH:MM(:SS)(am/pm)
+        if self.meridianFlag == 1:
+            if sourceTime is None:
+                (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = time.localtime()
+            else:
+                (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
+
+            m = self.CRE_TIMEHMS2.search(s)
+            if m is not None:
+                dt = s[:m.start('meridian')].strip()
+                if len(dt) <= 2:
+                    hr  = int(dt)
+                    mn  = 0
+                    sec = 0
+                else:
+                    hr, mn, sec = _extract_time(m)
+
+                if hr == 24:
+                    hr = 0
+
+                sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
+                meridian   = m.group('meridian')
+
+                if (re.compile("a").search(meridian)) and hr == 12:
+                    sourceTime = (yr, mth, dy, 0, mn, sec, wd, yd, isdst)
+                if (re.compile("p").search(meridian)) and hr < 12:
+                    sourceTime = (yr, mth, dy, hr+12, mn, sec, wd, yd, isdst)
+
+              # invalid time
+            if hr > 24 or mn > 59 or sec > 59:
+                sourceTime       = time.localtime()
+                self.invalidFlag = 1
+
+            self.meridianFlag = 0
+
+          # Given string is in the format HH:MM(:SS)
+        if self.timeFlag == 1:
+            if sourceTime is None:
+                (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = time.localtime()
+            else:
+                (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
+
+            m = self.CRE_TIMEHMS.search(s)
+            if m is not None:
+                hr, mn, sec = _extract_time(m)
+            if hr == 24:
+                hr = 0
+
+            if hr > 24 or mn > 59 or sec > 59:
+                # invalid time
+                sourceTime = time.localtime()
+                self.invalidFlag = 1
+            else:
+                sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
+
+            self.timeFlag = 0
+
+          # Given string is in the format 07/21/2006
+        if self.dateStdFlag == 1:
+            sourceTime       = self.parseDate(s)
+            self.dateStdFlag = 0
+
+          # Given string is in the format  "May 23rd, 2005"
+        if self.dateStrFlag == 1:
+            sourceTime       = self.parseDateText(s)
+            self.dateStrFlag = 0
+
+          # Given string is a weekday
+        if self.weekdyFlag == 1:
+            yr, mth, dy, hr, mn, sec, wd, yd, isdst = time.localtime()
+            start = datetime.datetime(yr, mth, dy, hr, mn, sec)
+            wkDy  = self.ptc.WeekDays[s]
+
+            if wkDy > wd:
+                qty    = wkDy - wd
+                target = start + datetime.timedelta(days=qty)
+                wd     = wkDy
+            else:
+                qty    = 6 - wd + wkDy + 1
+                target = start + datetime.timedelta(days=qty)
+                wd     = wkDy
+
+            sourceTime      = target.timetuple()
+            self.weekdyFlag = 0
+
+          # Given string is a natural language time string like lunch, midnight, etc
+        if self.timeStrFlag == 1:
+            if sourceTime is None:
+                (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = time.localtime()
+            else:
+                (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
+
+            sources = { 'now':       (yr, mth, dy, hr, mn, sec, wd, yd, isdst),
+                        'noon':      (yr, mth, dy, 12,  0,   0, wd, yd, isdst),
+                        'lunch':     (yr, mth, dy, 12,  0,   0, wd, yd, isdst),
+                        'morning':   (yr, mth, dy,  6,  0,   0, wd, yd, isdst),
+                        'breakfast': (yr, mth, dy,  8,  0,   0, wd, yd, isdst),
+                        'dinner':    (yr, mth, dy, 19,  0,   0, wd, yd, isdst),
+                        'evening':   (yr, mth, dy, 18,  0,   0, wd, yd, isdst),
+                        'midnight':  (yr, mth, dy,  0,  0,   0, wd, yd, isdst),
+                        'night':     (yr, mth, dy, 21,  0,   0, wd, yd, isdst),
+                        'tonight':   (yr, mth, dy, 21,  0,   0, wd, yd, isdst),
+                      }
+
+            if s in sources:
+                sourceTime = sources[s]
+            else:
+                sourceTime       = time.localtime()
+                self.invalidFlag = 1
+
+            self.timeStrFlag = 0
+
+           # Given string is a natural language date string like today, tomorrow..
+        if self.dayStrFlag == 1:
+            if sourceTime is None:
+                sourceTime = time.localtime()
+
+            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
+
+            sources = { 'tomorrow':   1,
+                        'today':      0,
+                        'yesterday': -1,
+                       }
+
+            start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
+            target     = start + datetime.timedelta(days=sources[s])
+            sourceTime = target.timetuple()
+
+            self.dayStrFlag = 0
+
+          # Given string is a time string with units like "5 hrs 30 min"
+        if self.unitsFlag == 1:
+            modifier = ''  # TODO
+
+            if sourceTime is None:
+                sourceTime = time.localtime()
+
+            m = self.CRE_UNITS.search(s)
+            if m is not None:
+                units    = m.group('units')
+                quantity = s[:m.start('units')]
+
+            sourceTime     = self._buildTime(sourceTime, quantity, modifier, units)
+            self.unitsFlag = 0
+
+          # Given string is a time string with single char units like "5 h 30 m"
+        if self.qunitsFlag == 1:
+            modifier = ''  # TODO
+
+            if sourceTime is None:
+                sourceTime = time.localtime()
+
+            m = self.CRE_QUNITS.search(s)
+            if m is not None:
+                units    = m.group('qunits')
+                quantity = s[:m.start('qunits')]
+
+            sourceTime      = self._buildTime(sourceTime, quantity, modifier, units)
+            self.qunitsFlag = 0
+
+          # Given string does not match anything
+        if sourceTime is None:
+            sourceTime       = time.localtime()
+            self.invalidFlag = 1
+
+        return sourceTime
+
+
+    def parse(self, datetimeString, sourceTime=None):
+        """
+        Splits the L{datetimeString} into tokens, finds the regex patters
+        that match and then calculates a datetime value from the chunks
+
+        if L{sourceTime} is given then the datetime value will be calcualted
+        from that datetime, otherwise from the current datetime.
+
+        @type  datetimeString: string
+        @param datetimeString: datetime text to evaluate
+        @type  sourceTime:     datetime
+        @param sourceTime:     datetime value to use as the base
+
+        @rtype:  tuple
+        @return: tuple of any remaining text and the modified sourceTime
+        """
+        s         = string.strip(datetimeString.lower())
+        dateStr   = ''
+        parseStr  = ''
+        totalTime = sourceTime
+
+        self.invalidFlag = 0
+
+        if s == '' :
+            if sourceTime is not None:
+                return (sourceTime, 0)
+            else:
+                return (time.localtime(), 1)
+
+        while len(s) > 0:
+            flag   = 0
+            chunk1 = ''
+            chunk2 = ''
+
+            if _debug:
+                print 'parse (top of loop): [%s][%s]' % (s, parseStr)
+
+            if parseStr == '':
+                # Modifier like next\prev..
+                m = self.CRE_MODIFIER.search(s)
+                if m is not None:
+                    self.modifierFlag = 1
+                    if (m.group('modifier') != s):
+                        # capture remaining string
+                        parseStr = m.group('modifier')
+                        chunk1   = string.strip(s[:m.start('modifier')])
+                        chunk2   = string.strip(s[m.end('modifier'):])
+                        flag     = 1
+                    else:
+                        parseStr = s
+
+            if parseStr == '':
+                # Modifier like from\after\prior..
+                m = self.CRE_MODIFIER2.search(s)
+                if m is not None:
+                    self.modifier2Flag = 1
+                    if (m.group('modifier') != s):
+                        # capture remaining string
+                        parseStr = m.group('modifier')
+                        chunk1   = string.strip(s[:m.start('modifier')])
+                        chunk2   = string.strip(s[m.end('modifier'):])
+                        flag     = 1
+                    else:
+                        parseStr = s
+
+            if parseStr == '':
+                # String date format
+                m = self.CRE_DATE3.search(s)
+                if m is not None:
+                    self.dateStrFlag = 1
+                    if (m.group('date') != s):
+                        # capture remaining string
+                        parseStr = m.group('date')
+                        chunk1   = s[:m.start('date')]
+                        chunk2   = s[m.end('date'):]
+                        s        = '%s %s' % (chunk1, chunk2)
+                        flag     = 1
+                    else:
+                        parseStr = s
+
+            if parseStr == '':
+                # Standard date format
+                m = self.CRE_DATE.search(s)
+                if m is not None:
+                    self.dateStdFlag = 1
+                    if (m.group('date') != s):
+                        # capture remaining string
+                        parseStr = m.group('date')
+                        chunk1   = s[:m.start('date')]
+                        chunk2   = s[m.end('date'):]
+                        s        = '%s %s' % (chunk1, chunk2)
+                        flag     = 1
+                    else:
+                        parseStr = s
+
+            if parseStr == '':
+                # Natural language day strings
+                m = self.CRE_DAY.search(s)
+                if m is not None:
+                    self.dayStrFlag = 1
+                    if (m.group('day') != s):
+                        # capture remaining string
+                        parseStr = m.group('day')
+                        chunk1   = s[:m.start('day')]
+                        chunk2   = s[m.end('day'):]
+                        s        = '%s %s' % (chunk1, chunk2)
+                        flag     = 1
+                    else:
+                        parseStr = s
+
+            if parseStr == '':
+                # Quantity + Units
+                m = self.CRE_UNITS.search(s)
+                if m is not None:
+                    self.unitsFlag = 1
+                    if (m.group('qty') != s):
+                        # capture remaining string
+                        parseStr = m.group('qty')
+                        chunk1   = s[:m.start('qty')]
+                        chunk2   = s[m.end('qty'):]
+                        s        = '%s %s' % (chunk1, chunk2)
+                        flag     = 1
+                    else:
+                        parseStr = s
+
+            if parseStr == '':
+                # Quantity + Units
+                m = self.CRE_QUNITS.search(s)
+                if m is not None:
+                    self.qunitsFlag = 1
+                    if (m.group('qty') != s):
+                        # capture remaining string
+                        parseStr = m.group('qty')
+                        chunk1   = s[:m.start('qty')]
+                        chunk2   = s[m.end('qty'):]
+                        s        = '%s %s' % (chunk1, chunk2)
+                        flag     = 1
+                    else:
+                        parseStr = s 
+
+            if parseStr == '':
+                # Weekday
+                m = self.CRE_WEEKDAY.search(s)
+                if m is not None:
+                    self.weekdyFlag = 1
+                    if (m.group('weekday') != s):
+                        # capture remaining string
+                        parseStr = m.group()
+                        chunk1   = s[:m.start('weekday')]
+                        chunk2   = s[m.end('weekday'):]
+                        s        = '%s %s' % (chunk1, chunk2)
+                        flag     = 1
+                    else:
+                        parseStr = s
+
+            if parseStr == '':
+                # Natural language time strings
+                m = self.CRE_TIME.search(s)
+                if m is not None:
+                    self.timeStrFlag = 1
+                    if (m.group('time') != s):
+                        # capture remaining string
+                        parseStr = m.group('time')
+                        chunk1   = s[:m.start('time')]
+                        chunk2   = s[m.end('time'):]
+                        s        = '%s %s' % (chunk1, chunk2)
+                        flag     = 1
+                    else:
+                        parseStr = s
+
+            if parseStr == '':
+                # HH:MM(:SS) am/pm time strings
+                m = self.CRE_TIMEHMS2.search(s)
+                if m is not None:
+                    self.meridianFlag = 1
+                    if m.group('minutes') is not None:
+                        if m.group('seconds') is not None:
+                            parseStr = '%s:%s:%s %s' % (m.group('hours'), m.group('minutes'), m.group('seconds'), m.group('meridian'))
+                        else:
+                            parseStr = '%s:%s %s' % (m.group('hours'), m.group('minutes'), m.group('meridian'))
+                    else:
+                        parseStr = '%s %s' % (m.group('hours'), m.group('meridian'))
+
+                    chunk1 = s[:m.start('hours')]
+                    chunk2 = s[m.end('meridian'):]
+
+                    s    = '%s %s' % (chunk1, chunk2)
+                    flag = 1
+
+            if parseStr == '':
+                # HH:MM(:SS) time strings
+                m = self.CRE_TIMEHMS.search(s)
+                if m is not None:
+                    self.timeFlag = 1
+                    if m.group('seconds') is not None:
+                        parseStr = '%s:%s:%s' % (m.group('hours'), m.group('minutes'), m.group('seconds'))
+                        chunk1   = s[:m.start('hours')]
+                        chunk2   = s[m.end('seconds'):]
+                    else:
+                        parseStr = '%s:%s' % (m.group('hours'), m.group('minutes'))
+                        chunk1   = s[:m.start('hours')]
+                        chunk2   = s[m.end('minutes'):]
+
+                    s    = '%s %s' % (chunk1, chunk2)
+                    flag = 1
+
+            # if string does not match any regex, empty string to come out of the while loop
+            if flag is 0:
+                s = ''
+
+            if _debug:
+                print 'parse (bottom) [%s][%s][%s][%s]' % (s, parseStr, chunk1, chunk2)
+                print 'invalid [%d] weekday [%d] dateStd [%d] dateStr [%d] time [%d] timeStr [%d] meridian [%d]' % \
+                       (self.invalidFlag, self.weekdyFlag, self.dateStdFlag, self.dateStrFlag, self.timeFlag, self.timeStrFlag, self.meridianFlag)
+                print 'dayStr [%d] modifier [%d] modifier2 [%d] units [%d] qunits[%d]' % \
+                       (self.dayStrFlag, self.modifierFlag, self.modifier2Flag, self.unitsFlag, self.qunitsFlag)
+
+            # evaluate the matched string
+            if parseStr != '':
+                if self.modifierFlag == 1:
+                    t, totalTime = self._evalModifier(parseStr, chunk1, chunk2, totalTime)
+
+                    return self.parse(t, totalTime)
+
+                elif self.modifier2Flag == 1:
+                    s, totalTime = self._evalModifier2(parseStr, chunk1, chunk2, totalTime)
+                else:
+                    totalTime = self._evalString(parseStr, totalTime)
+                    parseStr  = ''
+
+        # String is not parsed at all
+        if totalTime is None or totalTime == sourceTime:
+            totalTime        = time.localtime()
+            self.invalidFlag = 1
+
+        return (totalTime, self.invalidFlag)
+
+
+    def inc(self, source, month=None, year=None):
+        """
+        Takes the given date, or current date if none is passed, and
+        increments it according to the values passed in by month
+        and/or year.
+
+        This routine is needed because the timedelta() routine does
+        not allow for month or year increments.
+
+        @type  source: datetime
+        @param source: datetime value to increment
+        @type  month:  integer
+        @param month:  optional number of months to increment
+        @type  year:   integer
+        @param year:   optional number of years to increment
+
+        @rtype:  datetime
+        @return: L{source} incremented by the number of months and/or years
+        """
+        yr  = source.year
+        mth = source.month
+
+        if year:
+            try:
+                yi = int(year)
+            except ValueError:
+                yi = 0
+
+            yr += yi
+
+        if month:
+            try:
+                mi = int(month)
+            except ValueError:
+                mi = 0
+
+            m = abs(mi)
+            y = m / 12      # how many years are in month increment
+            m = m % 12      # get remaining months
+
+            if mi < 0:
+                mth = mth - m           # sub months from start month
+                if mth < 1:             # cross start-of-year?
+                    y   -= 1            #   yes - decrement year
+                    mth += 12           #         and fix month
+            else:
+                mth = mth + m           # add months to start month
+                if mth > 12:            # cross end-of-year?
+                    y   += 1            #   yes - increment year
+                    mth -= 12           #         and fix month
+
+            yr += y
+
+        d = source.replace(year=yr, month=mth)
+
+        return source + (d - source)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/parsedatetime/parsedatetime_consts.py	Mon Aug 21 02:30:05 2006 +0200
@@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+"""
+CalendarConstants defines all constants used by parsedatetime.py.
+"""
+
+__license__ = """Copyright (c) 2004-2006 Mike Taylor, All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+__author__       = 'Mike Taylor <http://code-bear.com>'
+__contributors__ = ['Darshana Chhajed <mailto://darshana@osafoundation.org>',
+                   ]
+
+
+class CalendarConstants:
+    def __init__(self):
+        self.Locale = 'American'
+
+        self.TIMESEP      = ':'
+
+        self.RE_SPECIAL   = r'(?P<special>^[in|on|of|at]+)\s+'
+        self.RE_UNITS     = r'(?P<qty>(-?\d+\s*(?P<units>((hour|hr|minute|min|second|sec|day|dy|week|wk|month|mth|year|yr)s?))))'
+        self.RE_QUNITS    = r'(?P<qty>(-?\d+\s?(?P<qunits>h|m|s|d|w|m|y)(\s|,|$)))'
+        self.RE_MODIFIER  = r'(?P<modifier>(previous|prev|last|next|this|eo|(end\sof)|(in\sa)))'
+        self.RE_MODIFIER2 = r'(?P<modifier>(from|before|after|ago|prior))'
+        self.RE_TIMEHMS   = r'(?P<hours>\d\d?)(?P<tsep>:|)(?P<minutes>\d\d)(?:(?P=tsep)(?P<seconds>\d\d(?:[.,]\d+)?))?'
+        self.RE_TIMEHMS2  = r'(?P<hours>(\d\d?))((?P<tsep>:|)(?P<minutes>(\d\d?))(?:(?P=tsep)(?P<seconds>\d\d?(?:[.,]\d+)?))?)?\s?(?P<meridian>(am|pm|a.m.|p.m.|a|p))'
+        self.RE_DATE      = r'(?P<date>\d+([/.\\]\d+)+)'
+        self.RE_DATE2     = r'[/.\\-]'
+        self.RE_DATE3     = r'(?P<date>((?P<mthname>(january|february|march|april|may|june|july|august|september|october|november|december))\s?((?P<day>\d\d?)(\s|rd|st|nd|th|,|$)+)?(?P<year>\d\d\d\d)?))'
+        self.RE_MONTH     = r'(?P<month>((?P<mthname>(january|february|march|april|may|june|july|august|september|october|november|december))(\s?(?P<year>(\d\d\d\d)))?))'
+        self.RE_WEEKDAY   = r'(?P<weekday>(monday|mon|tuesday|tue|wednesday|wed|thursday|thu|friday|saturday|sat|sunday|sun))'
+        self.RE_DAY       = r'(?P<day>(today|tomorrow|yesterday))'
+        self.RE_TIME      = r'\s*(?P<time>(morning|breakfast|noon|lunch|evening|midnight|tonight|dinner|night|now))' 
+        self.RE_REMAINING = r'\s+'
+
+          # Used to adjust the returned date before/after the source
+
+        self.Modifiers = { 'from':       1,
+                           'before':    -1,
+                           'after':      1,
+                           'ago':        1,
+                           'prior':     -1,
+                           'prev':      -1,
+                           'last':      -1,
+                           'next':       1,
+                           'this':       0,
+                           'previous':  -1,
+                           'in a':       2,
+                           'end of':     0,
+                           'eo':         0,
+                        }
+
+        self.Second =   1
+        self.Minute =  60 * self.Second
+        self.Hour   =  60 * self.Minute
+        self.Day    =  24 * self.Hour
+        self.Week   =   7 * self.Day
+        self.Month  =  30 * self.Day
+        self.Year   = 365 * self.Day
+
+        self.WeekDays = { 'monday':    0,
+                          'mon':       0,
+                          'tuesday':   1,
+                          'tue':       1,
+                          'wednesday': 2,
+                          'wed':       2,
+                          'thursday':  3,
+                          'thu':       3,
+                          'friday':    4,
+                          'fri':       4,
+                          'saturday':  5,
+                          'sat':       5,
+                          'sunday':    6,
+                          'sun':       6,
+                        }
+
+          # dictionary to allow for locale specific text
+          # NOTE: The keys are the localized values - the parsing
+          #       code will be using Target_Text using the values
+          #       extracted *from* the user's input
+
+        self.Target_Text = { 'datesep':   '-',
+                             'timesep':   ':',
+                             'day':       'day',
+                             'dy':        'dy',
+                             'd':         'd',
+                             'week':      'week',
+                             'wk':        'wk',
+                             'w':         'w',
+                             'month':     'month',
+                             'mth':       'mth',
+                             'year':      'year',
+                             'yr':        'yr',
+                             'y':         'y',
+                             'hour':      'hour',
+                             'hr':        'hr',
+                             'h':         'h',
+                             'minute':    'minute',
+                             'min':       'min',
+                             'm':         'm',
+                             'second':    'second',
+                             'sec':       'sec',
+                             's':         's',
+                             'now':       'now',
+                             'noon':      'noon',
+                             'morning':   'morning',
+                             'evening':   'evening',
+                             'breakfast': 'breakfast',
+                             'lunch':     'lunch',
+                             'dinner':    'dinner',
+                             'monday':    'monday',
+                             'mon':       'mon',
+                             'tuesday':   'tuesday',
+                             'tue':       'tue',
+                             'wednesday': 'wednesday',
+                             'wed':       'wed',
+                             'thursday':  'thursday',
+                             'thu':       'thu',
+                             'friday':    'friday',
+                             'fri':       'fri',
+                             'saturday':  'saturday',
+                             'sat':       'sat',
+                             'sunday':    'sunday',
+                             'sun':       'sun',
+                             'january':   'january',
+                             'jan':       'jan',
+                             'febuary':   'febuary',
+                             'feb':       'feb',
+                             'march':     'march',
+                             'mar':       'mar',
+                             'april':     'april',
+                             'apr':       'apr',
+                             'may':       'may',
+                             'may':       'may',
+                             'june':      'june',
+                             'jun':       'jun',
+                             'july':      'july',
+                             'jul':       'jul',
+                             'august':    'august',
+                             'aug':       'aug',
+                             'september': 'september',
+                             'sept':      'sep',
+                             'october':   'october',
+                             'oct':       'oct',
+                             'november':  'november',
+                             'nov':       'nov',
+                             'december':  'december',
+                             'dec':       'dec',
+                           }
+
+          # FIXME: there *has* to be a standard routine that does this
+
+        self.DOW_Text = [self.Target_Text['mon'],
+                         self.Target_Text['tue'],
+                         self.Target_Text['wed'],
+                         self.Target_Text['thu'],
+                         self.Target_Text['fri'],
+                         self.Target_Text['sat'],
+                         self.Target_Text['sun'],
+                        ]
+
+        self.DaysInMonthList = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
+
+        self.DaysInMonth = {}
+        self.DaysInMonth[self.Target_Text['january']]   = self.DaysInMonthList[0]
+        self.DaysInMonth[self.Target_Text['febuary']]   = self.DaysInMonthList[1]
+        self.DaysInMonth[self.Target_Text['march']]     = self.DaysInMonthList[2]
+        self.DaysInMonth[self.Target_Text['april']]     = self.DaysInMonthList[3]
+        self.DaysInMonth[self.Target_Text['may']]       = self.DaysInMonthList[4]
+        self.DaysInMonth[self.Target_Text['june']]      = self.DaysInMonthList[5]
+        self.DaysInMonth[self.Target_Text['july']]      = self.DaysInMonthList[6]
+        self.DaysInMonth[self.Target_Text['august']]    = self.DaysInMonthList[7]
+        self.DaysInMonth[self.Target_Text['september']] = self.DaysInMonthList[8]
+        self.DaysInMonth[self.Target_Text['october']]   = self.DaysInMonthList[9]
+        self.DaysInMonth[self.Target_Text['november']]  = self.DaysInMonthList[10]
+        self.DaysInMonth[self.Target_Text['december']]  = self.DaysInMonthList[11]
+
+        self.Month_Text = [ self.Target_Text['january'],
+                            self.Target_Text['febuary'],
+                            self.Target_Text['march'],
+                            self.Target_Text['april'],
+                            self.Target_Text['may'],
+                            self.Target_Text['june'],
+                            self.Target_Text['july'],
+                            self.Target_Text['august'],
+                            self.Target_Text['september'],
+                            self.Target_Text['october'],
+                            self.Target_Text['november'],
+                            self.Target_Text['december'],
+                          ]
+
+
+        self.MthNames = { 'january':    1,
+                          'february':   2,
+                          'march':      3,
+                          'april':      4,
+                          'may' :       5,
+                          'june':       6,
+                          'july':       7,
+                          'august':     8,
+                          'september':  9,
+                          'october':   10,
+                          'november':  11,
+                          'december':  12,
+                        }
+
+
+
+          # This looks hokey - but it is a nice simple way to get
+          # the proper unit value and it has the advantage that
+          # later I can morph it into something localized.
+          # Any trailing s will be removed before lookup.
+
+        self.Units = {}
+        self.Units[self.Target_Text['second']] = self.Second
+        self.Units[self.Target_Text['sec']]    = self.Second
+        self.Units[self.Target_Text['s']]      = self.Second
+        self.Units[self.Target_Text['minute']] = self.Minute
+        self.Units[self.Target_Text['min']]    = self.Minute
+        self.Units[self.Target_Text['m']]      = self.Minute
+        self.Units[self.Target_Text['hour']]   = self.Hour
+        self.Units[self.Target_Text['hr']]     = self.Hour
+        self.Units[self.Target_Text['h']]      = self.Hour
+        self.Units[self.Target_Text['day']]    = self.Day
+        self.Units[self.Target_Text['dy']]     = self.Day
+        self.Units[self.Target_Text['d']]      = self.Day
+        self.Units[self.Target_Text['week']]   = self.Week
+        self.Units[self.Target_Text['wk']]     = self.Week
+        self.Units[self.Target_Text['w']]      = self.Week
+        self.Units[self.Target_Text['month']]  = self.Month
+        self.Units[self.Target_Text['mth']]    = self.Month
+        self.Units[self.Target_Text['year']]   = self.Year
+        self.Units[self.Target_Text['yr']]     = self.Year
+        self.Units[self.Target_Text['y']]      = self.Year
+
+        self.Units_Text = { 'one':        1,
+                            'two':        2,
+                            'three':      3,
+                            'four':       4,
+                            'five':       5,
+                            'six':        6,
+                            'seven':      7,
+                            'eight':      8,
+                            'nine':       9,
+                            'ten':       10,
+                            'eleven':    11,
+                            'twelve':    12,
+                            'thirteen':  13,
+                            'fourteen':  14,
+                            'fifteen':   15,
+                            'sixteen':   16,
+                            'seventeen': 17,
+                            'eighteen':  18,
+                            'nineteen':  19,
+                            'twenty':    20,
+                            'thirty':    30,
+                            'forty':     40,
+                            'fifty':     50,
+                            'sixty':     60,
+                            'seventy':   70,
+                            'eighty':    80,
+                            'ninety':    90,
+                            'half':      0.5,
+                            'quarter':  0.25,
+                         }
+