4
Parse human-readable date/time text.
8
Copyright (c) 2004-2008 Mike Taylor
9
Copyright (c) 2006-2008 Darshana Chhajed
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
16
http://www.apache.org/licenses/LICENSE-2.0
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.
32
import parsedatetime_consts
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()
40
year = int(m.group('year'))
42
year = 100 * int(time.gmtime()[0] / 100) + int(year)
45
julian = m.group('julian')
48
month = julian / 30 + 1
52
t = time.mktime((year, month, day, 0, 0, 0, 0, 0, 0))
53
jday = time.gmtime(t)[-2]
54
diff = abs(jday - julian)
66
return year, month, day
67
month = m.group('month')
78
return year, month, day
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()
87
hours = m.group('hours')
91
minutes = int(m.group('minutes'))
92
seconds = m.group('seconds')
94
seconds = int(seconds)
97
return hours, minutes, seconds
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
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
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.'''
122
hours = int(m.group('tzdhours'))
123
minutes = m.group('tzdminutes')
125
minutes = int(minutes)
128
offset = (hours*60 + minutes) * 60
133
__date_re = ('(?P<year>\d\d\d\d)'
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+)?))?'
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)
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
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:
163
data[3:] = [s[:i], s[i+1:]]
166
dateString = " ".join(data)
168
dateString += ' 00:00:00 GMT'
169
return rfc822.parsedate_tz(dateString)
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,
176
rfc822._timezones.update(_additional_timezones)
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.
185
def __init__(self, constants=None):
187
Default constructor for the L{Calendar} class.
189
@type constants: object
190
@param constants: Instance of the class L{parsedatetime_consts.Constants}
193
@return: L{Calendar} instance
195
# if a constants reference is not included, use default
196
if constants is None:
197
self.ptc = parsedatetime_consts.Constants()
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..
217
def _convertUnitAsWords(self, unitText):
219
Converts text units into their number value
223
Two hundred twenty five = 225
224
Two thousand and twenty five = 2025
225
Two thousand twenty five = 2025
227
@type unitText: string
228
@param unitText: number text to convert
231
@return: numerical value of unitText
233
# TODO: implement this
237
def _buildTime(self, source, quantity, modifier, units):
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.
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
249
@param units: unit of the quantity (i.e. hours, days, months, etc)
252
@return: C{struct_time} of the calculated time
255
print '_buildTime: [%s][%s][%s]' % (quantity, modifier, units)
258
source = time.localtime()
263
quantity = quantity.strip()
265
if len(quantity) == 0:
273
if modifier in self.ptc.Modifiers:
274
qty = qty * self.ptc.Modifiers[modifier]
276
if units is None or units == '':
279
# plurals are handled by regex's (could be a bug tho)
281
(yr, mth, dy, hr, mn, sec, _, _, _) = source
283
start = datetime.datetime(yr, mth, dy, hr, mn, sec)
286
if units.startswith('y'):
287
target = self.inc(start, year=qty)
289
elif units.endswith('th') or units.endswith('ths'):
290
target = self.inc(start, month=qty)
293
if units.startswith('d'):
294
target = start + datetime.timedelta(days=qty)
296
elif units.startswith('h'):
297
target = start + datetime.timedelta(hours=qty)
299
elif units.startswith('m'):
300
target = start + datetime.timedelta(minutes=qty)
302
elif units.startswith('s'):
303
target = start + datetime.timedelta(seconds=qty)
305
elif units.startswith('w'):
306
target = start + datetime.timedelta(weeks=qty)
309
return target.timetuple()
312
def parseDate(self, dateString):
314
Parse short-form date strings::
316
'05/28/2006' or '04.21'
318
@type dateString: string
319
@param dateString: text to convert to a C{datetime}
322
@return: calculated C{struct_time} value of dateString
324
yr, mth, dy, hr, mn, sec, wd, yd, isdst = time.localtime()
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
335
m = self.ptc.CRE_DATE2.search(s)
341
m = self.ptc.CRE_DATE2.search(s)
345
v3 = int(s[index + 1:])
350
d = { 'm': mth, 'd': dy, 'y': yr }
352
for i in range(0, 3):
354
c = self.ptc.dp_order[i]
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'])):
368
# birthday epoch constraint
369
if yr < self.ptc.BirthdayEpoch:
375
print 'parseDate: ', yr, mth, dy, self.ptc.daysInMonth(mth, yr)
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)
383
sourceTime = time.localtime() # return current time if date
389
def parseDateText(self, dateString):
391
Parse long-form date strings::
397
@type dateString: string
398
@param dateString: text to convert to a datetime
401
@return: calculated C{struct_time} value of dateString
403
yr, mth, dy, hr, mn, sec, wd, yd, isdst = time.localtime()
408
s = dateString.lower()
409
m = self.ptc.CRE_DATE3.search(s)
410
mth = m.group('mthname')
411
mth = self.ptc.MonthOffsets[mth]
413
if m.group('day') != None:
414
dy = int(m.group('day'))
418
if m.group('year') != None:
419
yr = int(m.group('year'))
421
# birthday epoch constraint
422
if yr < self.ptc.BirthdayEpoch:
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
432
if dy > 0 and dy <= self.ptc.daysInMonth(mth, yr):
433
sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
435
# Return current time if date string is invalid
438
sourceTime = time.localtime()
443
def evalRanges(self, datetimeString, sourceTime=None):
445
Evaluate the C{datetimeString} text and determine if
446
it represents a date or time range.
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
454
@return: tuple of: start datetime, end datetime and the invalid flag
462
s = datetimeString.strip().lower()
464
if self.ptc.rangeSep in s:
465
s = s.replace(self.ptc.rangeSep, ' %s ' % self.ptc.rangeSep)
466
s = s.replace(' ', ' ')
468
m = self.ptc.CRE_TIMERNG1.search(s)
472
m = self.ptc.CRE_TIMERNG2.search(s)
476
m = self.ptc.CRE_TIMERNG4.search(s)
480
m = self.ptc.CRE_TIMERNG3.search(s)
484
m = self.ptc.CRE_DATERNG1.search(s)
488
m = self.ptc.CRE_DATERNG2.search(s)
492
m = self.ptc.CRE_DATERNG3.search(s)
497
print 'evalRanges: rangeFlag =', rangeFlag, '[%s]' % s
501
# capture remaining string
503
chunk1 = s[:m.start()]
505
s = '%s %s' % (chunk1, chunk2)
508
sourceTime, flag = self.parse(s, sourceTime)
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)
520
if (eflag != 0) and (sflag != 0):
521
return (startTime, endTime, 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)
528
if (eflag != 0) and (sflag != 0):
529
return (startTime, endTime, 2)
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)
537
# appending the meridian to the start time
539
startTime, sflag = self.parse((parseStr[:m.start()] + self.ptc.meridian[0]), sourceTime)
541
startTime, sflag = self.parse((parseStr[:m.start()] + self.ptc.meridian[1]), sourceTime)
543
startTime, sflag = self.parse((parseStr[:m.start()]), sourceTime)
545
endTime, eflag = self.parse(parseStr[(m.start() + 1):], sourceTime)
547
if (eflag != 0) and (sflag != 0):
548
return (startTime, endTime, 2)
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)
555
if (eflag != 0) and (sflag != 0):
556
return (startDate, endDate, 1)
559
m = re.search(self.ptc.rangeSep, parseStr)
560
endDate = parseStr[(m.start() + 1):]
562
# capturing the year from the end date
563
date = self.ptc.CRE_DATE3.search(endDate)
564
endYear = date.group('year')
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')
574
if startYear is None:
575
startDate = startDate + ', ' + endYear
577
startDate = parseStr[:m.start()]
579
startDate, sflag = self.parse(startDate, sourceTime)
580
endDate, eflag = self.parse(endDate, sourceTime)
582
if (eflag != 0) and (sflag != 0):
583
return (startDate, endDate, 1)
586
m = re.search(self.ptc.rangeSep, parseStr)
588
startDate = parseStr[:m.start()]
590
# capturing the month from the start date
591
mth = self.ptc.CRE_DATE3.search(startDate)
592
mth = mth.group('mthname')
594
# appending the month name to the end date
595
endDate = mth + parseStr[(m.start() + 1):]
597
startDate, sflag = self.parse(startDate, sourceTime)
598
endDate, eflag = self.parse(endDate, sourceTime)
600
if (eflag != 0) and (sflag != 0):
601
return (startDate, endDate, 1)
603
# if range is not found
604
sourceTime = time.localtime()
606
return (sourceTime, sourceTime, 0)
609
def _CalculateDOWDelta(self, wd, wkdy, offset, style, currentDayStyle):
611
Based on the C{style} and C{currentDayStyle} determine what
612
day-of-week value is to be returned.
615
@param wd: day-of-week value for the current day
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)
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}
626
@return: calculated day-of-week
629
# modifier is indicating future week eg: "next".
630
# DOW is calculated as DOW of next week
634
# modifier is indicating past week eg: "last","previous"
635
# DOW is calculated as DOW of previous week
639
# modifier is indiacting current week eg: "this"
640
# DOW is calculated as DOW of this week
644
# no modifier is present.
645
# i.e. string to be parsed is just DOW
647
# next occurance of the DOW is calculated
648
if currentDayStyle == True:
660
# last occurance of the DOW is calculated
661
if currentDayStyle == True:
672
# occurance of the DOW in the current week is calculated
676
print "wd %s, wkdy %s, offset %d, style %d\n" % (wd, wkdy, offset, style)
681
def _evalModifier(self, modifier, chunk1, chunk2, sourceTime):
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}.
687
@type modifier: string
688
@param modifier: modifier text to apply to sourceTime
690
@param chunk1: first text chunk that followed modifier (if any)
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
697
@return: tuple of: remaining text and the modified sourceTime
699
offset = self.ptc.Modifiers[modifier]
701
if sourceTime is not None:
702
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
704
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = time.localtime()
706
# capture the units after the modifier and the remaining
707
# string after the unit
708
m = self.ptc.CRE_REMAINING.search(chunk2)
710
index = m.start() + 1
711
unit = chunk2[:m.start()]
712
chunk2 = chunk2[index:]
719
if unit == 'month' or \
723
dy = self.ptc.daysInMonth(mth, yr)
724
sourceTime = (yr, mth, dy, 9, 0, 0, wd, yd, isdst)
726
# if day is the last day of the month, calculate the last day
728
if dy == self.ptc.daysInMonth(mth, yr):
729
dy = self.ptc.daysInMonth(mth + 1, yr)
731
start = datetime.datetime(yr, mth, dy, 9, 0, 0)
732
target = self.inc(start, month=1)
733
sourceTime = target.timetuple()
735
start = datetime.datetime(yr, mth, 1, 9, 0, 0)
736
target = self.inc(start, month=offset)
737
sourceTime = target.timetuple()
742
if unit == 'week' or \
746
start = datetime.datetime(yr, mth, dy, 17, 0, 0)
747
target = start + datetime.timedelta(days=(4 - wd))
748
sourceTime = target.timetuple()
750
start = datetime.datetime(yr, mth, dy, 9, 0, 0)
751
target = start + datetime.timedelta(days=7)
752
sourceTime = target.timetuple()
754
return self._evalModifier(modifier, chunk1, "monday " + chunk2, sourceTime)
759
if unit == 'day' or \
763
sourceTime = (yr, mth, dy, 17, 0, 0, wd, yd, isdst)
766
start = datetime.datetime(yr, mth, dy, hr, mn, sec)
767
target = start + datetime.timedelta(days=1)
768
sourceTime = target.timetuple()
770
start = datetime.datetime(yr, mth, dy, 9, 0, 0)
771
target = start + datetime.timedelta(days=offset)
772
sourceTime = target.timetuple()
777
if unit == 'hour' or \
780
sourceTime = (yr, mth, dy, hr, 0, 0, wd, yd, isdst)
782
start = datetime.datetime(yr, mth, dy, hr, 0, 0)
783
target = start + datetime.timedelta(hours=offset)
784
sourceTime = target.timetuple()
789
if unit == 'year' or \
793
sourceTime = (yr, 12, 31, hr, mn, sec, wd, yd, isdst)
795
sourceTime = (yr + 1, mth, dy, hr, mn, sec, wd, yd, isdst)
797
sourceTime = (yr + offset, 1, 1, 9, 0, 0, wd, yd, isdst)
803
m = self.ptc.CRE_WEEKDAY.match(unit)
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)
815
if modifier in sources:
816
sourceTime = sources[modifier]
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()
831
m = self.ptc.CRE_TIME.match(unit)
833
self.modifierFlag = False
834
(yr, mth, dy, hr, mn, sec, wd, yd, isdst), _ = self.parse(unit)
836
start = datetime.datetime(yr, mth, dy, hr, mn, sec)
837
target = start + datetime.timedelta(days=offset)
838
sourceTime = target.timetuple()
841
self.modifierFlag = False
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)
850
sources = self.ptc.buildSources(sourceTime)
852
if modifier in sources:
853
sourceTime = sources[modifier]
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
862
# if offset is negative, the unit has to be made negative
865
chunk2 = '%s %s' % (unit, chunk2)
867
self.modifierFlag = False
869
#return '%s %s' % (chunk1, chunk2), sourceTime
870
return '%s' % chunk2, sourceTime
872
def _evalModifier2(self, modifier, chunk1 , chunk2, sourceTime):
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}.
878
@type modifier: string
879
@param modifier: modifier text to apply to C{sourceTime}
881
@param chunk1: first text chunk that followed modifier (if any)
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
888
@return: tuple of: remaining text and the modified sourceTime
890
offset = self.ptc.Modifiers[modifier]
893
self.modifier2Flag = False
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
902
# This is not required for strings not starting with digits since the
903
# string is enough to calculate the sourceTime
906
m = re.match(digit, chunk2.strip())
908
qty = int(m.group()) * -1
909
chunk2 = chunk2[m.end():]
910
chunk2 = '%d%s' % (qty, chunk2)
912
sourceTime, flag1 = self.parse(chunk2, sourceTime)
923
m = re.search(digit, chunk1.strip())
925
qty = int(m.group()) * -1
926
chunk1 = chunk1[m.end():]
927
chunk1 = '%d%s' % (qty, chunk1)
929
tempDateFlag = self.dateFlag
930
tempTimeFlag = self.timeFlag
931
sourceTime2, flag2 = self.parse(chunk1, sourceTime)
933
return sourceTime, (flag1 and flag2)
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
940
self.timeFlag = tempTimeFlag
941
self.dateFlag = tempDateFlag
943
return sourceTime, (flag1 and flag2)
946
def _evalString(self, datetimeString, sourceTime=None):
948
Calculate the datetime based on flags set by the L{parse()} routine
951
RFC822, W3CDTF formatted dates
956
@type datetimeString: string
957
@param datetimeString: text to try and parse as more "traditional"
959
@type sourceTime: struct_time
960
@param sourceTime: C{struct_time} value to use as the base
963
@return: calculated C{struct_time} value or current C{struct_time}
966
s = datetimeString.strip()
967
now = time.localtime()
969
# Given string date is a RFC822 date
970
if sourceTime is None:
971
sourceTime = _parse_date_rfc822(s)
973
if sourceTime is not None:
974
(yr, mth, dy, hr, mn, sec, wd, yd, isdst, _) = sourceTime
977
if (hr != 0) and (mn != 0) and (sec != 0):
980
sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
982
# Given string date is a W3CDTF date
983
if sourceTime is None:
984
sourceTime = _parse_date_w3dtf(s)
986
if sourceTime is not None:
990
if sourceTime is None:
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
998
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
1000
m = self.ptc.CRE_TIMEHMS2.search(s)
1002
dt = s[:m.start('meridian')].strip()
1008
hr, mn, sec = _extract_time(m)
1013
sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
1014
meridian = m.group('meridian').lower()
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)
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)
1025
if hr > 24 or mn > 59 or sec > 59:
1030
self.meridianFlag = False
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
1037
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
1039
m = self.ptc.CRE_TIMEHMS.search(s)
1041
hr, mn, sec = _extract_time(m)
1045
if hr > 24 or mn > 59 or sec > 59:
1051
sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
1053
self.timeStdFlag = False
1055
# Given string is in the format 07/21/2006
1056
if self.dateStdFlag:
1057
sourceTime = self.parseDate(s)
1058
self.dateStdFlag = False
1060
# Given string is in the format "May 23rd, 2005"
1061
if self.dateStrFlag:
1062
sourceTime = self.parseDateText(s)
1063
self.dateStrFlag = False
1065
# Given string is a weekday
1067
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = now
1069
start = datetime.datetime(yr, mth, dy, hr, mn, sec)
1070
wkdy = self.ptc.WeekdayOffsets[s]
1073
qty = self._CalculateDOWDelta(wd, wkdy, 2,
1074
self.ptc.DOWParseStyle,
1075
self.ptc.CurrentDOWParseStyle)
1077
qty = self._CalculateDOWDelta(wd, wkdy, 2,
1078
self.ptc.DOWParseStyle,
1079
self.ptc.CurrentDOWParseStyle)
1081
target = start + datetime.timedelta(days=qty)
1084
sourceTime = target.timetuple()
1085
self.weekdyFlag = False
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']:
1093
sources = self.ptc.buildSources(sourceTime)
1096
sourceTime = sources[s]
1102
self.timeStrFlag = False
1104
# Given string is a natural language date string like today, tomorrow..
1106
if sourceTime is None:
1109
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
1111
if s in self.ptc.dayOffsets:
1112
offset = self.ptc.dayOffsets[s]
1116
start = datetime.datetime(yr, mth, dy, 9, 0, 0)
1117
target = start + datetime.timedelta(days=offset)
1118
sourceTime = target.timetuple()
1120
self.dayStrFlag = False
1122
# Given string is a time string with units like "5 hrs 30 min"
1124
modifier = '' # TODO
1126
if sourceTime is None:
1129
m = self.ptc.CRE_UNITS.search(s)
1131
units = m.group('units')
1132
quantity = s[:m.start('units')]
1134
sourceTime = self._buildTime(sourceTime, quantity, modifier, units)
1135
self.unitsFlag = False
1137
# Given string is a time string with single char units like "5 h 30 m"
1139
modifier = '' # TODO
1141
if sourceTime is None:
1144
m = self.ptc.CRE_QUNITS.search(s)
1146
units = m.group('qunits')
1147
quantity = s[:m.start('qunits')]
1149
sourceTime = self._buildTime(sourceTime, quantity, modifier, units)
1150
self.qunitsFlag = False
1152
# Given string does not match anything
1153
if sourceTime is None:
1161
def parse(self, datetimeString, sourceTime=None):
1163
Splits the given C{datetimeString} into tokens, finds the regex
1164
patterns that match and then calculates a C{struct_time} value from
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.
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::
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}
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
1185
@return: tuple of: modified C{sourceTime} and the result flag
1189
if isinstance(sourceTime, datetime.datetime):
1191
print 'coercing datetime to timetuple'
1192
sourceTime = sourceTime.timetuple()
1194
if not isinstance(sourceTime, time.struct_time) and \
1195
not isinstance(sourceTime, tuple):
1196
raise Exception('sourceTime is not a struct_time')
1198
s = datetimeString.strip().lower()
1200
totalTime = sourceTime
1203
if sourceTime is not None:
1204
return (sourceTime, self.dateFlag + self.timeFlag)
1206
return (time.localtime(), 0)
1217
print 'parse (top of loop): [%s][%s]' % (s, parseStr)
1220
# Modifier like next\prev..
1221
m = self.ptc.CRE_MODIFIER.search(s)
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()
1234
# Modifier like from\after\prior..
1235
m = self.ptc.CRE_MODIFIER2.search(s)
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()
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())
1258
# String date format
1260
self.dateStrFlag = True
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)
1273
# Standard date format
1274
m = self.ptc.CRE_DATE.search(s)
1276
self.dateStdFlag = True
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)
1289
# Natural language day strings
1290
m = self.ptc.CRE_DAY.search(s)
1292
self.dayStrFlag = True
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)
1306
m = self.ptc.CRE_UNITS.search(s)
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()
1315
if chunk1[-1:] == '-':
1316
parseStr = '-%s' % parseStr
1317
chunk1 = chunk1[:-1]
1319
s = '%s %s' % (chunk1, chunk2)
1326
m = self.ptc.CRE_QUNITS.search(s)
1328
self.qunitsFlag = True
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()
1336
if chunk1[-1:] == '-':
1337
parseStr = '-%s' % parseStr
1338
chunk1 = chunk1[:-1]
1340
s = '%s %s' % (chunk1, chunk2)
1347
m = self.ptc.CRE_WEEKDAY.search(s)
1349
gv = m.group('weekday')
1350
if s not in self.ptc.dayOffsets:
1351
self.weekdyFlag = True
1354
# capture remaining string
1356
chunk1 = s[:m.start('weekday')]
1357
chunk2 = s[m.end('weekday'):]
1358
s = '%s %s' % (chunk1, chunk2)
1364
# Natural language time strings
1365
m = self.ptc.CRE_TIME.search(s)
1367
self.timeStrFlag = True
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)
1380
# HH:MM(:SS) am/pm time strings
1381
m = self.ptc.CRE_TIMEHMS2.search(s)
1383
self.meridianFlag = True
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'),
1390
m.group('meridian'))
1392
parseStr = '%s:%s %s' % (m.group('hours'),
1394
m.group('meridian'))
1396
parseStr = '%s %s' % (m.group('hours'),
1397
m.group('meridian'))
1399
chunk1 = s[:m.start('hours')]
1400
chunk2 = s[m.end('meridian'):]
1402
s = '%s %s' % (chunk1, chunk2)
1406
# HH:MM(:SS) time strings
1407
m = self.ptc.CRE_TIMEHMS.search(s)
1409
self.timeStdFlag = True
1411
if m.group('seconds') is not None:
1412
parseStr = '%s:%s:%s' % (m.group('hours'),
1415
chunk1 = s[:m.start('hours')]
1416
chunk2 = s[m.end('seconds'):]
1418
parseStr = '%s:%s' % (m.group('hours'),
1420
chunk1 = s[:m.start('hours')]
1421
chunk2 = s[m.end('minutes'):]
1423
s = '%s %s' % (chunk1, chunk2)
1426
# if string does not match any regex, empty string to
1427
# come out of the while loop
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)
1438
# evaluate the matched string
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
1446
if (t != '') and (t != None):
1447
tempDateFlag = self.dateFlag
1448
tempTimeFlag = self.timeFlag
1449
(totalTime2, flag) = self.parse(t, totalTime)
1451
if flag == 0 and totalTime is not None:
1452
self.timeFlag = tempTimeFlag
1453
self.dateFlag = tempDateFlag
1455
return (totalTime, self.dateFlag + self.timeFlag)
1457
return (totalTime2, self.dateFlag + self.timeFlag)
1459
elif self.modifier2Flag == True:
1460
totalTime, invalidFlag = self._evalModifier2(parseStr, chunk1, chunk2, totalTime)
1462
if invalidFlag == True:
1467
totalTime = self._evalString(parseStr, totalTime)
1470
# String is not parsed at all
1471
if totalTime is None or totalTime == sourceTime:
1472
totalTime = time.localtime()
1476
return (totalTime, self.dateFlag + self.timeFlag)
1479
def inc(self, source, month=None, year=None):
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.
1485
This routine is needed because Python's C{timedelta()} function
1486
does not allow for month or year increments.
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
1493
@param year: optional number of years to increment
1496
@return: C{source} incremented by the number of months and/or years
1517
y = m / 12 # how many years are in month increment
1518
m = m % 12 # get remaining months
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
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
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)
1538
d = source.replace(year=yr, month=mth, day=dy)
1540
return source + (d - source)
4
Parse human-readable date/time text.
7
__license__ = """Copyright (c) 2004-2006 Mike Taylor, All rights reserved.
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
13
http://www.apache.org/licenses/LICENSE-2.0
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.
21
__author__ = 'Mike Taylor <http://code-bear.com>'
22
__contributors__ = ['Darshana Chhajed <mailto://darshana@osafoundation.org>',
28
import string, re, time
29
import datetime, calendar, rfc822
30
import parsedatetime_consts
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()
37
year = int(m.group('year'))
39
year = 100 * int(time.gmtime()[0] / 100) + int(year)
42
julian = m.group('julian')
45
month = julian / 30 + 1
49
t = time.mktime((year, month, day, 0, 0, 0, 0, 0, 0))
50
jday = time.gmtime(t)[-2]
51
diff = abs(jday - julian)
63
return year, month, day
64
month = m.group('month')
75
return year, month, day
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()
83
hours = m.group('hours')
87
minutes = int(m.group('minutes'))
88
seconds = m.group('seconds')
90
seconds = int(seconds)
93
return hours, minutes, seconds
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
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
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.'''
117
hours = int(m.group('tzdhours'))
118
minutes = m.group('tzdminutes')
120
minutes = int(minutes)
123
offset = (hours*60 + minutes) * 60
128
__date_re = ('(?P<year>\d\d\d\d)'
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+)?))?'
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)
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
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:
157
data[3:] = [s[:i], s[i+1:]]
160
dateString = " ".join(data)
162
dateString += ' 00:00:00 GMT'
163
return rfc822.parsedate_tz(dateString)
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)
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.
177
def __init__(self, constants=None):
179
Default constructor for the Calendar class.
181
@type constants: object
182
@param constants: Instance of the class L{CalendarConstants}
185
@return: Calendar instance
187
# if a constants reference is not included, use default
188
if constants is None:
189
self.ptc = parsedatetime_consts.Constants()
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)
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)
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)
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..
236
def _convertUnitAsWords(self, unitText):
238
Converts text units into their number value
242
Two hundred twenty five = 225
243
Two thousand and twenty five = 2025
244
Two thousand twenty five = 2025
246
@type unitText: string
247
@param unitText: number string
250
@return: numerical value of unitText
252
# TODO: implement this
256
def _buildTime(self, source, quantity, modifier, units):
258
Take quantity, modifier and unit strings and convert them into values.
259
Then calcuate the time and return the adjusted sourceTime
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
268
@param units: unit of the quantity (i.e. hours, days, months, etc)
271
@return: timetuple of the calculated time
274
print '_buildTime: [%s][%s][%s]' % (quantity, modifier, units)
277
source = time.localtime()
282
quantity = string.strip(quantity)
284
if len(quantity) == 0:
292
if modifier in self.ptc.Modifiers:
293
qty = qty * self.ptc.Modifiers[modifier]
295
if units is None or units == '':
298
# plurals are handled by regex's (could be a bug tho)
300
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = source
302
start = datetime.datetime(yr, mth, dy, hr, mn, sec)
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)
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)
322
self.invalidFlag = False
324
return target.timetuple()
327
def parseDate(self, dateString):
329
Parses strings like 05/28/200 or 04.21
331
@type dateString: string
332
@param dateString: text to convert to a datetime
335
@return: calculated datetime value of dateString
337
yr, mth, dy, hr, mn, sec, wd, yd, isdst = time.localtime()
340
m = self.CRE_DATE2.search(s)
346
m = self.CRE_DATE2.search(s)
350
yr = int(s[index + 1:])
351
# TODO should this have a birthday epoch constraint?
355
dy = int(string.strip(s))
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)
360
self.invalidFlag = True
361
sourceTime = time.localtime() #return current time if date string is invalid
366
def parseDateText(self, dateString):
368
Parses strings like "May 31st, 2006" or "Jan 1st" or "July 2006"
370
@type dateString: string
371
@param dateString: text to convert to a datetime
374
@return: calculated datetime value of dateString
376
yr, mth, dy, hr, mn, sec, wd, yd, isdst = time.localtime()
381
s = dateString.lower()
382
m = self.CRE_DATE3.search(s)
383
mth = m.group('mthname')
384
mth = self.ptc.MonthOffsets[mth]
386
if m.group('day') != None:
387
dy = int(m.group('day'))
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
398
if dy > 0 and dy <= self.ptc.DaysInMonthList[mth - 1]:
399
sourceTime = (yr, mth, dy, 9, 0, 0, wd, yd, isdst)
401
# Return current time if date string is invalid
402
self.invalidFlag = True
403
sourceTime = time.localtime()
408
def evalRanges(self, datetimeString, sourceTime=None):
410
Evaluates the strings with time or date ranges
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
418
@return: tuple of the start datetime, end datetime and the invalid flag
426
s = string.strip(datetimeString.lower())
428
m = self.CRE_TIMERNG1.search(s)
432
m = self.CRE_TIMERNG2.search(s)
436
m = self.CRE_TIMERNG3.search(s)
440
m = self.CRE_DATERNG1.search(s)
444
m = self.CRE_DATERNG2.search(s)
448
m = self.CRE_DATERNG3.search(s)
453
print 'evalRanges: rangeFlag =', rangeFlag, '[%s]' % s
457
# capture remaining string
459
chunk1 = s[:m.start()]
461
s = '%s %s' % (chunk1, chunk2)
464
sourceTime, flag = self.parse(s, sourceTime)
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)
477
if eflag is False and sflag is False:
478
return (startTime, endTime, False)
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)
486
if eflag is False and sflag is False:
487
return (startTime, endTime, False)
490
# FIXME hardcoded seperator
491
m = re.search('-', parseStr)
493
# capturing the meridian from the end time
494
# FIXME hardcoded meridian
495
if self.ptc.usesMeridian:
496
ampm = re.search('a', parseStr)
498
# appending the meridian to the start time
500
startTime, sflag = self.parse((parseStr[:m.start()] + self.ptc.meridian[0]), sourceTime)
502
startTime, sflag = self.parse((parseStr[:m.start()] + self.ptc.meridian[1]), sourceTime)
504
startTime, sflag = self.parse((parseStr[:m.start()]), sourceTime)
506
endTime, eflag = self.parse(parseStr[(m.start() + 1):], sourceTime)
508
if eflag is False and sflag is False:
509
return (startTime, endTime, False)
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)
517
if eflag is False and sflag is False:
518
return (startDate, endDate, False)
521
# FIXME hardcoded seperator
522
m = re.search('-', parseStr)
523
endDate = parseStr[(m.start() + 1):]
525
# capturing the year from the end date
526
date = self.CRE_DATE3.search(endDate)
527
endYear = date.group('year')
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')
537
if startYear is None:
540
startDate = parseStr[:m.start()]
542
startDate, sflag = self.parse(startDate, sourceTime)
543
endDate, eflag = self.parse(endDate, sourceTime)
545
if eflag is False and sflag is False:
546
return (startDate, endDate, False)
549
# FIXME hardcoded seperator
550
m = re.search('-', parseStr)
552
startDate = parseStr[:m.start()]
554
# capturing the month from the start date
555
mth = self.CRE_DATE3.search(startDate)
556
mth = mth.group('mthname')
558
# appending the month name to the end date
559
endDate = mth + parseStr[(m.start() + 1):]
561
startDate, sflag = self.parse(startDate, sourceTime)
562
endDate, eflag = self.parse(endDate, sourceTime)
564
if eflag is False and sflag is False:
565
return (startDate, endDate, False)
567
# if range is not found
568
sourceTime = time.localtime()
570
return (sourceTime, sourceTime, True)
573
def _evalModifier(self, modifier, chunk1, chunk2, sourceTime):
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
579
@type modifier: string
580
@param modifier: modifier text to apply to sourceTime
582
@param chunk1: first text chunk that followed modifier (if any)
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
589
@return: tuple of any remaining text and the modified sourceTime
591
offset = self.ptc.Modifiers[modifier]
593
if sourceTime is not None:
594
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
596
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = time.localtime()
598
# capture the units after the modifier and the remaining string after the unit
599
m = self.CRE_REMAINING.search(chunk2)
601
index = m.start() + 1
602
unit = chunk2[:m.start()]
603
chunk2 = chunk2[index:]
610
if unit == 'month' or \
613
dy = self.ptc.DaysInMonthList[mth - 1]
614
sourceTime = (yr, mth, dy, 9, 0, 0, wd, yd, isdst)
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]
620
start = datetime.datetime(yr, mth, dy, 9, 0, 0)
621
target = self.inc(start, month=1)
622
sourceTime = target.timetuple()
624
start = datetime.datetime(yr, mth, 1, 9, 0, 0)
625
target = self.inc(start, month=offset)
626
sourceTime = target.timetuple()
630
if unit == 'week' or \
634
start = datetime.datetime(yr, mth, dy, 17, 0, 0)
635
target = start + datetime.timedelta(days=(4 - wd))
636
sourceTime = target.timetuple()
638
start = datetime.datetime(yr, mth, dy, 9, 0, 0)
639
target = start + datetime.timedelta(days=7)
640
sourceTime = target.timetuple()
642
return self._evalModifier(modifier, chunk1, "monday " + chunk2, sourceTime)
646
if unit == 'day' or \
650
sourceTime = (yr, mth, dy, 17, 0, 0, wd, yd, isdst)
652
start = datetime.datetime(yr, mth, dy, hr, mn, sec)
653
target = start + datetime.timedelta(days=1)
654
sourceTime = target.timetuple()
656
start = datetime.datetime(yr, mth, dy, 9, 0, 0)
657
target = start + datetime.timedelta(days=offset)
658
sourceTime = target.timetuple()
662
if unit == 'hour' or \
665
sourceTime = (yr, mth, dy, hr, 0, 0, wd, yd, isdst)
667
start = datetime.datetime(yr, mth, dy, hr, 0, 0)
668
target = start + datetime.timedelta(hours=offset)
669
sourceTime = target.timetuple()
673
if unit == 'year' or \
677
sourceTime = (yr, 12, 31, hr, mn, sec, wd, yd, isdst)
679
sourceTime = (yr + 1, mth, dy, hr, mn, sec, wd, yd, isdst)
681
sourceTime = (yr + offset, 1, 1, 9, 0, 0, wd, yd, isdst)
686
m = self.CRE_WEEKDAY.match(unit)
689
wkdy = self.ptc.WeekdayOffsets[wkdy]
693
start = datetime.datetime(yr, mth, dy, 9, 0, 0)
694
target = start + datetime.timedelta(days=diff)
695
sourceTime = target.timetuple()
698
start = datetime.datetime(yr, mth, dy, 9, 0, 0)
699
target = start + datetime.timedelta(days=diff + 7 * offset)
700
sourceTime = target.timetuple()
705
m = self.CRE_TIME.match(unit)
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()
713
self.modifierFlag = False
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
720
# if offset is negative, the unit has to be made negative
723
chunk2 = '%s %s' % (unit, chunk2)
725
self.modifierFlag = False
727
return '%s %s' % (chunk1, chunk2), sourceTime
730
def _evalModifier2(self, modifier, chunk1 , chunk2, sourceTime):
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
736
@type modifier: string
737
@param modifier: modifier text to apply to sourceTime
739
@param chunk1: first text chunk that followed modifier (if any)
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
746
@return: tuple of any remaining text and the modified sourceTime
748
offset = self.ptc.Modifiers[modifier]
751
if sourceTime is not None:
752
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
754
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = time.localtime()
756
self.modifier2Flag = False
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
768
m = re.match(digit, string.strip(chunk2))
770
qty = int(m.group()) * -1
771
chunk2 = chunk2[m.end():]
772
chunk2 = '%d%s' % (qty, chunk2)
774
sourceTime, flag = self.parse(chunk2, sourceTime)
778
m = re.match(digit, string.strip(chunk1))
780
qty = int(m.group()) * -1
781
chunk1 = chunk1[m.end():]
782
chunk1 = '%d%s' % (qty, chunk1)
784
sourceTime, flag = self.parse(chunk1, sourceTime)
786
return '', sourceTime
789
def _evalString(self, datetimeString, sourceTime=None):
791
Calculate the datetime based on flags set by the L{parse()} routine
794
RFC822, W3CDTF formatted dates
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
805
@return: calculated datetime value or current datetime if not parsed
807
s = string.strip(datetimeString)
808
now = time.localtime()
810
# Given string date is a RFC822 date
811
if sourceTime is None:
812
sourceTime = _parse_date_rfc822(s)
814
# Given string date is a W3CDTF date
815
if sourceTime is None:
816
sourceTime = _parse_date_w3dtf(s)
818
if sourceTime is None:
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
826
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
828
m = self.CRE_TIMEHMS2.search(s)
830
dt = s[:m.start('meridian')].strip()
836
hr, mn, sec = _extract_time(m)
841
sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
842
meridian = m.group('meridian').lower()
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)
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)
853
if hr > 24 or mn > 59 or sec > 59:
855
self.invalidFlag = True
857
self.meridianFlag = False
859
# Given string is in the format HH:MM(:SS)
861
if sourceTime is None:
862
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = now
864
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
866
m = self.CRE_TIMEHMS.search(s)
868
hr, mn, sec = _extract_time(m)
872
if hr > 24 or mn > 59 or sec > 59:
875
self.invalidFlag = True
877
sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
879
self.timeFlag = False
881
# Given string is in the format 07/21/2006
883
sourceTime = self.parseDate(s)
884
self.dateStdFlag = False
886
# Given string is in the format "May 23rd, 2005"
888
sourceTime = self.parseDateText(s)
889
self.dateStrFlag = False
891
# Given string is a weekday
893
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = now
895
start = datetime.datetime(yr, mth, dy, hr, mn, sec)
896
wkDy = self.ptc.WeekdayOffsets[s]
900
target = start + datetime.timedelta(days=qty)
903
qty = 6 - wd + wkDy + 1
904
target = start + datetime.timedelta(days=qty)
907
sourceTime = target.timetuple()
908
self.weekdyFlag = False
910
# Given string is a natural language time string like lunch, midnight, etc
912
if s in self.ptc.re_values['now']:
915
sources = self.ptc.buildSources(now)
918
sourceTime = sources[s]
921
self.invalidFlag = True
923
self.timeStrFlag = False
925
# Given string is a natural language date string like today, tomorrow..
927
if sourceTime is None:
930
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
932
if s in self.ptc.dayOffsets:
933
offset = self.ptc.dayOffsets[s]
937
start = datetime.datetime(yr, mth, dy, 9, 0, 0)
938
target = start + datetime.timedelta(days=offset)
939
sourceTime = target.timetuple()
941
self.dayStrFlag = False
943
# Given string is a time string with units like "5 hrs 30 min"
947
if sourceTime is None:
950
m = self.CRE_UNITS.search(s)
952
units = m.group('units')
953
quantity = s[:m.start('units')]
955
sourceTime = self._buildTime(sourceTime, quantity, modifier, units)
956
self.unitsFlag = False
958
# Given string is a time string with single char units like "5 h 30 m"
962
if sourceTime is None:
965
m = self.CRE_QUNITS.search(s)
967
units = m.group('qunits')
968
quantity = s[:m.start('qunits')]
970
sourceTime = self._buildTime(sourceTime, quantity, modifier, units)
971
self.qunitsFlag = False
973
# Given string does not match anything
974
if sourceTime is None:
976
self.invalidFlag = True
981
def parse(self, datetimeString, sourceTime=None):
983
Splits the L{datetimeString} into tokens, finds the regex patters
984
that match and then calculates a datetime value from the chunks
986
if L{sourceTime} is given then the datetime value will be calcualted
987
from that datetime, otherwise from the current datetime.
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
995
@return: tuple of any remaining text and the modified sourceTime
997
s = string.strip(datetimeString.lower())
1000
totalTime = sourceTime
1002
self.invalidFlag = False
1005
if sourceTime is not None:
1006
return (sourceTime, False)
1008
return (time.localtime(), True)
1016
print 'parse (top of loop): [%s][%s]' % (s, parseStr)
1019
# Modifier like next\prev..
1020
m = self.CRE_MODIFIER.search(s)
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'):])
1033
# Modifier like from\after\prior..
1034
m = self.CRE_MODIFIER2.search(s)
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'):])
1047
# String date format
1048
m = self.CRE_DATE3.search(s)
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)
1062
# Standard date format
1063
m = self.CRE_DATE.search(s)
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)
1077
# Natural language day strings
1078
m = self.CRE_DAY.search(s)
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)
1093
m = self.CRE_UNITS.search(s)
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()
1102
if chunk1[-1:] == '-':
1103
parseStr = '-%s' % parseStr
1104
chunk1 = chunk1[:-1]
1106
s = '%s %s' % (chunk1, chunk2)
1113
m = self.CRE_QUNITS.search(s)
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()
1122
if chunk1[-1:] == '-':
1123
parseStr = '-%s' % parseStr
1124
chunk1 = chunk1[:-1]
1126
s = '%s %s' % (chunk1, chunk2)
1133
m = self.CRE_WEEKDAY.search(s)
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)
1147
# Natural language time strings
1148
m = self.CRE_TIME.search(s)
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)
1162
# HH:MM(:SS) am/pm time strings
1163
m = self.CRE_TIMEHMS2.search(s)
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'))
1170
parseStr = '%s:%s %s' % (m.group('hours'), m.group('minutes'), m.group('meridian'))
1172
parseStr = '%s %s' % (m.group('hours'), m.group('meridian'))
1174
chunk1 = s[:m.start('hours')]
1175
chunk2 = s[m.end('meridian'):]
1177
s = '%s %s' % (chunk1, chunk2)
1181
# HH:MM(:SS) time strings
1182
m = self.CRE_TIMEHMS.search(s)
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'):]
1190
parseStr = '%s:%s' % (m.group('hours'), m.group('minutes'))
1191
chunk1 = s[:m.start('hours')]
1192
chunk2 = s[m.end('minutes'):]
1194
s = '%s %s' % (chunk1, chunk2)
1197
# if string does not match any regex, empty string to come out of the while loop
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)
1208
# evaluate the matched string
1210
if self.modifierFlag == True:
1211
t, totalTime = self._evalModifier(parseStr, chunk1, chunk2, totalTime)
1213
return self.parse(t, totalTime)
1215
elif self.modifier2Flag == True:
1216
s, totalTime = self._evalModifier2(parseStr, chunk1, chunk2, totalTime)
1218
totalTime = self._evalString(parseStr, totalTime)
1221
# String is not parsed at all
1222
if totalTime is None or totalTime == sourceTime:
1223
totalTime = time.localtime()
1224
self.invalidFlag = True
1226
return (totalTime, self.invalidFlag)
1229
def inc(self, source, month=None, year=None):
1231
Takes the given date, or current date if none is passed, and
1232
increments it according to the values passed in by month
1235
This routine is needed because the timedelta() routine does
1236
not allow for month or year increments.
1238
@type source: datetime
1239
@param source: datetime value to increment
1240
@type month: integer
1241
@param month: optional number of months to increment
1243
@param year: optional number of years to increment
1246
@return: L{source} incremented by the number of months and/or years
1266
y = m / 12 # how many years are in month increment
1267
m = m % 12 # get remaining months
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
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
1282
d = source.replace(year=yr, month=mth)
1284
return source + (d - source)