~ubuntu-branches/ubuntu/natty/moin/natty-updates

« back to all changes in this revision

Viewing changes to MoinMoin/support/parsedatetime/parsedatetime.py

  • Committer: Bazaar Package Importer
  • Author(s): Jonas Smedegaard
  • Date: 2008-06-22 21:17:13 UTC
  • mto: This revision was merged to the branch mainline in revision 18.
  • Revision ID: james.westby@ubuntu.com-20080622211713-inlv5k4eifxckelr
ImportĀ upstreamĀ versionĀ 1.7.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python
2
 
 
3
 
"""
4
 
Parse human-readable date/time text.
5
 
"""
6
 
 
7
 
__license__ = """
8
 
Copyright (c) 2004-2008 Mike Taylor
9
 
Copyright (c) 2006-2008 Darshana Chhajed
10
 
All rights reserved.
11
 
 
12
 
Licensed under the Apache License, Version 2.0 (the "License");
13
 
you may not use this file except in compliance with the License.
14
 
You may obtain a copy of the License at
15
 
 
16
 
   http://www.apache.org/licenses/LICENSE-2.0
17
 
 
18
 
Unless required by applicable law or agreed to in writing, software
19
 
distributed under the License is distributed on an "AS IS" BASIS,
20
 
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
 
See the License for the specific language governing permissions and
22
 
limitations under the License.
23
 
"""
24
 
 
25
 
_debug = False
26
 
 
27
 
 
28
 
import re
29
 
import time
30
 
import datetime
31
 
import rfc822
32
 
import parsedatetime_consts
33
 
 
34
 
 
35
 
# Copied from feedparser.py
36
 
# Universal Feedparser
37
 
# Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
38
 
# Originally a def inside of _parse_date_w3dtf()
39
 
def _extract_date(m):
40
 
    year = int(m.group('year'))
41
 
    if year < 100:
42
 
        year = 100 * int(time.gmtime()[0] / 100) + int(year)
43
 
    if year < 1000:
44
 
        return 0, 0, 0
45
 
    julian = m.group('julian')
46
 
    if julian:
47
 
        julian = int(julian)
48
 
        month = julian / 30 + 1
49
 
        day = julian % 30 + 1
50
 
        jday = None
51
 
        while jday != julian:
52
 
            t = time.mktime((year, month, day, 0, 0, 0, 0, 0, 0))
53
 
            jday = time.gmtime(t)[-2]
54
 
            diff = abs(jday - julian)
55
 
            if jday > julian:
56
 
                if diff < day:
57
 
                    day = day - diff
58
 
                else:
59
 
                    month = month - 1
60
 
                    day = 31
61
 
            elif jday < julian:
62
 
                if day + diff < 28:
63
 
                    day = day + diff
64
 
                else:
65
 
                    month = month + 1
66
 
        return year, month, day
67
 
    month = m.group('month')
68
 
    day = 1
69
 
    if month is None:
70
 
        month = 1
71
 
    else:
72
 
        month = int(month)
73
 
        day = m.group('day')
74
 
        if day:
75
 
            day = int(day)
76
 
        else:
77
 
            day = 1
78
 
    return year, month, day
79
 
 
80
 
# Copied from feedparser.py
81
 
# Universal Feedparser
82
 
# Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
83
 
# Originally a def inside of _parse_date_w3dtf()
84
 
def _extract_time(m):
85
 
    if not m:
86
 
        return 0, 0, 0
87
 
    hours = m.group('hours')
88
 
    if not hours:
89
 
        return 0, 0, 0
90
 
    hours = int(hours)
91
 
    minutes = int(m.group('minutes'))
92
 
    seconds = m.group('seconds')
93
 
    if seconds:
94
 
        seconds = int(seconds)
95
 
    else:
96
 
        seconds = 0
97
 
    return hours, minutes, seconds
98
 
 
99
 
 
100
 
# Copied from feedparser.py
101
 
# Universal Feedparser
102
 
# Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
103
 
# Modified to return a tuple instead of mktime
104
 
#
105
 
# Original comment:
106
 
#   W3DTF-style date parsing adapted from PyXML xml.utils.iso8601, written by
107
 
#   Drake and licensed under the Python license.  Removed all range checking
108
 
#   for month, day, hour, minute, and second, since mktime will normalize
109
 
#   these later
110
 
def _parse_date_w3dtf(dateString):
111
 
    # the __extract_date and __extract_time methods were
112
 
    # copied-out so they could be used by my code --bear
113
 
    def __extract_tzd(m):
114
 
        '''Return the Time Zone Designator as an offset in seconds from UTC.'''
115
 
        if not m:
116
 
            return 0
117
 
        tzd = m.group('tzd')
118
 
        if not tzd:
119
 
            return 0
120
 
        if tzd == 'Z':
121
 
            return 0
122
 
        hours = int(m.group('tzdhours'))
123
 
        minutes = m.group('tzdminutes')
124
 
        if minutes:
125
 
            minutes = int(minutes)
126
 
        else:
127
 
            minutes = 0
128
 
        offset = (hours*60 + minutes) * 60
129
 
        if tzd[0] == '+':
130
 
            return -offset
131
 
        return offset
132
 
 
133
 
    __date_re = ('(?P<year>\d\d\d\d)'
134
 
                 '(?:(?P<dsep>-|)'
135
 
                 '(?:(?P<julian>\d\d\d)'
136
 
                 '|(?P<month>\d\d)(?:(?P=dsep)(?P<day>\d\d))?))?')
137
 
    __tzd_re = '(?P<tzd>[-+](?P<tzdhours>\d\d)(?::?(?P<tzdminutes>\d\d))|Z)'
138
 
    __tzd_rx = re.compile(__tzd_re)
139
 
    __time_re = ('(?P<hours>\d\d)(?P<tsep>:|)(?P<minutes>\d\d)'
140
 
                 '(?:(?P=tsep)(?P<seconds>\d\d(?:[.,]\d+)?))?'
141
 
                 + __tzd_re)
142
 
    __datetime_re = '%s(?:T%s)?' % (__date_re, __time_re)
143
 
    __datetime_rx = re.compile(__datetime_re)
144
 
    m = __datetime_rx.match(dateString)
145
 
    if (m is None) or (m.group() != dateString): return
146
 
    return _extract_date(m) + _extract_time(m) + (0, 0, 0)
147
 
 
148
 
 
149
 
# Copied from feedparser.py
150
 
# Universal Feedparser
151
 
# Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
152
 
# Modified to return a tuple instead of mktime
153
 
#
154
 
def _parse_date_rfc822(dateString):
155
 
    '''Parse an RFC822, RFC1123, RFC2822, or asctime-style date'''
156
 
    data = dateString.split()
157
 
    if data[0][-1] in (',', '.') or data[0].lower() in rfc822._daynames:
158
 
        del data[0]
159
 
    if len(data) == 4:
160
 
        s = data[3]
161
 
        i = s.find('+')
162
 
        if i > 0:
163
 
            data[3:] = [s[:i], s[i+1:]]
164
 
        else:
165
 
            data.append('')
166
 
        dateString = " ".join(data)
167
 
    if len(data) < 5:
168
 
        dateString += ' 00:00:00 GMT'
169
 
    return rfc822.parsedate_tz(dateString)
170
 
 
171
 
# rfc822.py defines several time zones, but we define some extra ones.
172
 
# 'ET' is equivalent to 'EST', etc.
173
 
_additional_timezones = {'AT': -400, 'ET': -500,
174
 
                         'CT': -600, 'MT': -700,
175
 
                         'PT': -800}
176
 
rfc822._timezones.update(_additional_timezones)
177
 
 
178
 
 
179
 
class Calendar:
180
 
    """
181
 
    A collection of routines to input, parse and manipulate date and times.
182
 
    The text can either be 'normal' date values or it can be human readable.
183
 
    """
184
 
 
185
 
    def __init__(self, constants=None):
186
 
        """
187
 
        Default constructor for the L{Calendar} class.
188
 
 
189
 
        @type  constants: object
190
 
        @param constants: Instance of the class L{parsedatetime_consts.Constants}
191
 
 
192
 
        @rtype:  object
193
 
        @return: L{Calendar} instance
194
 
        """
195
 
          # if a constants reference is not included, use default
196
 
        if constants is None:
197
 
            self.ptc = parsedatetime_consts.Constants()
198
 
        else:
199
 
            self.ptc = constants
200
 
 
201
 
        self.weekdyFlag    = False  # monday/tuesday/...
202
 
        self.dateStdFlag   = False  # 07/21/06
203
 
        self.dateStrFlag   = False  # July 21st, 2006
204
 
        self.timeStdFlag   = False  # 5:50 
205
 
        self.meridianFlag  = False  # am/pm
206
 
        self.dayStrFlag    = False  # tomorrow/yesterday/today/..
207
 
        self.timeStrFlag   = False  # lunch/noon/breakfast/...
208
 
        self.modifierFlag  = False  # after/before/prev/next/..
209
 
        self.modifier2Flag = False  # after/before/prev/next/..
210
 
        self.unitsFlag     = False  # hrs/weeks/yrs/min/..
211
 
        self.qunitsFlag    = False  # h/m/t/d..
212
 
 
213
 
        self.timeFlag      = 0
214
 
        self.dateFlag      = 0
215
 
 
216
 
 
217
 
    def _convertUnitAsWords(self, unitText):
218
 
        """
219
 
        Converts text units into their number value
220
 
 
221
 
        Five = 5
222
 
        Twenty Five = 25
223
 
        Two hundred twenty five = 225
224
 
        Two thousand and twenty five = 2025
225
 
        Two thousand twenty five = 2025
226
 
 
227
 
        @type  unitText: string
228
 
        @param unitText: number text to convert
229
 
 
230
 
        @rtype:  integer
231
 
        @return: numerical value of unitText
232
 
        """
233
 
        # TODO: implement this
234
 
        pass
235
 
 
236
 
 
237
 
    def _buildTime(self, source, quantity, modifier, units):
238
 
        """
239
 
        Take C{quantity}, C{modifier} and C{unit} strings and convert them into values.
240
 
        After converting, calcuate the time and return the adjusted sourceTime.
241
 
 
242
 
        @type  source:   time
243
 
        @param source:   time to use as the base (or source)
244
 
        @type  quantity: string
245
 
        @param quantity: quantity string
246
 
        @type  modifier: string
247
 
        @param modifier: how quantity and units modify the source time
248
 
        @type  units:    string
249
 
        @param units:    unit of the quantity (i.e. hours, days, months, etc)
250
 
 
251
 
        @rtype:  struct_time
252
 
        @return: C{struct_time} of the calculated time
253
 
        """
254
 
        if _debug:
255
 
            print '_buildTime: [%s][%s][%s]' % (quantity, modifier, units)
256
 
 
257
 
        if source is None:
258
 
            source = time.localtime()
259
 
 
260
 
        if quantity is None:
261
 
            quantity = ''
262
 
        else:
263
 
            quantity = quantity.strip()
264
 
 
265
 
        if len(quantity) == 0:
266
 
            qty = 1
267
 
        else:
268
 
            try:
269
 
                qty = int(quantity)
270
 
            except ValueError:
271
 
                qty = 0
272
 
 
273
 
        if modifier in self.ptc.Modifiers:
274
 
            qty = qty * self.ptc.Modifiers[modifier]
275
 
 
276
 
            if units is None or units == '':
277
 
                units = 'dy'
278
 
 
279
 
        # plurals are handled by regex's (could be a bug tho)
280
 
 
281
 
        (yr, mth, dy, hr, mn, sec, _, _, _) = source
282
 
 
283
 
        start  = datetime.datetime(yr, mth, dy, hr, mn, sec)
284
 
        target = start
285
 
 
286
 
        if units.startswith('y'):
287
 
            target        = self.inc(start, year=qty)
288
 
            self.dateFlag = 1
289
 
        elif units.endswith('th') or units.endswith('ths'):
290
 
            target        = self.inc(start, month=qty)
291
 
            self.dateFlag = 1
292
 
        else:
293
 
            if units.startswith('d'):
294
 
                target        = start + datetime.timedelta(days=qty)
295
 
                self.dateFlag = 1
296
 
            elif units.startswith('h'):
297
 
                target        = start + datetime.timedelta(hours=qty)
298
 
                self.timeFlag = 2
299
 
            elif units.startswith('m'):
300
 
                target        = start + datetime.timedelta(minutes=qty)
301
 
                self.timeFlag = 2
302
 
            elif units.startswith('s'):
303
 
                target        = start + datetime.timedelta(seconds=qty)
304
 
                self.timeFlag = 2
305
 
            elif units.startswith('w'):
306
 
                target        = start + datetime.timedelta(weeks=qty)
307
 
                self.dateFlag = 1
308
 
 
309
 
        return target.timetuple()
310
 
 
311
 
 
312
 
    def parseDate(self, dateString):
313
 
        """
314
 
        Parse short-form date strings::
315
 
 
316
 
            '05/28/2006' or '04.21'
317
 
 
318
 
        @type  dateString: string
319
 
        @param dateString: text to convert to a C{datetime}
320
 
 
321
 
        @rtype:  struct_time
322
 
        @return: calculated C{struct_time} value of dateString
323
 
        """
324
 
        yr, mth, dy, hr, mn, sec, wd, yd, isdst = time.localtime()
325
 
 
326
 
        # values pulled from regex's will be stored here and later
327
 
        # assigned to mth, dy, yr based on information from the locale
328
 
        # -1 is used as the marker value because we want zero values
329
 
        # to be passed thru so they can be flagged as errors later
330
 
        v1 = -1
331
 
        v2 = -1
332
 
        v3 = -1
333
 
 
334
 
        s = dateString
335
 
        m = self.ptc.CRE_DATE2.search(s)
336
 
        if m is not None:
337
 
            index = m.start()
338
 
            v1    = int(s[:index])
339
 
            s     = s[index + 1:]
340
 
 
341
 
        m = self.ptc.CRE_DATE2.search(s)
342
 
        if m is not None:
343
 
            index = m.start()
344
 
            v2    = int(s[:index])
345
 
            v3    = int(s[index + 1:])
346
 
        else:
347
 
            v2 = int(s.strip())
348
 
 
349
 
        v = [ v1, v2, v3 ]
350
 
        d = { 'm': mth, 'd': dy, 'y': yr }
351
 
 
352
 
        for i in range(0, 3):
353
 
            n = v[i]
354
 
            c = self.ptc.dp_order[i]
355
 
            if n >= 0:
356
 
                d[c] = n
357
 
 
358
 
        # if the year is not specified and the date has already
359
 
        # passed, increment the year
360
 
        if v3 == -1 and ((mth > d['m']) or (mth == d['m'] and dy > d['d'])):
361
 
            yr = d['y'] + 1
362
 
        else:
363
 
            yr  = d['y']
364
 
 
365
 
        mth = d['m']
366
 
        dy  = d['d']
367
 
 
368
 
        # birthday epoch constraint
369
 
        if yr < self.ptc.BirthdayEpoch:
370
 
            yr += 2000
371
 
        elif yr < 100:
372
 
            yr += 1900
373
 
 
374
 
        if _debug:
375
 
            print 'parseDate: ', yr, mth, dy, self.ptc.daysInMonth(mth, yr)
376
 
 
377
 
        if (mth > 0 and mth <= 12) and \
378
 
           (dy > 0 and dy <= self.ptc.daysInMonth(mth, yr)):
379
 
            sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
380
 
        else:
381
 
            self.dateFlag = 0
382
 
            self.timeFlag = 0
383
 
            sourceTime    = time.localtime() # return current time if date
384
 
                                             # string is invalid
385
 
 
386
 
        return sourceTime
387
 
 
388
 
 
389
 
    def parseDateText(self, dateString):
390
 
        """
391
 
        Parse long-form date strings::
392
 
 
393
 
            'May 31st, 2006'
394
 
            'Jan 1st'
395
 
            'July 2006'
396
 
 
397
 
        @type  dateString: string
398
 
        @param dateString: text to convert to a datetime
399
 
 
400
 
        @rtype:  struct_time
401
 
        @return: calculated C{struct_time} value of dateString
402
 
        """
403
 
        yr, mth, dy, hr, mn, sec, wd, yd, isdst = time.localtime()
404
 
 
405
 
        currentMth = mth
406
 
        currentDy  = dy
407
 
 
408
 
        s   = dateString.lower()
409
 
        m   = self.ptc.CRE_DATE3.search(s)
410
 
        mth = m.group('mthname')
411
 
        mth = self.ptc.MonthOffsets[mth]
412
 
 
413
 
        if m.group('day') !=  None:
414
 
            dy = int(m.group('day'))
415
 
        else:
416
 
            dy = 1
417
 
 
418
 
        if m.group('year') !=  None:
419
 
            yr = int(m.group('year'))
420
 
 
421
 
            # birthday epoch constraint
422
 
            if yr < self.ptc.BirthdayEpoch:
423
 
                yr += 2000
424
 
            elif yr < 100:
425
 
                yr += 1900
426
 
 
427
 
        elif (mth < currentMth) or (mth == currentMth and dy < currentDy):
428
 
            # if that day and month have already passed in this year,
429
 
            # then increment the year by 1
430
 
            yr += 1
431
 
 
432
 
        if dy > 0 and dy <= self.ptc.daysInMonth(mth, yr):
433
 
            sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
434
 
        else:
435
 
            # Return current time if date string is invalid
436
 
            self.dateFlag = 0
437
 
            self.timeFlag = 0
438
 
            sourceTime    = time.localtime()
439
 
 
440
 
        return sourceTime
441
 
 
442
 
 
443
 
    def evalRanges(self, datetimeString, sourceTime=None):
444
 
        """
445
 
        Evaluate the C{datetimeString} text and determine if
446
 
        it represents a date or time range.
447
 
 
448
 
        @type  datetimeString: string
449
 
        @param datetimeString: datetime text to evaluate
450
 
        @type  sourceTime:     struct_time
451
 
        @param sourceTime:     C{struct_time} value to use as the base
452
 
 
453
 
        @rtype:  tuple
454
 
        @return: tuple of: start datetime, end datetime and the invalid flag
455
 
        """
456
 
        startTime = ''
457
 
        endTime   = ''
458
 
        startDate = ''
459
 
        endDate   = ''
460
 
        rangeFlag = 0
461
 
 
462
 
        s = datetimeString.strip().lower()
463
 
 
464
 
        if self.ptc.rangeSep in s:
465
 
            s = s.replace(self.ptc.rangeSep, ' %s ' % self.ptc.rangeSep)
466
 
            s = s.replace('  ', ' ')
467
 
 
468
 
        m = self.ptc.CRE_TIMERNG1.search(s)
469
 
        if m is not None:
470
 
            rangeFlag = 1
471
 
        else:
472
 
            m = self.ptc.CRE_TIMERNG2.search(s)
473
 
            if m is not None:
474
 
                rangeFlag = 2
475
 
            else:
476
 
                m = self.ptc.CRE_TIMERNG4.search(s)
477
 
                if m is not None:
478
 
                    rangeFlag = 7
479
 
                else:
480
 
                    m = self.ptc.CRE_TIMERNG3.search(s)
481
 
                    if m is not None:
482
 
                        rangeFlag = 3
483
 
                    else:
484
 
                        m = self.ptc.CRE_DATERNG1.search(s)
485
 
                        if m is not None:
486
 
                            rangeFlag = 4
487
 
                        else:
488
 
                            m = self.ptc.CRE_DATERNG2.search(s)
489
 
                            if m is not None:
490
 
                                rangeFlag = 5
491
 
                            else:
492
 
                                m = self.ptc.CRE_DATERNG3.search(s)
493
 
                                if m is not None:
494
 
                                    rangeFlag = 6
495
 
 
496
 
        if _debug:
497
 
            print 'evalRanges: rangeFlag =', rangeFlag, '[%s]' % s
498
 
 
499
 
        if m is not None:
500
 
            if (m.group() != s):
501
 
                # capture remaining string
502
 
                parseStr = m.group()
503
 
                chunk1   = s[:m.start()]
504
 
                chunk2   = s[m.end():]
505
 
                s        = '%s %s' % (chunk1, chunk2)
506
 
                flag     = 1
507
 
 
508
 
                sourceTime, flag = self.parse(s, sourceTime)
509
 
 
510
 
                if flag == 0:
511
 
                    sourceTime = None
512
 
            else:
513
 
                parseStr = s
514
 
 
515
 
        if rangeFlag == 1:
516
 
            m                = re.search(self.ptc.rangeSep, parseStr)
517
 
            startTime, sflag = self.parse((parseStr[:m.start()]),       sourceTime)
518
 
            endTime, eflag   = self.parse((parseStr[(m.start() + 1):]), sourceTime)
519
 
 
520
 
            if (eflag != 0)  and (sflag != 0):
521
 
                return (startTime, endTime, 2)
522
 
 
523
 
        elif rangeFlag == 2:
524
 
            m                = re.search(self.ptc.rangeSep, parseStr)
525
 
            startTime, sflag = self.parse((parseStr[:m.start()]),       sourceTime)
526
 
            endTime, eflag   = self.parse((parseStr[(m.start() + 1):]), sourceTime)
527
 
 
528
 
            if (eflag != 0)  and (sflag != 0):
529
 
                return (startTime, endTime, 2)
530
 
 
531
 
        elif rangeFlag == 3 or rangeFlag == 7:
532
 
            m = re.search(self.ptc.rangeSep, parseStr)
533
 
            # capturing the meridian from the end time
534
 
            if self.ptc.usesMeridian:
535
 
                ampm = re.search(self.ptc.am[0], parseStr)
536
 
 
537
 
                # appending the meridian to the start time
538
 
                if ampm is not None:
539
 
                    startTime, sflag = self.parse((parseStr[:m.start()] + self.ptc.meridian[0]), sourceTime)
540
 
                else:
541
 
                    startTime, sflag = self.parse((parseStr[:m.start()] + self.ptc.meridian[1]), sourceTime)
542
 
            else:
543
 
                startTime, sflag = self.parse((parseStr[:m.start()]), sourceTime)
544
 
 
545
 
            endTime, eflag = self.parse(parseStr[(m.start() + 1):], sourceTime)
546
 
 
547
 
            if (eflag != 0)  and (sflag != 0):
548
 
                return (startTime, endTime, 2)
549
 
 
550
 
        elif rangeFlag == 4:
551
 
            m                = re.search(self.ptc.rangeSep, parseStr)
552
 
            startDate, sflag = self.parse((parseStr[:m.start()]),       sourceTime)
553
 
            endDate, eflag   = self.parse((parseStr[(m.start() + 1):]), sourceTime)
554
 
 
555
 
            if (eflag != 0)  and (sflag != 0):
556
 
                return (startDate, endDate, 1)
557
 
 
558
 
        elif rangeFlag == 5:
559
 
            m       = re.search(self.ptc.rangeSep, parseStr)
560
 
            endDate = parseStr[(m.start() + 1):]
561
 
 
562
 
            # capturing the year from the end date
563
 
            date    = self.ptc.CRE_DATE3.search(endDate)
564
 
            endYear = date.group('year')
565
 
 
566
 
            # appending the year to the start date if the start date
567
 
            # does not have year information and the end date does.
568
 
            # eg : "Aug 21 - Sep 4, 2007"
569
 
            if endYear is not None:
570
 
                startDate = (parseStr[:m.start()]).strip()
571
 
                date      = self.ptc.CRE_DATE3.search(startDate)
572
 
                startYear = date.group('year')
573
 
 
574
 
                if startYear is None:
575
 
                    startDate = startDate + ', ' + endYear
576
 
            else:
577
 
                startDate = parseStr[:m.start()]
578
 
 
579
 
            startDate, sflag = self.parse(startDate, sourceTime)
580
 
            endDate, eflag   = self.parse(endDate, sourceTime)
581
 
 
582
 
            if (eflag != 0)  and (sflag != 0):
583
 
                return (startDate, endDate, 1)
584
 
 
585
 
        elif rangeFlag == 6:
586
 
            m = re.search(self.ptc.rangeSep, parseStr)
587
 
 
588
 
            startDate = parseStr[:m.start()]
589
 
 
590
 
            # capturing the month from the start date
591
 
            mth = self.ptc.CRE_DATE3.search(startDate)
592
 
            mth = mth.group('mthname')
593
 
 
594
 
            # appending the month name to the end date
595
 
            endDate = mth + parseStr[(m.start() + 1):]
596
 
 
597
 
            startDate, sflag = self.parse(startDate, sourceTime)
598
 
            endDate, eflag   = self.parse(endDate, sourceTime)
599
 
 
600
 
            if (eflag != 0)  and (sflag != 0):
601
 
                return (startDate, endDate, 1)
602
 
        else:
603
 
            # if range is not found
604
 
            sourceTime = time.localtime()
605
 
 
606
 
            return (sourceTime, sourceTime, 0)
607
 
 
608
 
 
609
 
    def _CalculateDOWDelta(self, wd, wkdy, offset, style, currentDayStyle):
610
 
        """
611
 
        Based on the C{style} and C{currentDayStyle} determine what
612
 
        day-of-week value is to be returned.
613
 
 
614
 
        @type  wd:              integer
615
 
        @param wd:              day-of-week value for the current day
616
 
        @type  wkdy:            integer
617
 
        @param wkdy:            day-of-week value for the parsed day
618
 
        @type  offset:          integer
619
 
        @param offset:          offset direction for any modifiers (-1, 0, 1)
620
 
        @type  style:           integer
621
 
        @param style:           normally the value set in C{Constants.DOWParseStyle}
622
 
        @type  currentDayStyle: integer
623
 
        @param currentDayStyle: normally the value set in C{Constants.CurrentDOWParseStyle}
624
 
 
625
 
        @rtype:  integer
626
 
        @return: calculated day-of-week
627
 
        """
628
 
        if offset == 1:
629
 
            # modifier is indicating future week eg: "next".
630
 
            # DOW is calculated as DOW of next week
631
 
            diff = 7 - wd + wkdy
632
 
 
633
 
        elif offset == -1:
634
 
            # modifier is indicating past week eg: "last","previous"
635
 
            # DOW is calculated as DOW of previous week
636
 
            diff = wkdy - wd - 7
637
 
 
638
 
        elif offset == 0:
639
 
            # modifier is indiacting current week eg: "this"
640
 
            # DOW is calculated as DOW of this week
641
 
            diff = wkdy - wd
642
 
 
643
 
        elif offset == 2:
644
 
            # no modifier is present.
645
 
            # i.e. string to be parsed is just DOW
646
 
            if style == 1:
647
 
                # next occurance of the DOW is calculated
648
 
                if currentDayStyle == True:
649
 
                    if wkdy >= wd:
650
 
                        diff = wkdy - wd
651
 
                    else:
652
 
                        diff = 7 - wd + wkdy
653
 
                else:
654
 
                    if wkdy > wd:
655
 
                        diff = wkdy - wd
656
 
                    else:
657
 
                        diff = 7 - wd + wkdy
658
 
 
659
 
            elif style == -1:
660
 
                # last occurance of the DOW is calculated
661
 
                if currentDayStyle == True:
662
 
                    if wkdy <= wd:
663
 
                        diff = wkdy - wd
664
 
                    else:
665
 
                        diff = wkdy - wd - 7
666
 
                else:
667
 
                    if wkdy < wd:
668
 
                        diff = wkdy - wd
669
 
                    else:
670
 
                        diff = wkdy - wd - 7
671
 
            else:
672
 
                # occurance of the DOW in the current week is calculated
673
 
                diff = wkdy - wd
674
 
 
675
 
        if _debug:
676
 
            print "wd %s, wkdy %s, offset %d, style %d\n" % (wd, wkdy, offset, style)
677
 
 
678
 
        return diff
679
 
 
680
 
 
681
 
    def _evalModifier(self, modifier, chunk1, chunk2, sourceTime):
682
 
        """
683
 
        Evaluate the C{modifier} string and following text (passed in
684
 
        as C{chunk1} and C{chunk2}) and if they match any known modifiers
685
 
        calculate the delta and apply it to C{sourceTime}.
686
 
 
687
 
        @type  modifier:   string
688
 
        @param modifier:   modifier text to apply to sourceTime
689
 
        @type  chunk1:     string
690
 
        @param chunk1:     first text chunk that followed modifier (if any)
691
 
        @type  chunk2:     string
692
 
        @param chunk2:     second text chunk that followed modifier (if any)
693
 
        @type  sourceTime: struct_time
694
 
        @param sourceTime: C{struct_time} value to use as the base
695
 
 
696
 
        @rtype:  tuple
697
 
        @return: tuple of: remaining text and the modified sourceTime
698
 
        """
699
 
        offset = self.ptc.Modifiers[modifier]
700
 
 
701
 
        if sourceTime is not None:
702
 
            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
703
 
        else:
704
 
            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = time.localtime()
705
 
 
706
 
        # capture the units after the modifier and the remaining
707
 
        # string after the unit
708
 
        m = self.ptc.CRE_REMAINING.search(chunk2)
709
 
        if m is not None:
710
 
            index  = m.start() + 1
711
 
            unit   = chunk2[:m.start()]
712
 
            chunk2 = chunk2[index:]
713
 
        else:
714
 
            unit   = chunk2
715
 
            chunk2 = ''
716
 
 
717
 
        flag = False
718
 
 
719
 
        if unit == 'month' or \
720
 
           unit == 'mth' or \
721
 
           unit == 'm':
722
 
            if offset == 0:
723
 
                dy         = self.ptc.daysInMonth(mth, yr)
724
 
                sourceTime = (yr, mth, dy, 9, 0, 0, wd, yd, isdst)
725
 
            elif offset == 2:
726
 
                # if day is the last day of the month, calculate the last day
727
 
                # of the next month
728
 
                if dy == self.ptc.daysInMonth(mth, yr):
729
 
                    dy = self.ptc.daysInMonth(mth + 1, yr)
730
 
 
731
 
                start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
732
 
                target     = self.inc(start, month=1)
733
 
                sourceTime = target.timetuple()
734
 
            else:
735
 
                start      = datetime.datetime(yr, mth, 1, 9, 0, 0)
736
 
                target     = self.inc(start, month=offset)
737
 
                sourceTime = target.timetuple()
738
 
 
739
 
            flag = True
740
 
            self.dateFlag = 1
741
 
 
742
 
        if unit == 'week' or \
743
 
             unit == 'wk' or \
744
 
             unit == 'w':
745
 
            if offset == 0:
746
 
                start      = datetime.datetime(yr, mth, dy, 17, 0, 0)
747
 
                target     = start + datetime.timedelta(days=(4 - wd))
748
 
                sourceTime = target.timetuple()
749
 
            elif offset == 2:
750
 
                start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
751
 
                target     = start + datetime.timedelta(days=7)
752
 
                sourceTime = target.timetuple()
753
 
            else:
754
 
                return self._evalModifier(modifier, chunk1, "monday " + chunk2, sourceTime)
755
 
 
756
 
            flag          = True
757
 
            self.dateFlag = 1
758
 
 
759
 
        if unit == 'day' or \
760
 
            unit == 'dy' or \
761
 
            unit == 'd':
762
 
            if offset == 0:
763
 
                sourceTime    = (yr, mth, dy, 17, 0, 0, wd, yd, isdst)
764
 
                self.timeFlag = 2
765
 
            elif offset == 2:
766
 
                start      = datetime.datetime(yr, mth, dy, hr, mn, sec)
767
 
                target     = start + datetime.timedelta(days=1)
768
 
                sourceTime = target.timetuple()
769
 
            else:
770
 
                start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
771
 
                target     = start + datetime.timedelta(days=offset)
772
 
                sourceTime = target.timetuple()
773
 
 
774
 
            flag          = True
775
 
            self.dateFlag = 1
776
 
 
777
 
        if unit == 'hour' or \
778
 
           unit == 'hr':
779
 
            if offset == 0:
780
 
                sourceTime = (yr, mth, dy, hr, 0, 0, wd, yd, isdst)
781
 
            else:
782
 
                start      = datetime.datetime(yr, mth, dy, hr, 0, 0)
783
 
                target     = start + datetime.timedelta(hours=offset)
784
 
                sourceTime = target.timetuple()
785
 
 
786
 
            flag          = True
787
 
            self.timeFlag = 2
788
 
 
789
 
        if unit == 'year' or \
790
 
             unit == 'yr' or \
791
 
             unit == 'y':
792
 
            if offset == 0:
793
 
                sourceTime = (yr, 12, 31, hr, mn, sec, wd, yd, isdst)
794
 
            elif offset == 2:
795
 
                sourceTime = (yr + 1, mth, dy, hr, mn, sec, wd, yd, isdst)
796
 
            else:
797
 
                sourceTime = (yr + offset, 1, 1, 9, 0, 0, wd, yd, isdst)
798
 
 
799
 
            flag          = True
800
 
            self.dateFlag = 1
801
 
 
802
 
        if flag == False:
803
 
            m = self.ptc.CRE_WEEKDAY.match(unit)
804
 
            if m is not None:
805
 
                wkdy          = m.group()
806
 
                self.dateFlag = 1
807
 
 
808
 
                if modifier == 'eod':
809
 
                    # Calculate the  upcoming weekday
810
 
                    self.modifierFlag = False
811
 
                    (sourceTime, _)   = self.parse(wkdy, sourceTime)
812
 
                    sources           = self.ptc.buildSources(sourceTime)
813
 
                    self.timeFlag     = 2
814
 
 
815
 
                    if modifier in sources:
816
 
                        sourceTime = sources[modifier]
817
 
 
818
 
                else:
819
 
                    wkdy       = self.ptc.WeekdayOffsets[wkdy]
820
 
                    diff       = self._CalculateDOWDelta(wd, wkdy, offset,
821
 
                                                         self.ptc.DOWParseStyle,
822
 
                                                         self.ptc.CurrentDOWParseStyle)
823
 
                    start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
824
 
                    target     = start + datetime.timedelta(days=diff)
825
 
                    sourceTime = target.timetuple()
826
 
 
827
 
                flag          = True
828
 
                self.dateFlag = 1
829
 
 
830
 
        if not flag:
831
 
            m = self.ptc.CRE_TIME.match(unit)
832
 
            if m is not None:
833
 
                self.modifierFlag = False
834
 
                (yr, mth, dy, hr, mn, sec, wd, yd, isdst), _ = self.parse(unit)
835
 
 
836
 
                start      = datetime.datetime(yr, mth, dy, hr, mn, sec)
837
 
                target     = start + datetime.timedelta(days=offset)
838
 
                sourceTime = target.timetuple()
839
 
                flag       = True
840
 
            else:
841
 
                self.modifierFlag = False
842
 
 
843
 
                # check if the remaining text is parsable and if so,
844
 
                # use it as the base time for the modifier source time
845
 
                t, flag2 = self.parse('%s %s' % (chunk1, unit), sourceTime)
846
 
 
847
 
                if flag2 != 0:
848
 
                    sourceTime = t
849
 
 
850
 
                sources = self.ptc.buildSources(sourceTime)
851
 
 
852
 
                if modifier in sources:
853
 
                    sourceTime    = sources[modifier]
854
 
                    flag          = True
855
 
                    self.timeFlag = 2
856
 
 
857
 
        # if the word after next is a number, the string is more than likely
858
 
        # to be "next 4 hrs" which we will have to combine the units with the
859
 
        # rest of the string
860
 
        if not flag:
861
 
            if offset < 0:
862
 
                # if offset is negative, the unit has to be made negative
863
 
                unit = '-%s' % unit
864
 
 
865
 
            chunk2 = '%s %s' % (unit, chunk2)
866
 
 
867
 
        self.modifierFlag = False
868
 
 
869
 
        #return '%s %s' % (chunk1, chunk2), sourceTime
870
 
        return '%s' % chunk2, sourceTime
871
 
 
872
 
    def _evalModifier2(self, modifier, chunk1 , chunk2, sourceTime):
873
 
        """
874
 
        Evaluate the C{modifier} string and following text (passed in
875
 
        as C{chunk1} and C{chunk2}) and if they match any known modifiers
876
 
        calculate the delta and apply it to C{sourceTime}.
877
 
 
878
 
        @type  modifier:   string
879
 
        @param modifier:   modifier text to apply to C{sourceTime}
880
 
        @type  chunk1:     string
881
 
        @param chunk1:     first text chunk that followed modifier (if any)
882
 
        @type  chunk2:     string
883
 
        @param chunk2:     second text chunk that followed modifier (if any)
884
 
        @type  sourceTime: struct_time
885
 
        @param sourceTime: C{struct_time} value to use as the base
886
 
 
887
 
        @rtype:  tuple
888
 
        @return: tuple of: remaining text and the modified sourceTime
889
 
        """
890
 
        offset = self.ptc.Modifiers[modifier]
891
 
        digit  = r'\d+'
892
 
 
893
 
        self.modifier2Flag = False
894
 
 
895
 
        # If the string after the negative modifier starts with digits,
896
 
        # then it is likely that the string is similar to ' before 3 days'
897
 
        # or 'evening prior to 3 days'.
898
 
        # In this case, the total time is calculated by subtracting '3 days'
899
 
        # from the current date.
900
 
        # So, we have to identify the quantity and negate it before parsing
901
 
        # the string.
902
 
        # This is not required for strings not starting with digits since the
903
 
        # string is enough to calculate the sourceTime
904
 
        if chunk2 != '':
905
 
            if offset < 0:
906
 
                m = re.match(digit, chunk2.strip())
907
 
                if m is not None:
908
 
                    qty    = int(m.group()) * -1
909
 
                    chunk2 = chunk2[m.end():]
910
 
                    chunk2 = '%d%s' % (qty, chunk2)
911
 
 
912
 
            sourceTime, flag1 = self.parse(chunk2, sourceTime)
913
 
            if flag1 == 0:
914
 
                flag1 = True
915
 
            else:
916
 
                flag1 = False
917
 
            flag2 = False
918
 
        else:
919
 
            flag1 = False
920
 
 
921
 
        if chunk1 != '':
922
 
            if offset < 0:
923
 
                m = re.search(digit, chunk1.strip())
924
 
                if m is not None:
925
 
                    qty    = int(m.group()) * -1
926
 
                    chunk1 = chunk1[m.end():]
927
 
                    chunk1 = '%d%s' % (qty, chunk1)
928
 
 
929
 
            tempDateFlag       = self.dateFlag
930
 
            tempTimeFlag       = self.timeFlag
931
 
            sourceTime2, flag2 = self.parse(chunk1, sourceTime)
932
 
        else:
933
 
            return sourceTime, (flag1 and flag2)
934
 
 
935
 
        # if chunk1 is not a datetime and chunk2 is then do not use datetime
936
 
        # value returned by parsing chunk1
937
 
        if not (flag1 == False and flag2 == 0):
938
 
            sourceTime = sourceTime2
939
 
        else:
940
 
            self.timeFlag = tempTimeFlag
941
 
            self.dateFlag = tempDateFlag
942
 
 
943
 
        return sourceTime, (flag1 and flag2)
944
 
 
945
 
 
946
 
    def _evalString(self, datetimeString, sourceTime=None):
947
 
        """
948
 
        Calculate the datetime based on flags set by the L{parse()} routine
949
 
 
950
 
        Examples handled::
951
 
            RFC822, W3CDTF formatted dates
952
 
            HH:MM[:SS][ am/pm]
953
 
            MM/DD/YYYY
954
 
            DD MMMM YYYY
955
 
 
956
 
        @type  datetimeString: string
957
 
        @param datetimeString: text to try and parse as more "traditional"
958
 
                               date/time text
959
 
        @type  sourceTime:     struct_time
960
 
        @param sourceTime:     C{struct_time} value to use as the base
961
 
 
962
 
        @rtype:  datetime
963
 
        @return: calculated C{struct_time} value or current C{struct_time}
964
 
                 if not parsed
965
 
        """
966
 
        s   = datetimeString.strip()
967
 
        now = time.localtime()
968
 
 
969
 
        # Given string date is a RFC822 date
970
 
        if sourceTime is None:
971
 
            sourceTime = _parse_date_rfc822(s)
972
 
 
973
 
            if sourceTime is not None:
974
 
                (yr, mth, dy, hr, mn, sec, wd, yd, isdst, _) = sourceTime
975
 
                self.dateFlag = 1
976
 
 
977
 
                if (hr != 0) and (mn != 0) and (sec != 0):
978
 
                    self.timeFlag = 2
979
 
 
980
 
                sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
981
 
 
982
 
        # Given string date is a W3CDTF date
983
 
        if sourceTime is None:
984
 
            sourceTime = _parse_date_w3dtf(s)
985
 
 
986
 
            if sourceTime is not None:
987
 
                self.dateFlag = 1
988
 
                self.timeFlag = 2
989
 
 
990
 
        if sourceTime is None:
991
 
            s = s.lower()
992
 
 
993
 
        # Given string is in the format HH:MM(:SS)(am/pm)
994
 
        if self.meridianFlag:
995
 
            if sourceTime is None:
996
 
                (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = now
997
 
            else:
998
 
                (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
999
 
 
1000
 
            m = self.ptc.CRE_TIMEHMS2.search(s)
1001
 
            if m is not None:
1002
 
                dt = s[:m.start('meridian')].strip()
1003
 
                if len(dt) <= 2:
1004
 
                    hr  = int(dt)
1005
 
                    mn  = 0
1006
 
                    sec = 0
1007
 
                else:
1008
 
                    hr, mn, sec = _extract_time(m)
1009
 
 
1010
 
                if hr == 24:
1011
 
                    hr = 0
1012
 
 
1013
 
                sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
1014
 
                meridian   = m.group('meridian').lower()
1015
 
 
1016
 
                  # if 'am' found and hour is 12 - force hour to 0 (midnight)
1017
 
                if (meridian in self.ptc.am) and hr == 12:
1018
 
                    sourceTime = (yr, mth, dy, 0, mn, sec, wd, yd, isdst)
1019
 
 
1020
 
                  # if 'pm' found and hour < 12, add 12 to shift to evening
1021
 
                if (meridian in self.ptc.pm) and hr < 12:
1022
 
                    sourceTime = (yr, mth, dy, hr + 12, mn, sec, wd, yd, isdst)
1023
 
 
1024
 
              # invalid time
1025
 
            if hr > 24 or mn > 59 or sec > 59:
1026
 
                sourceTime    = now
1027
 
                self.dateFlag = 0
1028
 
                self.timeFlag = 0
1029
 
 
1030
 
            self.meridianFlag = False
1031
 
 
1032
 
          # Given string is in the format HH:MM(:SS)
1033
 
        if self.timeStdFlag:
1034
 
            if sourceTime is None:
1035
 
                (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = now
1036
 
            else:
1037
 
                (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
1038
 
 
1039
 
            m = self.ptc.CRE_TIMEHMS.search(s)
1040
 
            if m is not None:
1041
 
                hr, mn, sec = _extract_time(m)
1042
 
            if hr == 24:
1043
 
                hr = 0
1044
 
 
1045
 
            if hr > 24 or mn > 59 or sec > 59:
1046
 
                # invalid time
1047
 
                sourceTime    = now
1048
 
                self.dateFlag = 0
1049
 
                self.timeFlag = 0
1050
 
            else:
1051
 
                sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
1052
 
 
1053
 
            self.timeStdFlag = False
1054
 
 
1055
 
        # Given string is in the format 07/21/2006
1056
 
        if self.dateStdFlag:
1057
 
            sourceTime       = self.parseDate(s)
1058
 
            self.dateStdFlag = False
1059
 
 
1060
 
        # Given string is in the format  "May 23rd, 2005"
1061
 
        if self.dateStrFlag:
1062
 
            sourceTime       = self.parseDateText(s)
1063
 
            self.dateStrFlag = False
1064
 
 
1065
 
        # Given string is a weekday
1066
 
        if self.weekdyFlag:
1067
 
            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = now
1068
 
 
1069
 
            start = datetime.datetime(yr, mth, dy, hr, mn, sec)
1070
 
            wkdy  = self.ptc.WeekdayOffsets[s]
1071
 
 
1072
 
            if wkdy > wd:
1073
 
                qty = self._CalculateDOWDelta(wd, wkdy, 2,
1074
 
                                              self.ptc.DOWParseStyle,
1075
 
                                              self.ptc.CurrentDOWParseStyle)
1076
 
            else:
1077
 
                qty = self._CalculateDOWDelta(wd, wkdy, 2,
1078
 
                                              self.ptc.DOWParseStyle,
1079
 
                                              self.ptc.CurrentDOWParseStyle)
1080
 
 
1081
 
            target = start + datetime.timedelta(days=qty)
1082
 
            wd     = wkdy
1083
 
 
1084
 
            sourceTime      = target.timetuple()
1085
 
            self.weekdyFlag = False
1086
 
 
1087
 
        # Given string is a natural language time string like
1088
 
        # lunch, midnight, etc
1089
 
        if self.timeStrFlag:
1090
 
            if s in self.ptc.re_values['now']:
1091
 
                sourceTime = now
1092
 
            else:
1093
 
                sources = self.ptc.buildSources(sourceTime)
1094
 
 
1095
 
                if s in sources:
1096
 
                    sourceTime = sources[s]
1097
 
                else:
1098
 
                    sourceTime    = now
1099
 
                    self.dateFlag = 0
1100
 
                    self.timeFlag = 0
1101
 
 
1102
 
            self.timeStrFlag = False
1103
 
 
1104
 
        # Given string is a natural language date string like today, tomorrow..
1105
 
        if self.dayStrFlag:
1106
 
            if sourceTime is None:
1107
 
                sourceTime = now
1108
 
 
1109
 
            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
1110
 
 
1111
 
            if s in self.ptc.dayOffsets:
1112
 
                offset = self.ptc.dayOffsets[s]
1113
 
            else:
1114
 
                offset = 0
1115
 
 
1116
 
            start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
1117
 
            target     = start + datetime.timedelta(days=offset)
1118
 
            sourceTime = target.timetuple()
1119
 
 
1120
 
            self.dayStrFlag = False
1121
 
 
1122
 
        # Given string is a time string with units like "5 hrs 30 min"
1123
 
        if self.unitsFlag:
1124
 
            modifier = ''  # TODO
1125
 
 
1126
 
            if sourceTime is None:
1127
 
                sourceTime = now
1128
 
 
1129
 
            m = self.ptc.CRE_UNITS.search(s)
1130
 
            if m is not None:
1131
 
                units    = m.group('units')
1132
 
                quantity = s[:m.start('units')]
1133
 
 
1134
 
            sourceTime     = self._buildTime(sourceTime, quantity, modifier, units)
1135
 
            self.unitsFlag = False
1136
 
 
1137
 
        # Given string is a time string with single char units like "5 h 30 m"
1138
 
        if self.qunitsFlag:
1139
 
            modifier = ''  # TODO
1140
 
 
1141
 
            if sourceTime is None:
1142
 
                sourceTime = now
1143
 
 
1144
 
            m = self.ptc.CRE_QUNITS.search(s)
1145
 
            if m is not None:
1146
 
                units    = m.group('qunits')
1147
 
                quantity = s[:m.start('qunits')]
1148
 
 
1149
 
            sourceTime      = self._buildTime(sourceTime, quantity, modifier, units)
1150
 
            self.qunitsFlag = False
1151
 
 
1152
 
          # Given string does not match anything
1153
 
        if sourceTime is None:
1154
 
            sourceTime    = now
1155
 
            self.dateFlag = 0
1156
 
            self.timeFlag = 0
1157
 
 
1158
 
        return sourceTime
1159
 
 
1160
 
 
1161
 
    def parse(self, datetimeString, sourceTime=None):
1162
 
        """
1163
 
        Splits the given C{datetimeString} into tokens, finds the regex
1164
 
        patterns that match and then calculates a C{struct_time} value from
1165
 
        the chunks.
1166
 
 
1167
 
        If C{sourceTime} is given then the C{struct_time} value will be
1168
 
        calculated from that value, otherwise from the current date/time.
1169
 
 
1170
 
        If the C{datetimeString} is parsed and date/time value found then
1171
 
        the second item of the returned tuple will be a flag to let you know
1172
 
        what kind of C{struct_time} value is being returned::
1173
 
 
1174
 
            0 = not parsed at all
1175
 
            1 = parsed as a C{date}
1176
 
            2 = parsed as a C{time}
1177
 
            3 = parsed as a C{datetime}
1178
 
 
1179
 
        @type  datetimeString: string
1180
 
        @param datetimeString: date/time text to evaluate
1181
 
        @type  sourceTime:     struct_time
1182
 
        @param sourceTime:     C{struct_time} value to use as the base
1183
 
 
1184
 
        @rtype:  tuple
1185
 
        @return: tuple of: modified C{sourceTime} and the result flag
1186
 
        """
1187
 
 
1188
 
        if sourceTime:
1189
 
            if isinstance(sourceTime, datetime.datetime):
1190
 
                if _debug:
1191
 
                    print 'coercing datetime to timetuple'
1192
 
                sourceTime = sourceTime.timetuple()
1193
 
            else:
1194
 
                if not isinstance(sourceTime, time.struct_time) and \
1195
 
                   not isinstance(sourceTime, tuple):
1196
 
                    raise Exception('sourceTime is not a struct_time')
1197
 
 
1198
 
        s         = datetimeString.strip().lower()
1199
 
        parseStr  = ''
1200
 
        totalTime = sourceTime
1201
 
 
1202
 
        if s == '' :
1203
 
            if sourceTime is not None:
1204
 
                return (sourceTime, self.dateFlag + self.timeFlag)
1205
 
            else:
1206
 
                return (time.localtime(), 0)
1207
 
 
1208
 
        self.timeFlag = 0
1209
 
        self.dateFlag = 0
1210
 
 
1211
 
        while len(s) > 0:
1212
 
            flag   = False
1213
 
            chunk1 = ''
1214
 
            chunk2 = ''
1215
 
 
1216
 
            if _debug:
1217
 
                print 'parse (top of loop): [%s][%s]' % (s, parseStr)
1218
 
 
1219
 
            if parseStr == '':
1220
 
                # Modifier like next\prev..
1221
 
                m = self.ptc.CRE_MODIFIER.search(s)
1222
 
                if m is not None:
1223
 
                    self.modifierFlag = True
1224
 
                    if (m.group('modifier') != s):
1225
 
                        # capture remaining string
1226
 
                        parseStr = m.group('modifier')
1227
 
                        chunk1   = s[:m.start('modifier')].strip()
1228
 
                        chunk2   = s[m.end('modifier'):].strip()
1229
 
                        flag     = True
1230
 
                    else:
1231
 
                        parseStr = s
1232
 
 
1233
 
            if parseStr == '':
1234
 
                # Modifier like from\after\prior..
1235
 
                m = self.ptc.CRE_MODIFIER2.search(s)
1236
 
                if m is not None:
1237
 
                    self.modifier2Flag = True
1238
 
                    if (m.group('modifier') != s):
1239
 
                        # capture remaining string
1240
 
                        parseStr = m.group('modifier')
1241
 
                        chunk1   = s[:m.start('modifier')].strip()
1242
 
                        chunk2   = s[m.end('modifier'):].strip()
1243
 
                        flag     = True
1244
 
                    else:
1245
 
                        parseStr = s
1246
 
 
1247
 
            if parseStr == '':
1248
 
                valid_date = False
1249
 
                for match in self.ptc.CRE_DATE3.finditer(s):
1250
 
                    # to prevent "HH:MM(:SS) time strings" expressions from triggering
1251
 
                    # this regex, we checks if the month field exists in the searched 
1252
 
                    # expression, if it doesn't exist, the date field is not valid
1253
 
                    if match.group('mthname'):
1254
 
                        m = self.ptc.CRE_DATE3.search(s, match.start())
1255
 
                        valid_date = True
1256
 
                        break
1257
 
 
1258
 
                # String date format
1259
 
                if valid_date:
1260
 
                    self.dateStrFlag = True
1261
 
                    self.dateFlag    = 1
1262
 
                    if (m.group('date') != s):
1263
 
                        # capture remaining string
1264
 
                        parseStr = m.group('date')
1265
 
                        chunk1   = s[:m.start('date')]
1266
 
                        chunk2   = s[m.end('date'):]
1267
 
                        s        = '%s %s' % (chunk1, chunk2)
1268
 
                        flag     = True
1269
 
                    else:
1270
 
                        parseStr = s
1271
 
 
1272
 
            if parseStr == '':
1273
 
                # Standard date format
1274
 
                m = self.ptc.CRE_DATE.search(s)
1275
 
                if m is not None:
1276
 
                    self.dateStdFlag = True
1277
 
                    self.dateFlag    = 1
1278
 
                    if (m.group('date') != s):
1279
 
                        # capture remaining string
1280
 
                        parseStr = m.group('date')
1281
 
                        chunk1   = s[:m.start('date')]
1282
 
                        chunk2   = s[m.end('date'):]
1283
 
                        s        = '%s %s' % (chunk1, chunk2)
1284
 
                        flag     = True
1285
 
                    else:
1286
 
                        parseStr = s
1287
 
 
1288
 
            if parseStr == '':
1289
 
                # Natural language day strings
1290
 
                m = self.ptc.CRE_DAY.search(s)
1291
 
                if m is not None:
1292
 
                    self.dayStrFlag = True
1293
 
                    self.dateFlag   = 1
1294
 
                    if (m.group('day') != s):
1295
 
                        # capture remaining string
1296
 
                        parseStr = m.group('day')
1297
 
                        chunk1   = s[:m.start('day')]
1298
 
                        chunk2   = s[m.end('day'):]
1299
 
                        s        = '%s %s' % (chunk1, chunk2)
1300
 
                        flag     = True
1301
 
                    else:
1302
 
                        parseStr = s
1303
 
 
1304
 
            if parseStr == '':
1305
 
                # Quantity + Units
1306
 
                m = self.ptc.CRE_UNITS.search(s)
1307
 
                if m is not None:
1308
 
                    self.unitsFlag = True
1309
 
                    if (m.group('qty') != s):
1310
 
                        # capture remaining string
1311
 
                        parseStr = m.group('qty')
1312
 
                        chunk1   = s[:m.start('qty')].strip()
1313
 
                        chunk2   = s[m.end('qty'):].strip()
1314
 
 
1315
 
                        if chunk1[-1:] == '-':
1316
 
                            parseStr = '-%s' % parseStr
1317
 
                            chunk1   = chunk1[:-1]
1318
 
 
1319
 
                        s    = '%s %s' % (chunk1, chunk2)
1320
 
                        flag = True
1321
 
                    else:
1322
 
                        parseStr = s
1323
 
 
1324
 
            if parseStr == '':
1325
 
                # Quantity + Units
1326
 
                m = self.ptc.CRE_QUNITS.search(s)
1327
 
                if m is not None:
1328
 
                    self.qunitsFlag = True
1329
 
 
1330
 
                    if (m.group('qty') != s):
1331
 
                        # capture remaining string
1332
 
                        parseStr = m.group('qty')
1333
 
                        chunk1   = s[:m.start('qty')].strip()
1334
 
                        chunk2   = s[m.end('qty'):].strip()
1335
 
 
1336
 
                        if chunk1[-1:] == '-':
1337
 
                            parseStr = '-%s' % parseStr
1338
 
                            chunk1   = chunk1[:-1]
1339
 
 
1340
 
                        s    = '%s %s' % (chunk1, chunk2)
1341
 
                        flag = True
1342
 
                    else:
1343
 
                        parseStr = s 
1344
 
 
1345
 
            if parseStr == '':
1346
 
                # Weekday
1347
 
                m = self.ptc.CRE_WEEKDAY.search(s)
1348
 
                if m is not None:
1349
 
                    gv = m.group('weekday')
1350
 
                    if s not in self.ptc.dayOffsets:
1351
 
                        self.weekdyFlag = True
1352
 
                        self.dateFlag   = 1
1353
 
                        if (gv != s):
1354
 
                            # capture remaining string
1355
 
                            parseStr = gv
1356
 
                            chunk1   = s[:m.start('weekday')]
1357
 
                            chunk2   = s[m.end('weekday'):]
1358
 
                            s        = '%s %s' % (chunk1, chunk2)
1359
 
                            flag     = True
1360
 
                        else:
1361
 
                            parseStr = s
1362
 
 
1363
 
            if parseStr == '':
1364
 
                # Natural language time strings
1365
 
                m = self.ptc.CRE_TIME.search(s)
1366
 
                if m is not None:
1367
 
                    self.timeStrFlag = True
1368
 
                    self.timeFlag    = 2
1369
 
                    if (m.group('time') != s):
1370
 
                        # capture remaining string
1371
 
                        parseStr = m.group('time')
1372
 
                        chunk1   = s[:m.start('time')]
1373
 
                        chunk2   = s[m.end('time'):]
1374
 
                        s        = '%s %s' % (chunk1, chunk2)
1375
 
                        flag     = True
1376
 
                    else:
1377
 
                        parseStr = s
1378
 
 
1379
 
            if parseStr == '':
1380
 
                # HH:MM(:SS) am/pm time strings
1381
 
                m = self.ptc.CRE_TIMEHMS2.search(s)
1382
 
                if m is not None:
1383
 
                    self.meridianFlag = True
1384
 
                    self.timeFlag     = 2
1385
 
                    if m.group('minutes') is not None:
1386
 
                        if m.group('seconds') is not None:
1387
 
                            parseStr = '%s:%s:%s %s' % (m.group('hours'),
1388
 
                                                        m.group('minutes'),
1389
 
                                                        m.group('seconds'),
1390
 
                                                        m.group('meridian'))
1391
 
                        else:
1392
 
                            parseStr = '%s:%s %s' % (m.group('hours'),
1393
 
                                                     m.group('minutes'),
1394
 
                                                     m.group('meridian'))
1395
 
                    else:
1396
 
                        parseStr = '%s %s' % (m.group('hours'),
1397
 
                                              m.group('meridian'))
1398
 
 
1399
 
                    chunk1 = s[:m.start('hours')]
1400
 
                    chunk2 = s[m.end('meridian'):]
1401
 
 
1402
 
                    s    = '%s %s' % (chunk1, chunk2)
1403
 
                    flag = True
1404
 
 
1405
 
            if parseStr == '':
1406
 
                # HH:MM(:SS) time strings
1407
 
                m = self.ptc.CRE_TIMEHMS.search(s)
1408
 
                if m is not None:
1409
 
                    self.timeStdFlag = True
1410
 
                    self.timeFlag    = 2
1411
 
                    if m.group('seconds') is not None:
1412
 
                        parseStr = '%s:%s:%s' % (m.group('hours'),
1413
 
                                                 m.group('minutes'),
1414
 
                                                 m.group('seconds'))
1415
 
                        chunk1   = s[:m.start('hours')]
1416
 
                        chunk2   = s[m.end('seconds'):]
1417
 
                    else:
1418
 
                        parseStr = '%s:%s' % (m.group('hours'),
1419
 
                                              m.group('minutes'))
1420
 
                        chunk1   = s[:m.start('hours')]
1421
 
                        chunk2   = s[m.end('minutes'):]
1422
 
 
1423
 
                    s    = '%s %s' % (chunk1, chunk2)
1424
 
                    flag = True
1425
 
 
1426
 
            # if string does not match any regex, empty string to
1427
 
            # come out of the while loop
1428
 
            if not flag:
1429
 
                s = ''
1430
 
 
1431
 
            if _debug:
1432
 
                print 'parse (bottom) [%s][%s][%s][%s]' % (s, parseStr, chunk1, chunk2)
1433
 
                print 'weekday %s, dateStd %s, dateStr %s, time %s, timeStr %s, meridian %s' % \
1434
 
                       (self.weekdyFlag, self.dateStdFlag, self.dateStrFlag, self.timeStdFlag, self.timeStrFlag, self.meridianFlag)
1435
 
                print 'dayStr %s, modifier %s, modifier2 %s, units %s, qunits %s' % \
1436
 
                       (self.dayStrFlag, self.modifierFlag, self.modifier2Flag, self.unitsFlag, self.qunitsFlag)
1437
 
 
1438
 
            # evaluate the matched string
1439
 
            if parseStr != '':
1440
 
                if self.modifierFlag == True:
1441
 
                    t, totalTime = self._evalModifier(parseStr, chunk1, chunk2, totalTime)
1442
 
                    # t is the unparsed part of the chunks.
1443
 
                    # If it is not date/time, return current
1444
 
                    # totalTime as it is; else return the output
1445
 
                    # after parsing t.
1446
 
                    if (t != '') and (t != None):
1447
 
                        tempDateFlag       = self.dateFlag
1448
 
                        tempTimeFlag       = self.timeFlag
1449
 
                        (totalTime2, flag) = self.parse(t, totalTime)
1450
 
 
1451
 
                        if flag == 0 and totalTime is not None:
1452
 
                            self.timeFlag = tempTimeFlag
1453
 
                            self.dateFlag = tempDateFlag
1454
 
 
1455
 
                            return (totalTime, self.dateFlag + self.timeFlag)
1456
 
                        else:
1457
 
                            return (totalTime2, self.dateFlag + self.timeFlag)
1458
 
 
1459
 
                elif self.modifier2Flag == True:
1460
 
                    totalTime, invalidFlag = self._evalModifier2(parseStr, chunk1, chunk2, totalTime)
1461
 
 
1462
 
                    if invalidFlag == True:
1463
 
                        self.dateFlag = 0
1464
 
                        self.timeFlag = 0
1465
 
 
1466
 
                else:
1467
 
                    totalTime = self._evalString(parseStr, totalTime)
1468
 
                    parseStr  = ''
1469
 
 
1470
 
        # String is not parsed at all
1471
 
        if totalTime is None or totalTime == sourceTime:
1472
 
            totalTime     = time.localtime()
1473
 
            self.dateFlag = 0
1474
 
            self.timeFlag = 0
1475
 
 
1476
 
        return (totalTime, self.dateFlag + self.timeFlag)
1477
 
 
1478
 
 
1479
 
    def inc(self, source, month=None, year=None):
1480
 
        """
1481
 
        Takes the given C{source} date, or current date if none is
1482
 
        passed, and increments it according to the values passed in
1483
 
        by month and/or year.
1484
 
 
1485
 
        This routine is needed because Python's C{timedelta()} function
1486
 
        does not allow for month or year increments.
1487
 
 
1488
 
        @type  source: struct_time
1489
 
        @param source: C{struct_time} value to increment
1490
 
        @type  month:  integer
1491
 
        @param month:  optional number of months to increment
1492
 
        @type  year:   integer
1493
 
        @param year:   optional number of years to increment
1494
 
 
1495
 
        @rtype:  datetime
1496
 
        @return: C{source} incremented by the number of months and/or years
1497
 
        """
1498
 
        yr  = source.year
1499
 
        mth = source.month
1500
 
        dy  = source.day
1501
 
 
1502
 
        if year:
1503
 
            try:
1504
 
                yi = int(year)
1505
 
            except ValueError:
1506
 
                yi = 0
1507
 
 
1508
 
            yr += yi
1509
 
 
1510
 
        if month:
1511
 
            try:
1512
 
                mi = int(month)
1513
 
            except ValueError:
1514
 
                mi = 0
1515
 
 
1516
 
            m = abs(mi)
1517
 
            y = m / 12      # how many years are in month increment
1518
 
            m = m % 12      # get remaining months
1519
 
 
1520
 
            if mi < 0:
1521
 
                mth = mth - m           # sub months from start month
1522
 
                if mth < 1:             # cross start-of-year?
1523
 
                    y   -= 1            #   yes - decrement year
1524
 
                    mth += 12           #         and fix month
1525
 
            else:
1526
 
                mth = mth + m           # add months to start month
1527
 
                if mth > 12:            # cross end-of-year?
1528
 
                    y   += 1            #   yes - increment year
1529
 
                    mth -= 12           #         and fix month
1530
 
 
1531
 
            yr += y
1532
 
 
1533
 
            # if the day ends up past the last day of
1534
 
            # the new month, set it to the last day
1535
 
            if dy > self.ptc.daysInMonth(mth, yr):
1536
 
                dy = self.ptc.daysInMonth(mth, yr)
1537
 
 
1538
 
        d = source.replace(year=yr, month=mth, day=dy)
1539
 
 
1540
 
        return source + (d - source)
1541
 
 
 
1
#!/usr/bin/env python
 
2
 
 
3
"""
 
4
Parse human-readable date/time text.
 
5
"""
 
6
 
 
7
__license__ = """Copyright (c) 2004-2006 Mike Taylor, All rights reserved.
 
8
 
 
9
Licensed under the Apache License, Version 2.0 (the "License");
 
10
you may not use this file except in compliance with the License.
 
11
You may obtain a copy of the License at
 
12
 
 
13
   http://www.apache.org/licenses/LICENSE-2.0
 
14
 
 
15
Unless required by applicable law or agreed to in writing, software
 
16
distributed under the License is distributed on an "AS IS" BASIS,
 
17
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
18
See the License for the specific language governing permissions and
 
19
limitations under the License.
 
20
"""
 
21
__author__       = 'Mike Taylor <http://code-bear.com>'
 
22
__contributors__ = ['Darshana Chhajed <mailto://darshana@osafoundation.org>',
 
23
                   ]
 
24
 
 
25
_debug = False
 
26
 
 
27
 
 
28
import string, re, time
 
29
import datetime, calendar, rfc822
 
30
import parsedatetime_consts
 
31
 
 
32
 
 
33
# Copied from feedparser.py
 
34
# Universal Feedparser, Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
 
35
# Originally a def inside of _parse_date_w3dtf()
 
36
def _extract_date(m):
 
37
    year = int(m.group('year'))
 
38
    if year < 100:
 
39
        year = 100 * int(time.gmtime()[0] / 100) + int(year)
 
40
    if year < 1000:
 
41
        return 0, 0, 0
 
42
    julian = m.group('julian')
 
43
    if julian:
 
44
        julian = int(julian)
 
45
        month = julian / 30 + 1
 
46
        day = julian % 30 + 1
 
47
        jday = None
 
48
        while jday != julian:
 
49
            t = time.mktime((year, month, day, 0, 0, 0, 0, 0, 0))
 
50
            jday = time.gmtime(t)[-2]
 
51
            diff = abs(jday - julian)
 
52
            if jday > julian:
 
53
                if diff < day:
 
54
                    day = day - diff
 
55
                else:
 
56
                    month = month - 1
 
57
                    day = 31
 
58
            elif jday < julian:
 
59
                if day + diff < 28:
 
60
                   day = day + diff
 
61
                else:
 
62
                    month = month + 1
 
63
        return year, month, day
 
64
    month = m.group('month')
 
65
    day = 1
 
66
    if month is None:
 
67
        month = 1
 
68
    else:
 
69
        month = int(month)
 
70
        day = m.group('day')
 
71
        if day:
 
72
            day = int(day)
 
73
        else:
 
74
            day = 1
 
75
    return year, month, day
 
76
 
 
77
# Copied from feedparser.py 
 
78
# Universal Feedparser, Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
 
79
# Originally a def inside of _parse_date_w3dtf()
 
80
def _extract_time(m):
 
81
    if not m:
 
82
        return 0, 0, 0
 
83
    hours = m.group('hours')
 
84
    if not hours:
 
85
        return 0, 0, 0
 
86
    hours = int(hours)
 
87
    minutes = int(m.group('minutes'))
 
88
    seconds = m.group('seconds')
 
89
    if seconds:
 
90
        seconds = int(seconds)
 
91
    else:
 
92
        seconds = 0
 
93
    return hours, minutes, seconds
 
94
 
 
95
 
 
96
# Copied from feedparser.py
 
97
# Universal Feedparser, Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
 
98
# Modified to return a tuple instead of mktime
 
99
#
 
100
# Original comment:
 
101
#       W3DTF-style date parsing adapted from PyXML xml.utils.iso8601, written by
 
102
#       Drake and licensed under the Python license.  Removed all range checking
 
103
#       for month, day, hour, minute, and second, since mktime will normalize
 
104
#       these later
 
105
def _parse_date_w3dtf(dateString):
 
106
    # the __extract_date and __extract_time methods were
 
107
    # copied-out so they could be used by my code --bear
 
108
    def __extract_tzd(m):
 
109
        '''Return the Time Zone Designator as an offset in seconds from UTC.'''
 
110
        if not m:
 
111
            return 0
 
112
        tzd = m.group('tzd')
 
113
        if not tzd:
 
114
            return 0
 
115
        if tzd == 'Z':
 
116
            return 0
 
117
        hours = int(m.group('tzdhours'))
 
118
        minutes = m.group('tzdminutes')
 
119
        if minutes:
 
120
            minutes = int(minutes)
 
121
        else:
 
122
            minutes = 0
 
123
        offset = (hours*60 + minutes) * 60
 
124
        if tzd[0] == '+':
 
125
            return -offset
 
126
        return offset
 
127
 
 
128
    __date_re = ('(?P<year>\d\d\d\d)'
 
129
                 '(?:(?P<dsep>-|)'
 
130
                 '(?:(?P<julian>\d\d\d)'
 
131
                 '|(?P<month>\d\d)(?:(?P=dsep)(?P<day>\d\d))?))?')
 
132
    __tzd_re = '(?P<tzd>[-+](?P<tzdhours>\d\d)(?::?(?P<tzdminutes>\d\d))|Z)'
 
133
    __tzd_rx = re.compile(__tzd_re)
 
134
    __time_re = ('(?P<hours>\d\d)(?P<tsep>:|)(?P<minutes>\d\d)'
 
135
                 '(?:(?P=tsep)(?P<seconds>\d\d(?:[.,]\d+)?))?'
 
136
                 + __tzd_re)
 
137
    __datetime_re = '%s(?:T%s)?' % (__date_re, __time_re)
 
138
    __datetime_rx = re.compile(__datetime_re)
 
139
    m = __datetime_rx.match(dateString)
 
140
    if (m is None) or (m.group() != dateString): return
 
141
    return _extract_date(m) + _extract_time(m) + (0, 0, 0)
 
142
 
 
143
 
 
144
# Copied from feedparser.py
 
145
# Universal Feedparser, Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
 
146
# Modified to return a tuple instead of mktime
 
147
#
 
148
def _parse_date_rfc822(dateString):
 
149
    '''Parse an RFC822, RFC1123, RFC2822, or asctime-style date'''
 
150
    data = dateString.split()
 
151
    if data[0][-1] in (',', '.') or data[0].lower() in rfc822._daynames:
 
152
        del data[0]
 
153
    if len(data) == 4:
 
154
        s = data[3]
 
155
        i = s.find('+')
 
156
        if i > 0:
 
157
            data[3:] = [s[:i], s[i+1:]]
 
158
        else:
 
159
            data.append('')
 
160
        dateString = " ".join(data)
 
161
    if len(data) < 5:
 
162
        dateString += ' 00:00:00 GMT'
 
163
    return rfc822.parsedate_tz(dateString)
 
164
 
 
165
# rfc822.py defines several time zones, but we define some extra ones.
 
166
# 'ET' is equivalent to 'EST', etc.
 
167
_additional_timezones = {'AT': -400, 'ET': -500, 'CT': -600, 'MT': -700, 'PT': -800}
 
168
rfc822._timezones.update(_additional_timezones)
 
169
 
 
170
 
 
171
class Calendar:
 
172
    """
 
173
    A collection of routines to input, parse and manipulate date and times.
 
174
    The text can either be 'normal' date values or it can be human readable.
 
175
    """
 
176
 
 
177
    def __init__(self, constants=None):
 
178
        """
 
179
        Default constructor for the Calendar class.
 
180
 
 
181
        @type  constants: object
 
182
        @param constants: Instance of the class L{CalendarConstants}
 
183
 
 
184
        @rtype:  object
 
185
        @return: Calendar instance
 
186
        """
 
187
          # if a constants reference is not included, use default
 
188
        if constants is None:
 
189
            self.ptc = parsedatetime_consts.Constants()
 
190
        else:
 
191
            self.ptc = constants
 
192
 
 
193
        self.CRE_SPECIAL   = re.compile(self.ptc.RE_SPECIAL,   re.IGNORECASE)
 
194
        self.CRE_UNITS     = re.compile(self.ptc.RE_UNITS,     re.IGNORECASE)
 
195
        self.CRE_QUNITS    = re.compile(self.ptc.RE_QUNITS,    re.IGNORECASE)
 
196
        self.CRE_MODIFIER  = re.compile(self.ptc.RE_MODIFIER,  re.IGNORECASE)
 
197
        self.CRE_MODIFIER2 = re.compile(self.ptc.RE_MODIFIER2, re.IGNORECASE)
 
198
        self.CRE_TIMEHMS   = re.compile(self.ptc.RE_TIMEHMS,   re.IGNORECASE)
 
199
        self.CRE_TIMEHMS2  = re.compile(self.ptc.RE_TIMEHMS2,  re.IGNORECASE)
 
200
        self.CRE_DATE      = re.compile(self.ptc.RE_DATE,      re.IGNORECASE)
 
201
        self.CRE_DATE2     = re.compile(self.ptc.RE_DATE2,     re.IGNORECASE)
 
202
        self.CRE_DATE3     = re.compile(self.ptc.RE_DATE3,     re.IGNORECASE)
 
203
        self.CRE_MONTH     = re.compile(self.ptc.RE_MONTH,     re.IGNORECASE)
 
204
        self.CRE_WEEKDAY   = re.compile(self.ptc.RE_WEEKDAY,   re.IGNORECASE)
 
205
        self.CRE_DAY       = re.compile(self.ptc.RE_DAY,       re.IGNORECASE)
 
206
        self.CRE_TIME      = re.compile(self.ptc.RE_TIME,      re.IGNORECASE)
 
207
        self.CRE_REMAINING = re.compile(self.ptc.RE_REMAINING, re.IGNORECASE)
 
208
 
 
209
        #regex for date/time ranges
 
210
        self.CRE_RTIMEHMS  = re.compile(self.ptc.RE_RTIMEHMS,  re.IGNORECASE)
 
211
        self.CRE_RTIMEHMS2 = re.compile(self.ptc.RE_RTIMEHMS2, re.IGNORECASE)
 
212
        self.CRE_RDATE     = re.compile(self.ptc.RE_RDATE,     re.IGNORECASE)
 
213
        self.CRE_RDATE3    = re.compile(self.ptc.RE_RDATE3,    re.IGNORECASE)
 
214
 
 
215
        self.CRE_TIMERNG1  = re.compile(self.ptc.TIMERNG1, re.IGNORECASE)
 
216
        self.CRE_TIMERNG2  = re.compile(self.ptc.TIMERNG2, re.IGNORECASE)
 
217
        self.CRE_TIMERNG3  = re.compile(self.ptc.TIMERNG3, re.IGNORECASE)
 
218
        self.CRE_DATERNG1  = re.compile(self.ptc.DATERNG1, re.IGNORECASE)
 
219
        self.CRE_DATERNG2  = re.compile(self.ptc.DATERNG2, re.IGNORECASE)
 
220
        self.CRE_DATERNG3  = re.compile(self.ptc.DATERNG3, re.IGNORECASE)
 
221
 
 
222
        self.invalidFlag   = False  # Is set if the datetime string entered cannot be parsed at all
 
223
        self.weekdyFlag    = False  # monday/tuesday/...
 
224
        self.dateStdFlag   = False  # 07/21/06
 
225
        self.dateStrFlag   = False  # July 21st, 2006
 
226
        self.timeFlag      = False  # 5:50 
 
227
        self.meridianFlag  = False  # am/pm
 
228
        self.dayStrFlag    = False  # tomorrow/yesterday/today/..
 
229
        self.timeStrFlag   = False  # lunch/noon/breakfast/...
 
230
        self.modifierFlag  = False  # after/before/prev/next/..
 
231
        self.modifier2Flag = False  # after/before/prev/next/..
 
232
        self.unitsFlag     = False  # hrs/weeks/yrs/min/..
 
233
        self.qunitsFlag    = False  # h/m/t/d..
 
234
 
 
235
 
 
236
    def _convertUnitAsWords(self, unitText):
 
237
        """
 
238
        Converts text units into their number value
 
239
 
 
240
        Five = 5
 
241
        Twenty Five = 25
 
242
        Two hundred twenty five = 225
 
243
        Two thousand and twenty five = 2025
 
244
        Two thousand twenty five = 2025
 
245
 
 
246
        @type  unitText: string
 
247
        @param unitText: number string
 
248
 
 
249
        @rtype:  integer
 
250
        @return: numerical value of unitText
 
251
        """
 
252
        # TODO: implement this
 
253
        pass
 
254
 
 
255
 
 
256
    def _buildTime(self, source, quantity, modifier, units):
 
257
        """
 
258
        Take quantity, modifier and unit strings and convert them into values.
 
259
        Then calcuate the time and return the adjusted sourceTime
 
260
 
 
261
        @type  source:   time
 
262
        @param source:   time to use as the base (or source)
 
263
        @type  quantity: string
 
264
        @param quantity: quantity string
 
265
        @type  modifier: string
 
266
        @param modifier: how quantity and units modify the source time
 
267
        @type  units:    string
 
268
        @param units:    unit of the quantity (i.e. hours, days, months, etc)
 
269
 
 
270
        @rtype:  timetuple
 
271
        @return: timetuple of the calculated time
 
272
        """
 
273
        if _debug:
 
274
            print '_buildTime: [%s][%s][%s]' % (quantity, modifier, units)
 
275
 
 
276
        if source is None:
 
277
            source = time.localtime()
 
278
 
 
279
        if quantity is None:
 
280
            quantity = ''
 
281
        else:
 
282
            quantity = string.strip(quantity)
 
283
 
 
284
        if len(quantity) == 0:
 
285
            qty = 1
 
286
        else:
 
287
            try:
 
288
                qty = int(quantity)
 
289
            except ValueError:
 
290
                qty = 0
 
291
 
 
292
        if modifier in self.ptc.Modifiers:
 
293
            qty = qty * self.ptc.Modifiers[modifier]
 
294
 
 
295
            if units is None or units == '':
 
296
                units = 'dy'
 
297
 
 
298
        # plurals are handled by regex's (could be a bug tho)
 
299
 
 
300
        (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = source
 
301
 
 
302
        start  = datetime.datetime(yr, mth, dy, hr, mn, sec)
 
303
        target = start
 
304
 
 
305
        if units.startswith('y'):
 
306
            target = self.inc(start, year=qty)
 
307
        elif units.endswith('th') or units.endswith('ths'):
 
308
            target = self.inc(start, month=qty)
 
309
        else:
 
310
            if units.startswith('d'):
 
311
                target = start + datetime.timedelta(days=qty)
 
312
            elif units.startswith('h'):
 
313
                target = start + datetime.timedelta(hours=qty)
 
314
            elif units.startswith('m'):
 
315
                target = start + datetime.timedelta(minutes=qty)
 
316
            elif units.startswith('s'):
 
317
                target = start + datetime.timedelta(seconds=qty)
 
318
            elif units.startswith('w'):
 
319
                target = start + datetime.timedelta(weeks=qty)
 
320
 
 
321
        if target != start:
 
322
            self.invalidFlag = False
 
323
 
 
324
        return target.timetuple()
 
325
 
 
326
 
 
327
    def parseDate(self, dateString):
 
328
        """
 
329
        Parses strings like 05/28/200 or 04.21
 
330
 
 
331
        @type  dateString: string
 
332
        @param dateString: text to convert to a datetime
 
333
 
 
334
        @rtype:  datetime
 
335
        @return: calculated datetime value of dateString
 
336
        """
 
337
        yr, mth, dy, hr, mn, sec, wd, yd, isdst = time.localtime()
 
338
 
 
339
        s = dateString
 
340
        m = self.CRE_DATE2.search(s)
 
341
        if m is not None:
 
342
            index = m.start()
 
343
            mth   = int(s[:index])
 
344
            s     = s[index + 1:]
 
345
 
 
346
        m = self.CRE_DATE2.search(s)
 
347
        if m is not None:
 
348
            index = m.start()
 
349
            dy    = int(s[:index])
 
350
            yr    = int(s[index + 1:])
 
351
            # TODO should this have a birthday epoch constraint?
 
352
            if yr < 99:
 
353
                yr += 2000
 
354
        else:
 
355
            dy = int(string.strip(s))
 
356
 
 
357
        if (mth > 0 and mth <= 12) and (dy > 0 and dy <= self.ptc.DaysInMonthList[mth - 1]):
 
358
            sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
 
359
        else:
 
360
            self.invalidFlag = True
 
361
            sourceTime       = time.localtime() #return current time if date string is invalid
 
362
 
 
363
        return sourceTime
 
364
 
 
365
 
 
366
    def parseDateText(self, dateString):
 
367
        """
 
368
        Parses strings like "May 31st, 2006" or "Jan 1st" or "July 2006"
 
369
 
 
370
        @type  dateString: string
 
371
        @param dateString: text to convert to a datetime
 
372
 
 
373
        @rtype:  datetime
 
374
        @return: calculated datetime value of dateString
 
375
        """
 
376
        yr, mth, dy, hr, mn, sec, wd, yd, isdst = time.localtime()
 
377
 
 
378
        currentMth = mth
 
379
        currentDy  = dy
 
380
 
 
381
        s   = dateString.lower()
 
382
        m   = self.CRE_DATE3.search(s)
 
383
        mth = m.group('mthname')
 
384
        mth = self.ptc.MonthOffsets[mth]
 
385
 
 
386
        if m.group('day') !=  None:
 
387
            dy = int(m.group('day'))
 
388
        else:
 
389
            dy = 1
 
390
 
 
391
        if m.group('year') !=  None:
 
392
            yr = int(m.group('year'))
 
393
        elif (mth < currentMth) or (mth == currentMth and dy < currentDy):
 
394
            # if that day and month have already passed in this year,
 
395
            # then increment the year by 1
 
396
            yr += 1
 
397
 
 
398
        if dy > 0 and dy <= self.ptc.DaysInMonthList[mth - 1]:
 
399
            sourceTime = (yr, mth, dy, 9, 0, 0, wd, yd, isdst)
 
400
        else:
 
401
              # Return current time if date string is invalid
 
402
            self.invalidFlag = True
 
403
            sourceTime       = time.localtime()
 
404
 
 
405
        return sourceTime
 
406
 
 
407
 
 
408
    def evalRanges(self, datetimeString, sourceTime=None):
 
409
        """
 
410
        Evaluates the strings with time or date ranges
 
411
 
 
412
        @type  datetimeString: string
 
413
        @param datetimeString: datetime text to evaluate
 
414
        @type  sourceTime:     datetime
 
415
        @param sourceTime:     datetime value to use as the base
 
416
 
 
417
        @rtype:  tuple
 
418
        @return: tuple of the start datetime, end datetime and the invalid flag
 
419
        """
 
420
        startTime = ''
 
421
        endTime   = ''
 
422
        startDate = ''
 
423
        endDate   = ''
 
424
        rangeFlag = 0
 
425
 
 
426
        s = string.strip(datetimeString.lower())
 
427
 
 
428
        m = self.CRE_TIMERNG1.search(s)
 
429
        if m is not None:
 
430
            rangeFlag = 1
 
431
        else:
 
432
            m = self.CRE_TIMERNG2.search(s)
 
433
            if m is not None:
 
434
                rangeFlag = 2
 
435
            else:
 
436
                m = self.CRE_TIMERNG3.search(s)
 
437
                if m is not None:
 
438
                    rangeFlag = 3
 
439
                else:
 
440
                    m = self.CRE_DATERNG1.search(s)
 
441
                    if m is not None:
 
442
                        rangeFlag = 4
 
443
                    else:
 
444
                        m = self.CRE_DATERNG2.search(s)
 
445
                        if m is not None:
 
446
                            rangeFlag = 5
 
447
                        else:
 
448
                            m = self.CRE_DATERNG3.search(s)
 
449
                            if m is not None:
 
450
                                rangeFlag = 6
 
451
 
 
452
        if _debug:
 
453
            print 'evalRanges: rangeFlag =', rangeFlag, '[%s]' % s
 
454
 
 
455
        if m is not None:
 
456
            if (m.group() != s):
 
457
                # capture remaining string
 
458
                parseStr = m.group()
 
459
                chunk1   = s[:m.start()]
 
460
                chunk2   = s[m.end():]
 
461
                s        = '%s %s' % (chunk1, chunk2)
 
462
                flag     = 1
 
463
 
 
464
                sourceTime, flag = self.parse(s, sourceTime)
 
465
 
 
466
                if flag == True:
 
467
                    sourceTime = None
 
468
            else:
 
469
                parseStr = s
 
470
 
 
471
        if rangeFlag == 1:
 
472
            # FIXME hardcoded seperator
 
473
            m                = re.search('-', parseStr)
 
474
            startTime, sflag = self.parse((parseStr[:m.start()]),       sourceTime)
 
475
            endTime, eflag   = self.parse((parseStr[(m.start() + 1):]), sourceTime)
 
476
 
 
477
            if eflag is False and sflag is False:
 
478
                return (startTime, endTime, False)
 
479
 
 
480
        elif rangeFlag == 2:
 
481
            # FIXME hardcoded seperator
 
482
            m                = re.search('-', parseStr)
 
483
            startTime, sflag = self.parse((parseStr[:m.start()]), sourceTime)
 
484
            endTime, eflag   = self.parse((parseStr[(m.start() + 1):]), sourceTime)
 
485
 
 
486
            if eflag is False and sflag is False:
 
487
                return (startTime, endTime, False)
 
488
 
 
489
        elif rangeFlag == 3:
 
490
            # FIXME hardcoded seperator
 
491
            m = re.search('-', parseStr)
 
492
 
 
493
            # capturing the meridian from the end time
 
494
            # FIXME hardcoded meridian
 
495
            if self.ptc.usesMeridian:
 
496
                ampm = re.search('a', parseStr)
 
497
 
 
498
                # appending the meridian to the start time
 
499
                if ampm is not None:
 
500
                    startTime, sflag = self.parse((parseStr[:m.start()] + self.ptc.meridian[0]), sourceTime)
 
501
                else:
 
502
                    startTime, sflag = self.parse((parseStr[:m.start()] + self.ptc.meridian[1]), sourceTime)
 
503
            else:
 
504
                startTime, sflag = self.parse((parseStr[:m.start()]), sourceTime)
 
505
 
 
506
            endTime, eflag = self.parse(parseStr[(m.start() + 1):], sourceTime)
 
507
 
 
508
            if eflag is False and sflag is False:
 
509
                return (startTime, endTime, False)
 
510
 
 
511
        elif rangeFlag == 4:
 
512
            # FIXME hardcoded seperator
 
513
            m                = re.search('-', parseStr)
 
514
            startDate, sflag = self.parse((parseStr[:m.start()]),       sourceTime)
 
515
            endDate, eflag   = self.parse((parseStr[(m.start() + 1):]), sourceTime)
 
516
 
 
517
            if eflag is False and sflag is False:
 
518
                return (startDate, endDate, False)
 
519
 
 
520
        elif rangeFlag == 5:
 
521
            # FIXME hardcoded seperator
 
522
            m       = re.search('-', parseStr)
 
523
            endDate = parseStr[(m.start() + 1):]
 
524
 
 
525
            # capturing the year from the end date
 
526
            date    = self.CRE_DATE3.search(endDate)
 
527
            endYear = date.group('year')
 
528
 
 
529
            # appending the year to the start date if the start date
 
530
            # does not have year information and the end date does.
 
531
            # eg : "Aug 21 - Sep 4, 2007"
 
532
            if endYear is not None:
 
533
                startDate = parseStr[:m.start()]
 
534
                date      = self.CRE_DATE3.search(startDate)
 
535
                startYear = date.group('year')
 
536
 
 
537
                if startYear is None:
 
538
                    startDate += endYear
 
539
            else:
 
540
                startDate = parseStr[:m.start()]
 
541
 
 
542
            startDate, sflag = self.parse(startDate, sourceTime)
 
543
            endDate, eflag   = self.parse(endDate, sourceTime)
 
544
 
 
545
            if eflag is False and sflag is False:
 
546
                return (startDate, endDate, False)
 
547
 
 
548
        elif rangeFlag == 6:
 
549
            # FIXME hardcoded seperator
 
550
            m = re.search('-', parseStr)
 
551
 
 
552
            startDate = parseStr[:m.start()]
 
553
 
 
554
            # capturing the month from the start date
 
555
            mth = self.CRE_DATE3.search(startDate)
 
556
            mth = mth.group('mthname')
 
557
 
 
558
            # appending the month name to the end date
 
559
            endDate = mth + parseStr[(m.start() + 1):]
 
560
 
 
561
            startDate, sflag = self.parse(startDate, sourceTime)
 
562
            endDate, eflag   = self.parse(endDate, sourceTime)
 
563
 
 
564
            if eflag is False and sflag is False:
 
565
                return (startDate, endDate, False)
 
566
        else:
 
567
            # if range is not found
 
568
            sourceTime = time.localtime()
 
569
 
 
570
            return (sourceTime, sourceTime, True)
 
571
 
 
572
 
 
573
    def _evalModifier(self, modifier, chunk1, chunk2, sourceTime):
 
574
        """
 
575
        Evaluate the modifier string and following text (passed in
 
576
        as chunk1 and chunk2) and if they match any known modifiers
 
577
        calculate the delta and apply it to sourceTime
 
578
 
 
579
        @type  modifier:   string
 
580
        @param modifier:   modifier text to apply to sourceTime
 
581
        @type  chunk1:     string
 
582
        @param chunk1:     first text chunk that followed modifier (if any)
 
583
        @type  chunk2:     string
 
584
        @param chunk2:     second text chunk that followed modifier (if any)
 
585
        @type  sourceTime: datetime
 
586
        @param sourceTime: datetime value to use as the base
 
587
 
 
588
        @rtype:  tuple
 
589
        @return: tuple of any remaining text and the modified sourceTime
 
590
        """
 
591
        offset = self.ptc.Modifiers[modifier]
 
592
 
 
593
        if sourceTime is not None:
 
594
            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
 
595
        else:
 
596
            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = time.localtime()
 
597
 
 
598
        # capture the units after the modifier and the remaining string after the unit
 
599
        m = self.CRE_REMAINING.search(chunk2)
 
600
        if m is not None:
 
601
            index  = m.start() + 1
 
602
            unit   = chunk2[:m.start()]
 
603
            chunk2 = chunk2[index:]
 
604
        else:
 
605
            unit   = chunk2
 
606
            chunk2 = ''
 
607
 
 
608
        flag = False
 
609
 
 
610
        if unit == 'month' or \
 
611
           unit == 'mth':
 
612
            if offset == 0:
 
613
                dy         = self.ptc.DaysInMonthList[mth - 1]
 
614
                sourceTime = (yr, mth, dy, 9, 0, 0, wd, yd, isdst)
 
615
            elif offset == 2:
 
616
                # if day is the last day of the month, calculate the last day of the next month
 
617
                if dy == self.ptc.DaysInMonthList[mth - 1]:
 
618
                    dy = self.ptc.DaysInMonthList[mth]
 
619
 
 
620
                start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
 
621
                target     = self.inc(start, month=1)
 
622
                sourceTime = target.timetuple()
 
623
            else:
 
624
                start      = datetime.datetime(yr, mth, 1, 9, 0, 0)
 
625
                target     = self.inc(start, month=offset)
 
626
                sourceTime = target.timetuple()
 
627
 
 
628
            flag = True
 
629
 
 
630
        if unit == 'week' or \
 
631
             unit == 'wk' or \
 
632
             unit == 'w':
 
633
            if offset == 0:
 
634
                start      = datetime.datetime(yr, mth, dy, 17, 0, 0)
 
635
                target     = start + datetime.timedelta(days=(4 - wd))
 
636
                sourceTime = target.timetuple()
 
637
            elif offset == 2:
 
638
                start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
 
639
                target     = start + datetime.timedelta(days=7)
 
640
                sourceTime = target.timetuple()
 
641
            else:
 
642
                return self._evalModifier(modifier, chunk1, "monday " + chunk2, sourceTime)
 
643
 
 
644
            flag = True
 
645
 
 
646
        if unit == 'day' or \
 
647
            unit == 'dy' or \
 
648
            unit == 'd':
 
649
            if offset == 0:
 
650
                sourceTime = (yr, mth, dy, 17, 0, 0, wd, yd, isdst)
 
651
            elif offset == 2:
 
652
                start      = datetime.datetime(yr, mth, dy, hr, mn, sec)
 
653
                target     = start + datetime.timedelta(days=1)
 
654
                sourceTime = target.timetuple()
 
655
            else:
 
656
                start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
 
657
                target     = start + datetime.timedelta(days=offset)
 
658
                sourceTime = target.timetuple()
 
659
 
 
660
            flag = True
 
661
 
 
662
        if unit == 'hour' or \
 
663
           unit == 'hr':
 
664
            if offset == 0:
 
665
                sourceTime = (yr, mth, dy, hr, 0, 0, wd, yd, isdst)
 
666
            else:
 
667
                start      = datetime.datetime(yr, mth, dy, hr, 0, 0)
 
668
                target     = start + datetime.timedelta(hours=offset)
 
669
                sourceTime = target.timetuple()
 
670
 
 
671
            flag = True
 
672
 
 
673
        if unit == 'year' or \
 
674
             unit == 'yr' or \
 
675
             unit == 'y':
 
676
            if offset == 0:
 
677
                sourceTime = (yr, 12, 31, hr, mn, sec, wd, yd, isdst)
 
678
            elif offset == 2:
 
679
                sourceTime = (yr + 1, mth, dy, hr, mn, sec, wd, yd, isdst)
 
680
            else:
 
681
                sourceTime = (yr + offset, 1, 1, 9, 0, 0, wd, yd, isdst)
 
682
 
 
683
            flag = True
 
684
 
 
685
        if flag == False:
 
686
            m = self.CRE_WEEKDAY.match(unit)
 
687
            if m is not None:
 
688
                wkdy = m.group()
 
689
                wkdy = self.ptc.WeekdayOffsets[wkdy]
 
690
 
 
691
                if offset == 0:
 
692
                    diff       = wkdy - wd
 
693
                    start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
 
694
                    target     = start + datetime.timedelta(days=diff)
 
695
                    sourceTime = target.timetuple()
 
696
                else:
 
697
                    diff       = wkdy - wd
 
698
                    start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
 
699
                    target     = start + datetime.timedelta(days=diff + 7 * offset)
 
700
                    sourceTime = target.timetuple()
 
701
 
 
702
                flag = True
 
703
 
 
704
        if not flag:
 
705
            m = self.CRE_TIME.match(unit)
 
706
            if m is not None:
 
707
                (yr, mth, dy, hr, mn, sec, wd, yd, isdst), self.invalidFlag = self.parse(unit)
 
708
                start      = datetime.datetime(yr, mth, dy, hr, mn, sec)
 
709
                target     = start + datetime.timedelta(days=offset)
 
710
                sourceTime = target.timetuple()
 
711
 
 
712
                flag              = True
 
713
                self.modifierFlag = False
 
714
 
 
715
        # if the word after next is a number, the string is likely
 
716
        # to be something like "next 4 hrs" for which we have to
 
717
        # combine the units with the rest of the string
 
718
        if not flag:
 
719
            if offset < 0:
 
720
                # if offset is negative, the unit has to be made negative
 
721
                unit = '-%s' % unit
 
722
 
 
723
            chunk2 = '%s %s' % (unit, chunk2)
 
724
 
 
725
        self.modifierFlag = False
 
726
 
 
727
        return '%s %s' % (chunk1, chunk2), sourceTime
 
728
 
 
729
 
 
730
    def _evalModifier2(self, modifier, chunk1 , chunk2, sourceTime):
 
731
        """
 
732
        Evaluate the modifier string and following text (passed in
 
733
        as chunk1 and chunk2) and if they match any known modifiers
 
734
        calculate the delta and apply it to sourceTime
 
735
 
 
736
        @type  modifier:   string
 
737
        @param modifier:   modifier text to apply to sourceTime
 
738
        @type  chunk1:     string
 
739
        @param chunk1:     first text chunk that followed modifier (if any)
 
740
        @type  chunk2:     string
 
741
        @param chunk2:     second text chunk that followed modifier (if any)
 
742
        @type  sourceTime: datetime
 
743
        @param sourceTime: datetime value to use as the base
 
744
 
 
745
        @rtype:  tuple
 
746
        @return: tuple of any remaining text and the modified sourceTime
 
747
        """
 
748
        offset = self.ptc.Modifiers[modifier]
 
749
        digit  = r'\d+'
 
750
 
 
751
        if sourceTime is not None:
 
752
            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
 
753
        else:
 
754
            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = time.localtime()
 
755
 
 
756
        self.modifier2Flag = False
 
757
 
 
758
        # If the string after the negative modifier starts with
 
759
        # digits, then it is likely that the string is similar to
 
760
        # " before 3 days" or 'evening prior to 3 days'.
 
761
        # In this case, the total time is calculated by subtracting
 
762
        # '3 days' from the current date.
 
763
        # So, we have to identify the quantity and negate it before
 
764
        # parsing the string.
 
765
        # This is not required for strings not starting with digits
 
766
        # since the string is enough to calculate the sourceTime
 
767
        if offset < 0:
 
768
            m = re.match(digit, string.strip(chunk2))
 
769
            if m is not None:
 
770
                qty    = int(m.group()) * -1
 
771
                chunk2 = chunk2[m.end():]
 
772
                chunk2 = '%d%s' % (qty, chunk2)
 
773
 
 
774
        sourceTime, flag = self.parse(chunk2, sourceTime)
 
775
 
 
776
        if chunk1 != '':
 
777
            if offset < 0:
 
778
                m = re.match(digit, string.strip(chunk1))
 
779
                if m is not None:
 
780
                    qty    = int(m.group()) * -1
 
781
                    chunk1 = chunk1[m.end():]
 
782
                    chunk1 = '%d%s' % (qty, chunk1)
 
783
 
 
784
            sourceTime, flag = self.parse(chunk1, sourceTime)
 
785
 
 
786
        return '', sourceTime
 
787
 
 
788
 
 
789
    def _evalString(self, datetimeString, sourceTime=None):
 
790
        """
 
791
        Calculate the datetime based on flags set by the L{parse()} routine
 
792
 
 
793
        Examples handled::
 
794
            RFC822, W3CDTF formatted dates
 
795
            HH:MM[:SS][ am/pm]
 
796
            MM/DD/YYYY
 
797
            DD MMMM YYYY
 
798
 
 
799
        @type  datetimeString: string
 
800
        @param datetimeString: text to try and parse as more "traditional" date/time text
 
801
        @type  sourceTime:     datetime
 
802
        @param sourceTime:     datetime value to use as the base
 
803
 
 
804
        @rtype:  datetime
 
805
        @return: calculated datetime value or current datetime if not parsed
 
806
        """
 
807
        s   = string.strip(datetimeString)
 
808
        now = time.localtime()
 
809
 
 
810
          # Given string date is a RFC822 date
 
811
        if sourceTime is None:
 
812
            sourceTime = _parse_date_rfc822(s)
 
813
 
 
814
          # Given string date is a W3CDTF date
 
815
        if sourceTime is None:
 
816
            sourceTime = _parse_date_w3dtf(s)
 
817
 
 
818
        if sourceTime is None:
 
819
            s = s.lower()
 
820
 
 
821
          # Given string is in the format HH:MM(:SS)(am/pm)
 
822
        if self.meridianFlag:
 
823
            if sourceTime is None:
 
824
                (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = now
 
825
            else:
 
826
                (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
 
827
 
 
828
            m = self.CRE_TIMEHMS2.search(s)
 
829
            if m is not None:
 
830
                dt = s[:m.start('meridian')].strip()
 
831
                if len(dt) <= 2:
 
832
                    hr  = int(dt)
 
833
                    mn  = 0
 
834
                    sec = 0
 
835
                else:
 
836
                    hr, mn, sec = _extract_time(m)
 
837
 
 
838
                if hr == 24:
 
839
                    hr = 0
 
840
 
 
841
                sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
 
842
                meridian   = m.group('meridian').lower()
 
843
 
 
844
                  # if 'am' found and hour is 12 - force hour to 0 (midnight)
 
845
                if (meridian in self.ptc.am) and hr == 12:
 
846
                    sourceTime = (yr, mth, dy, 0, mn, sec, wd, yd, isdst)
 
847
 
 
848
                  # if 'pm' found and hour < 12, add 12 to shift to evening
 
849
                if (meridian in self.ptc.pm) and hr < 12:
 
850
                    sourceTime = (yr, mth, dy, hr + 12, mn, sec, wd, yd, isdst)
 
851
 
 
852
              # invalid time
 
853
            if hr > 24 or mn > 59 or sec > 59:
 
854
                sourceTime       = now
 
855
                self.invalidFlag = True
 
856
 
 
857
            self.meridianFlag = False
 
858
 
 
859
          # Given string is in the format HH:MM(:SS)
 
860
        if self.timeFlag:
 
861
            if sourceTime is None:
 
862
                (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = now
 
863
            else:
 
864
                (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
 
865
 
 
866
            m = self.CRE_TIMEHMS.search(s)
 
867
            if m is not None:
 
868
                hr, mn, sec = _extract_time(m)
 
869
            if hr == 24:
 
870
                hr = 0
 
871
 
 
872
            if hr > 24 or mn > 59 or sec > 59:
 
873
                # invalid time
 
874
                sourceTime       = now
 
875
                self.invalidFlag = True
 
876
            else:
 
877
                sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
 
878
 
 
879
            self.timeFlag = False
 
880
 
 
881
          # Given string is in the format 07/21/2006
 
882
        if self.dateStdFlag:
 
883
            sourceTime       = self.parseDate(s)
 
884
            self.dateStdFlag = False
 
885
 
 
886
          # Given string is in the format  "May 23rd, 2005"
 
887
        if self.dateStrFlag:
 
888
            sourceTime       = self.parseDateText(s)
 
889
            self.dateStrFlag = False
 
890
 
 
891
          # Given string is a weekday
 
892
        if self.weekdyFlag:
 
893
            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = now
 
894
 
 
895
            start = datetime.datetime(yr, mth, dy, hr, mn, sec)
 
896
            wkDy  = self.ptc.WeekdayOffsets[s]
 
897
 
 
898
            if wkDy > wd:
 
899
                qty    = wkDy - wd
 
900
                target = start + datetime.timedelta(days=qty)
 
901
                wd     = wkDy
 
902
            else:
 
903
                qty    = 6 - wd + wkDy + 1
 
904
                target = start + datetime.timedelta(days=qty)
 
905
                wd     = wkDy
 
906
 
 
907
            sourceTime      = target.timetuple()
 
908
            self.weekdyFlag = False
 
909
 
 
910
          # Given string is a natural language time string like lunch, midnight, etc
 
911
        if self.timeStrFlag:
 
912
            if s in self.ptc.re_values['now']:
 
913
                sourceTime = now
 
914
            else:
 
915
                sources = self.ptc.buildSources(now)
 
916
 
 
917
                if s in sources:
 
918
                    sourceTime = sources[s]
 
919
                else:
 
920
                    sourceTime       = now
 
921
                    self.invalidFlag = True
 
922
 
 
923
            self.timeStrFlag = False
 
924
 
 
925
           # Given string is a natural language date string like today, tomorrow..
 
926
        if self.dayStrFlag:
 
927
            if sourceTime is None:
 
928
                sourceTime = now
 
929
 
 
930
            (yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
 
931
 
 
932
            if s in self.ptc.dayOffsets:
 
933
                offset = self.ptc.dayOffsets[s]
 
934
            else:
 
935
                offset = 0
 
936
 
 
937
            start      = datetime.datetime(yr, mth, dy, 9, 0, 0)
 
938
            target     = start + datetime.timedelta(days=offset)
 
939
            sourceTime = target.timetuple()
 
940
 
 
941
            self.dayStrFlag = False
 
942
 
 
943
          # Given string is a time string with units like "5 hrs 30 min"
 
944
        if self.unitsFlag:
 
945
            modifier = ''  # TODO
 
946
 
 
947
            if sourceTime is None:
 
948
                sourceTime = now
 
949
 
 
950
            m = self.CRE_UNITS.search(s)
 
951
            if m is not None:
 
952
                units    = m.group('units')
 
953
                quantity = s[:m.start('units')]
 
954
 
 
955
            sourceTime     = self._buildTime(sourceTime, quantity, modifier, units)
 
956
            self.unitsFlag = False
 
957
 
 
958
          # Given string is a time string with single char units like "5 h 30 m"
 
959
        if self.qunitsFlag:
 
960
            modifier = ''  # TODO
 
961
 
 
962
            if sourceTime is None:
 
963
                sourceTime = now
 
964
 
 
965
            m = self.CRE_QUNITS.search(s)
 
966
            if m is not None:
 
967
                units    = m.group('qunits')
 
968
                quantity = s[:m.start('qunits')]
 
969
 
 
970
            sourceTime      = self._buildTime(sourceTime, quantity, modifier, units)
 
971
            self.qunitsFlag = False
 
972
 
 
973
          # Given string does not match anything
 
974
        if sourceTime is None:
 
975
            sourceTime       = now
 
976
            self.invalidFlag = True
 
977
 
 
978
        return sourceTime
 
979
 
 
980
 
 
981
    def parse(self, datetimeString, sourceTime=None):
 
982
        """
 
983
        Splits the L{datetimeString} into tokens, finds the regex patters
 
984
        that match and then calculates a datetime value from the chunks
 
985
 
 
986
        if L{sourceTime} is given then the datetime value will be calcualted
 
987
        from that datetime, otherwise from the current datetime.
 
988
 
 
989
        @type  datetimeString: string
 
990
        @param datetimeString: datetime text to evaluate
 
991
        @type  sourceTime:     datetime
 
992
        @param sourceTime:     datetime value to use as the base
 
993
 
 
994
        @rtype:  tuple
 
995
        @return: tuple of any remaining text and the modified sourceTime
 
996
        """
 
997
        s         = string.strip(datetimeString.lower())
 
998
        dateStr   = ''
 
999
        parseStr  = ''
 
1000
        totalTime = sourceTime
 
1001
 
 
1002
        self.invalidFlag = False
 
1003
 
 
1004
        if s == '' :
 
1005
            if sourceTime is not None:
 
1006
                return (sourceTime, False)
 
1007
            else:
 
1008
                return (time.localtime(), True)
 
1009
 
 
1010
        while len(s) > 0:
 
1011
            flag   = False
 
1012
            chunk1 = ''
 
1013
            chunk2 = ''
 
1014
 
 
1015
            if _debug:
 
1016
                print 'parse (top of loop): [%s][%s]' % (s, parseStr)
 
1017
 
 
1018
            if parseStr == '':
 
1019
                # Modifier like next\prev..
 
1020
                m = self.CRE_MODIFIER.search(s)
 
1021
                if m is not None:
 
1022
                    self.modifierFlag = True
 
1023
                    if (m.group('modifier') != s):
 
1024
                        # capture remaining string
 
1025
                        parseStr = m.group('modifier')
 
1026
                        chunk1   = string.strip(s[:m.start('modifier')])
 
1027
                        chunk2   = string.strip(s[m.end('modifier'):])
 
1028
                        flag     = True
 
1029
                    else:
 
1030
                        parseStr = s
 
1031
 
 
1032
            if parseStr == '':
 
1033
                # Modifier like from\after\prior..
 
1034
                m = self.CRE_MODIFIER2.search(s)
 
1035
                if m is not None:
 
1036
                    self.modifier2Flag = True
 
1037
                    if (m.group('modifier') != s):
 
1038
                        # capture remaining string
 
1039
                        parseStr = m.group('modifier')
 
1040
                        chunk1   = string.strip(s[:m.start('modifier')])
 
1041
                        chunk2   = string.strip(s[m.end('modifier'):])
 
1042
                        flag     = True
 
1043
                    else:
 
1044
                        parseStr = s
 
1045
 
 
1046
            if parseStr == '':
 
1047
                # String date format
 
1048
                m = self.CRE_DATE3.search(s)
 
1049
                if m is not None:
 
1050
                    self.dateStrFlag = True
 
1051
                    if (m.group('date') != s):
 
1052
                        # capture remaining string
 
1053
                        parseStr = m.group('date')
 
1054
                        chunk1   = s[:m.start('date')]
 
1055
                        chunk2   = s[m.end('date'):]
 
1056
                        s        = '%s %s' % (chunk1, chunk2)
 
1057
                        flag     = True
 
1058
                    else:
 
1059
                        parseStr = s
 
1060
 
 
1061
            if parseStr == '':
 
1062
                # Standard date format
 
1063
                m = self.CRE_DATE.search(s)
 
1064
                if m is not None:
 
1065
                    self.dateStdFlag = True
 
1066
                    if (m.group('date') != s):
 
1067
                        # capture remaining string
 
1068
                        parseStr = m.group('date')
 
1069
                        chunk1   = s[:m.start('date')]
 
1070
                        chunk2   = s[m.end('date'):]
 
1071
                        s        = '%s %s' % (chunk1, chunk2)
 
1072
                        flag     = True
 
1073
                    else:
 
1074
                        parseStr = s
 
1075
 
 
1076
            if parseStr == '':
 
1077
                # Natural language day strings
 
1078
                m = self.CRE_DAY.search(s)
 
1079
                if m is not None:
 
1080
                    self.dayStrFlag = True
 
1081
                    if (m.group('day') != s):
 
1082
                        # capture remaining string
 
1083
                        parseStr = m.group('day')
 
1084
                        chunk1   = s[:m.start('day')]
 
1085
                        chunk2   = s[m.end('day'):]
 
1086
                        s        = '%s %s' % (chunk1, chunk2)
 
1087
                        flag     = True
 
1088
                    else:
 
1089
                        parseStr = s
 
1090
 
 
1091
            if parseStr == '':
 
1092
                # Quantity + Units
 
1093
                m = self.CRE_UNITS.search(s)
 
1094
                if m is not None:
 
1095
                    self.unitsFlag = True
 
1096
                    if (m.group('qty') != s):
 
1097
                        # capture remaining string
 
1098
                        parseStr = m.group('qty')
 
1099
                        chunk1   = s[:m.start('qty')].strip()
 
1100
                        chunk2   = s[m.end('qty'):].strip()
 
1101
 
 
1102
                        if chunk1[-1:] == '-':
 
1103
                            parseStr = '-%s' % parseStr
 
1104
                            chunk1   = chunk1[:-1]
 
1105
 
 
1106
                        s    = '%s %s' % (chunk1, chunk2)
 
1107
                        flag = True
 
1108
                    else:
 
1109
                        parseStr = s
 
1110
 
 
1111
            if parseStr == '':
 
1112
                # Quantity + Units
 
1113
                m = self.CRE_QUNITS.search(s)
 
1114
                if m is not None:
 
1115
                    self.qunitsFlag = True
 
1116
                    if (m.group('qty') != s):
 
1117
                        # capture remaining string
 
1118
                        parseStr = m.group('qty')
 
1119
                        chunk1   = s[:m.start('qty')].strip()
 
1120
                        chunk2   = s[m.end('qty'):].strip()
 
1121
 
 
1122
                        if chunk1[-1:] == '-':
 
1123
                            parseStr = '-%s' % parseStr
 
1124
                            chunk1   = chunk1[:-1]
 
1125
 
 
1126
                        s    = '%s %s' % (chunk1, chunk2)
 
1127
                        flag = True
 
1128
                    else:
 
1129
                        parseStr = s 
 
1130
 
 
1131
            if parseStr == '':
 
1132
                # Weekday
 
1133
                m = self.CRE_WEEKDAY.search(s)
 
1134
                if m is not None:
 
1135
                    self.weekdyFlag = True
 
1136
                    if (m.group('weekday') != s):
 
1137
                        # capture remaining string
 
1138
                        parseStr = m.group()
 
1139
                        chunk1   = s[:m.start('weekday')]
 
1140
                        chunk2   = s[m.end('weekday'):]
 
1141
                        s        = '%s %s' % (chunk1, chunk2)
 
1142
                        flag     = True
 
1143
                    else:
 
1144
                        parseStr = s
 
1145
 
 
1146
            if parseStr == '':
 
1147
                # Natural language time strings
 
1148
                m = self.CRE_TIME.search(s)
 
1149
                if m is not None:
 
1150
                    self.timeStrFlag = True
 
1151
                    if (m.group('time') != s):
 
1152
                        # capture remaining string
 
1153
                        parseStr = m.group('time')
 
1154
                        chunk1   = s[:m.start('time')]
 
1155
                        chunk2   = s[m.end('time'):]
 
1156
                        s        = '%s %s' % (chunk1, chunk2)
 
1157
                        flag     = True
 
1158
                    else:
 
1159
                        parseStr = s
 
1160
 
 
1161
            if parseStr == '':
 
1162
                # HH:MM(:SS) am/pm time strings
 
1163
                m = self.CRE_TIMEHMS2.search(s)
 
1164
                if m is not None:
 
1165
                    self.meridianFlag = True
 
1166
                    if m.group('minutes') is not None:
 
1167
                        if m.group('seconds') is not None:
 
1168
                            parseStr = '%s:%s:%s %s' % (m.group('hours'), m.group('minutes'), m.group('seconds'), m.group('meridian'))
 
1169
                        else:
 
1170
                            parseStr = '%s:%s %s' % (m.group('hours'), m.group('minutes'), m.group('meridian'))
 
1171
                    else:
 
1172
                        parseStr = '%s %s' % (m.group('hours'), m.group('meridian'))
 
1173
 
 
1174
                    chunk1 = s[:m.start('hours')]
 
1175
                    chunk2 = s[m.end('meridian'):]
 
1176
 
 
1177
                    s    = '%s %s' % (chunk1, chunk2)
 
1178
                    flag = True
 
1179
 
 
1180
            if parseStr == '':
 
1181
                # HH:MM(:SS) time strings
 
1182
                m = self.CRE_TIMEHMS.search(s)
 
1183
                if m is not None:
 
1184
                    self.timeFlag = True
 
1185
                    if m.group('seconds') is not None:
 
1186
                        parseStr = '%s:%s:%s' % (m.group('hours'), m.group('minutes'), m.group('seconds'))
 
1187
                        chunk1   = s[:m.start('hours')]
 
1188
                        chunk2   = s[m.end('seconds'):]
 
1189
                    else:
 
1190
                        parseStr = '%s:%s' % (m.group('hours'), m.group('minutes'))
 
1191
                        chunk1   = s[:m.start('hours')]
 
1192
                        chunk2   = s[m.end('minutes'):]
 
1193
 
 
1194
                    s    = '%s %s' % (chunk1, chunk2)
 
1195
                    flag = True
 
1196
 
 
1197
            # if string does not match any regex, empty string to come out of the while loop
 
1198
            if not flag:
 
1199
                s = ''
 
1200
 
 
1201
            if _debug:
 
1202
                print 'parse (bottom) [%s][%s][%s][%s]' % (s, parseStr, chunk1, chunk2)
 
1203
                print 'invalid %s, weekday %s, dateStd %s, dateStr %s, time %s, timeStr %s, meridian %s' % \
 
1204
                       (self.invalidFlag, self.weekdyFlag, self.dateStdFlag, self.dateStrFlag, self.timeFlag, self.timeStrFlag, self.meridianFlag)
 
1205
                print 'dayStr %s, modifier %s, modifier2 %s, units %s, qunits %s' % \
 
1206
                       (self.dayStrFlag, self.modifierFlag, self.modifier2Flag, self.unitsFlag, self.qunitsFlag)
 
1207
 
 
1208
            # evaluate the matched string
 
1209
            if parseStr != '':
 
1210
                if self.modifierFlag == True:
 
1211
                    t, totalTime = self._evalModifier(parseStr, chunk1, chunk2, totalTime)
 
1212
 
 
1213
                    return self.parse(t, totalTime)
 
1214
 
 
1215
                elif self.modifier2Flag == True:
 
1216
                    s, totalTime = self._evalModifier2(parseStr, chunk1, chunk2, totalTime)
 
1217
                else:
 
1218
                    totalTime = self._evalString(parseStr, totalTime)
 
1219
                    parseStr  = ''
 
1220
 
 
1221
        # String is not parsed at all
 
1222
        if totalTime is None or totalTime == sourceTime:
 
1223
            totalTime        = time.localtime()
 
1224
            self.invalidFlag = True
 
1225
 
 
1226
        return (totalTime, self.invalidFlag)
 
1227
 
 
1228
 
 
1229
    def inc(self, source, month=None, year=None):
 
1230
        """
 
1231
        Takes the given date, or current date if none is passed, and
 
1232
        increments it according to the values passed in by month
 
1233
        and/or year.
 
1234
 
 
1235
        This routine is needed because the timedelta() routine does
 
1236
        not allow for month or year increments.
 
1237
 
 
1238
        @type  source: datetime
 
1239
        @param source: datetime value to increment
 
1240
        @type  month:  integer
 
1241
        @param month:  optional number of months to increment
 
1242
        @type  year:   integer
 
1243
        @param year:   optional number of years to increment
 
1244
 
 
1245
        @rtype:  datetime
 
1246
        @return: L{source} incremented by the number of months and/or years
 
1247
        """
 
1248
        yr  = source.year
 
1249
        mth = source.month
 
1250
 
 
1251
        if year:
 
1252
            try:
 
1253
                yi = int(year)
 
1254
            except ValueError:
 
1255
                yi = 0
 
1256
 
 
1257
            yr += yi
 
1258
 
 
1259
        if month:
 
1260
            try:
 
1261
                mi = int(month)
 
1262
            except ValueError:
 
1263
                mi = 0
 
1264
 
 
1265
            m = abs(mi)
 
1266
            y = m / 12      # how many years are in month increment
 
1267
            m = m % 12      # get remaining months
 
1268
 
 
1269
            if mi < 0:
 
1270
                mth = mth - m           # sub months from start month
 
1271
                if mth < 1:             # cross start-of-year?
 
1272
                    y   -= 1            #   yes - decrement year
 
1273
                    mth += 12           #         and fix month
 
1274
            else:
 
1275
                mth = mth + m           # add months to start month
 
1276
                if mth > 12:            # cross end-of-year?
 
1277
                    y   += 1            #   yes - increment year
 
1278
                    mth -= 12           #         and fix month
 
1279
 
 
1280
            yr += y
 
1281
 
 
1282
        d = source.replace(year=yr, month=mth)
 
1283
 
 
1284
        return source + (d - source)
 
1285