2790 lines
103 KiB
Python
2790 lines
103 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
#
|
||
|
# vim: sw=2 ts=2 sts=2
|
||
|
#
|
||
|
# Copyright 2004-2019 Mike Taylor
|
||
|
#
|
||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
# you may not use this file except in compliance with the License.
|
||
|
# You may obtain a copy of the License at
|
||
|
#
|
||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||
|
#
|
||
|
# Unless required by applicable law or agreed to in writing, software
|
||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
# See the License for the specific language governing permissions and
|
||
|
# limitations under the License.
|
||
|
|
||
|
"""parsedatetime
|
||
|
|
||
|
Parse human-readable date/time text.
|
||
|
|
||
|
Requires Python 2.7 or later
|
||
|
"""
|
||
|
|
||
|
from __future__ import with_statement, absolute_import, unicode_literals
|
||
|
|
||
|
import re
|
||
|
import time
|
||
|
import logging
|
||
|
import warnings
|
||
|
import datetime
|
||
|
import calendar
|
||
|
import contextlib
|
||
|
import email.utils
|
||
|
|
||
|
from .pdt_locales import (locales as _locales,
|
||
|
get_icu, load_locale)
|
||
|
from .context import pdtContext, pdtContextStack
|
||
|
from .warns import pdt20DeprecationWarning
|
||
|
|
||
|
|
||
|
__author__ = 'Mike Taylor'
|
||
|
__email__ = 'bear@bear.im'
|
||
|
__copyright__ = 'Copyright (c) 2020 Mike Taylor'
|
||
|
__license__ = 'Apache License 2.0'
|
||
|
__version__ = '2.6'
|
||
|
__url__ = 'https://github.com/bear/parsedatetime'
|
||
|
__download_url__ = 'https://pypi.python.org/pypi/parsedatetime'
|
||
|
__description__ = 'Parse human-readable date/time text.'
|
||
|
|
||
|
# as a library, do *not* setup logging
|
||
|
# see docs.python.org/2/howto/logging.html#configuring-logging-for-a-library
|
||
|
# Set default logging handler to avoid "No handler found" warnings.
|
||
|
|
||
|
try: # Python 2.7+
|
||
|
from logging import NullHandler
|
||
|
except ImportError:
|
||
|
class NullHandler(logging.Handler):
|
||
|
|
||
|
def emit(self, record):
|
||
|
pass
|
||
|
|
||
|
log = logging.getLogger(__name__)
|
||
|
log.addHandler(NullHandler())
|
||
|
|
||
|
debug = False
|
||
|
|
||
|
pdtLocales = dict([(x, load_locale(x)) for x in _locales])
|
||
|
|
||
|
|
||
|
# Copied from feedparser.py
|
||
|
# Universal Feedparser
|
||
|
# Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
|
||
|
# Originally a def inside of _parse_date_w3dtf()
|
||
|
def _extract_date(m):
|
||
|
year = int(m.group('year'))
|
||
|
if year < 100:
|
||
|
year = 100 * int(time.gmtime()[0] / 100) + int(year)
|
||
|
if year < 1000:
|
||
|
return 0, 0, 0
|
||
|
julian = m.group('julian')
|
||
|
if julian:
|
||
|
julian = int(julian)
|
||
|
month = julian / 30 + 1
|
||
|
day = julian % 30 + 1
|
||
|
jday = None
|
||
|
while jday != julian:
|
||
|
t = time.mktime((year, month, day, 0, 0, 0, 0, 0, 0))
|
||
|
jday = time.gmtime(t)[-2]
|
||
|
diff = abs(jday - julian)
|
||
|
if jday > julian:
|
||
|
if diff < day:
|
||
|
day = day - diff
|
||
|
else:
|
||
|
month = month - 1
|
||
|
day = 31
|
||
|
elif jday < julian:
|
||
|
if day + diff < 28:
|
||
|
day = day + diff
|
||
|
else:
|
||
|
month = month + 1
|
||
|
return year, month, day
|
||
|
month = m.group('month')
|
||
|
day = 1
|
||
|
if month is None:
|
||
|
month = 1
|
||
|
else:
|
||
|
month = int(month)
|
||
|
day = m.group('day')
|
||
|
if day:
|
||
|
day = int(day)
|
||
|
else:
|
||
|
day = 1
|
||
|
return year, month, day
|
||
|
|
||
|
|
||
|
# Copied from feedparser.py
|
||
|
# Universal Feedparser
|
||
|
# Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
|
||
|
# Originally a def inside of _parse_date_w3dtf()
|
||
|
def _extract_time(m):
|
||
|
if not m:
|
||
|
return 0, 0, 0
|
||
|
hours = m.group('hours')
|
||
|
if not hours:
|
||
|
return 0, 0, 0
|
||
|
hours = int(hours)
|
||
|
minutes = int(m.group('minutes'))
|
||
|
seconds = m.group('seconds')
|
||
|
if seconds:
|
||
|
seconds = seconds.replace(',', '.').split('.', 1)[0]
|
||
|
seconds = int(seconds)
|
||
|
else:
|
||
|
seconds = 0
|
||
|
return hours, minutes, seconds
|
||
|
|
||
|
|
||
|
def _pop_time_accuracy(m, ctx):
|
||
|
if not m:
|
||
|
return
|
||
|
if m.group('hours'):
|
||
|
ctx.updateAccuracy(ctx.ACU_HOUR)
|
||
|
if m.group('minutes'):
|
||
|
ctx.updateAccuracy(ctx.ACU_MIN)
|
||
|
if m.group('seconds'):
|
||
|
ctx.updateAccuracy(ctx.ACU_SEC)
|
||
|
|
||
|
|
||
|
# Copied from feedparser.py
|
||
|
# Universal Feedparser
|
||
|
# Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
|
||
|
# Modified to return a tuple instead of mktime
|
||
|
#
|
||
|
# Original comment:
|
||
|
# W3DTF-style date parsing adapted from PyXML xml.utils.iso8601, written by
|
||
|
# Drake and licensed under the Python license. Removed all range checking
|
||
|
# for month, day, hour, minute, and second, since mktime will normalize
|
||
|
# these later
|
||
|
def __closure_parse_date_w3dtf():
|
||
|
# the __extract_date and __extract_time methods were
|
||
|
# copied-out so they could be used by my code --bear
|
||
|
def __extract_tzd(m):
|
||
|
'''Return the Time Zone Designator as an offset in seconds from UTC.'''
|
||
|
if not m:
|
||
|
return 0
|
||
|
tzd = m.group('tzd')
|
||
|
if not tzd:
|
||
|
return 0
|
||
|
if tzd == 'Z':
|
||
|
return 0
|
||
|
hours = int(m.group('tzdhours'))
|
||
|
minutes = m.group('tzdminutes')
|
||
|
if minutes:
|
||
|
minutes = int(minutes)
|
||
|
else:
|
||
|
minutes = 0
|
||
|
offset = (hours * 60 + minutes) * 60
|
||
|
if tzd[0] == '+':
|
||
|
return -offset
|
||
|
return offset
|
||
|
|
||
|
def _parse_date_w3dtf(dateString):
|
||
|
m = __datetime_rx.match(dateString)
|
||
|
if m is None or m.group() != dateString:
|
||
|
return
|
||
|
return _extract_date(m) + _extract_time(m) + (0, 0, 0)
|
||
|
|
||
|
__date_re = (r'(?P<year>\d\d\d\d)'
|
||
|
r'(?:(?P<dsep>-|)'
|
||
|
r'(?:(?P<julian>\d\d\d)'
|
||
|
r'|(?P<month>\d\d)(?:(?P=dsep)(?P<day>\d\d))?))?')
|
||
|
__tzd_re = r'(?P<tzd>[-+](?P<tzdhours>\d\d)(?::?(?P<tzdminutes>\d\d))|Z)'
|
||
|
# __tzd_rx = re.compile(__tzd_re)
|
||
|
__time_re = (r'(?P<hours>\d\d)(?P<tsep>:|)(?P<minutes>\d\d)'
|
||
|
r'(?:(?P=tsep)(?P<seconds>\d\d(?:[.,]\d+)?))?' + __tzd_re)
|
||
|
__datetime_re = '%s(?:T%s)?' % (__date_re, __time_re)
|
||
|
__datetime_rx = re.compile(__datetime_re)
|
||
|
|
||
|
return _parse_date_w3dtf
|
||
|
|
||
|
|
||
|
_parse_date_w3dtf = __closure_parse_date_w3dtf()
|
||
|
del __closure_parse_date_w3dtf
|
||
|
|
||
|
_monthnames = set([
|
||
|
'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul',
|
||
|
'aug', 'sep', 'oct', 'nov', 'dec',
|
||
|
'january', 'february', 'march', 'april', 'may', 'june', 'july',
|
||
|
'august', 'september', 'october', 'november', 'december'])
|
||
|
_daynames = set(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'])
|
||
|
|
||
|
|
||
|
# Copied from feedparser.py
|
||
|
# Universal Feedparser
|
||
|
# Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
|
||
|
# Modified to return a tuple instead of mktime
|
||
|
def _parse_date_rfc822(dateString):
|
||
|
'''Parse an RFC822, RFC1123, RFC2822, or asctime-style date'''
|
||
|
data = dateString.split()
|
||
|
if data[0][-1] in (',', '.') or data[0].lower() in _daynames:
|
||
|
del data[0]
|
||
|
if len(data) == 4:
|
||
|
s = data[3]
|
||
|
s = s.split('+', 1)
|
||
|
if len(s) == 2:
|
||
|
data[3:] = s
|
||
|
else:
|
||
|
data.append('')
|
||
|
dateString = " ".join(data)
|
||
|
if len(data) < 5:
|
||
|
dateString += ' 00:00:00 GMT'
|
||
|
return email.utils.parsedate_tz(dateString)
|
||
|
|
||
|
|
||
|
# rfc822.py defines several time zones, but we define some extra ones.
|
||
|
# 'ET' is equivalent to 'EST', etc.
|
||
|
# _additional_timezones = {'AT': -400, 'ET': -500,
|
||
|
# 'CT': -600, 'MT': -700,
|
||
|
# 'PT': -800}
|
||
|
# email.utils._timezones.update(_additional_timezones)
|
||
|
|
||
|
VERSION_FLAG_STYLE = 1
|
||
|
VERSION_CONTEXT_STYLE = 2
|
||
|
|
||
|
|
||
|
class Calendar(object):
|
||
|
|
||
|
"""
|
||
|
A collection of routines to input, parse and manipulate date and times.
|
||
|
The text can either be 'normal' date values or it can be human readable.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, constants=None, version=VERSION_FLAG_STYLE):
|
||
|
"""
|
||
|
Default constructor for the L{Calendar} class.
|
||
|
|
||
|
@type constants: object
|
||
|
@param constants: Instance of the class L{Constants}
|
||
|
@type version: integer
|
||
|
@param version: Default style version of current Calendar instance.
|
||
|
Valid value can be 1 (L{VERSION_FLAG_STYLE}) or
|
||
|
2 (L{VERSION_CONTEXT_STYLE}). See L{parse()}.
|
||
|
|
||
|
@rtype: object
|
||
|
@return: L{Calendar} instance
|
||
|
"""
|
||
|
# if a constants reference is not included, use default
|
||
|
if constants is None:
|
||
|
self.ptc = Constants()
|
||
|
else:
|
||
|
self.ptc = constants
|
||
|
|
||
|
self.version = version
|
||
|
if version == VERSION_FLAG_STYLE:
|
||
|
warnings.warn(
|
||
|
'Flag style will be deprecated in parsedatetime 2.0. '
|
||
|
'Instead use the context style by instantiating `Calendar()` '
|
||
|
'with argument `version=parsedatetime.VERSION_CONTEXT_STYLE`.',
|
||
|
pdt20DeprecationWarning)
|
||
|
self._ctxStack = pdtContextStack()
|
||
|
|
||
|
@contextlib.contextmanager
|
||
|
def context(self):
|
||
|
ctx = pdtContext()
|
||
|
self._ctxStack.push(ctx)
|
||
|
yield ctx
|
||
|
ctx = self._ctxStack.pop()
|
||
|
if not self._ctxStack.isEmpty():
|
||
|
self.currentContext.update(ctx)
|
||
|
|
||
|
@property
|
||
|
def currentContext(self):
|
||
|
return self._ctxStack.last()
|
||
|
|
||
|
def _convertUnitAsWords(self, unitText):
|
||
|
"""
|
||
|
Converts text units into their number value.
|
||
|
|
||
|
@type unitText: string
|
||
|
@param unitText: number text to convert
|
||
|
|
||
|
@rtype: integer
|
||
|
@return: numerical value of unitText
|
||
|
"""
|
||
|
word_list, a, b = re.split(r"[,\s-]+", unitText), 0, 0
|
||
|
for word in word_list:
|
||
|
x = self.ptc.small.get(word)
|
||
|
if x is not None:
|
||
|
a += x
|
||
|
elif word == "hundred":
|
||
|
a *= 100
|
||
|
else:
|
||
|
x = self.ptc.magnitude.get(word)
|
||
|
if x is not None:
|
||
|
b += a * x
|
||
|
a = 0
|
||
|
elif word in self.ptc.ignore:
|
||
|
pass
|
||
|
else:
|
||
|
raise Exception("Unknown number: " + word)
|
||
|
return a + b
|
||
|
|
||
|
def _buildTime(self, source, quantity, modifier, units):
|
||
|
"""
|
||
|
Take C{quantity}, C{modifier} and C{unit} strings and convert them
|
||
|
into values. After converting, calcuate the time and return the
|
||
|
adjusted sourceTime.
|
||
|
|
||
|
@type source: time
|
||
|
@param source: time to use as the base (or source)
|
||
|
@type quantity: string
|
||
|
@param quantity: quantity string
|
||
|
@type modifier: string
|
||
|
@param modifier: how quantity and units modify the source time
|
||
|
@type units: string
|
||
|
@param units: unit of the quantity (i.e. hours, days, months, etc)
|
||
|
|
||
|
@rtype: struct_time
|
||
|
@return: C{struct_time} of the calculated time
|
||
|
"""
|
||
|
ctx = self.currentContext
|
||
|
debug and log.debug('_buildTime: [%s][%s][%s]',
|
||
|
quantity, modifier, units)
|
||
|
|
||
|
if source is None:
|
||
|
source = time.localtime()
|
||
|
|
||
|
if quantity is None:
|
||
|
quantity = ''
|
||
|
else:
|
||
|
quantity = quantity.strip()
|
||
|
|
||
|
qty = self._quantityToReal(quantity)
|
||
|
|
||
|
if modifier in self.ptc.Modifiers:
|
||
|
qty = qty * self.ptc.Modifiers[modifier]
|
||
|
|
||
|
if units is None or units == '':
|
||
|
units = 'dy'
|
||
|
|
||
|
# plurals are handled by regex's (could be a bug tho)
|
||
|
|
||
|
(yr, mth, dy, hr, mn, sec, _, _, _) = source
|
||
|
|
||
|
start = datetime.datetime(yr, mth, dy, hr, mn, sec)
|
||
|
target = start
|
||
|
# realunit = next((key for key, values in self.ptc.units.items()
|
||
|
# if any(imap(units.__contains__, values))), None)
|
||
|
realunit = units
|
||
|
for key, values in self.ptc.units.items():
|
||
|
if units in values:
|
||
|
realunit = key
|
||
|
break
|
||
|
|
||
|
debug and log.debug('units %s --> realunit %s (qty=%s)',
|
||
|
units, realunit, qty)
|
||
|
|
||
|
try:
|
||
|
if realunit in ('years', 'months'):
|
||
|
target = self.inc(start, **{realunit[:-1]: qty})
|
||
|
elif realunit in ('days', 'hours', 'minutes', 'seconds', 'weeks'):
|
||
|
delta = datetime.timedelta(**{realunit: qty})
|
||
|
target = start + delta
|
||
|
except OverflowError:
|
||
|
# OverflowError is raise when target.year larger than 9999
|
||
|
pass
|
||
|
else:
|
||
|
ctx.updateAccuracy(realunit)
|
||
|
|
||
|
return target.timetuple()
|
||
|
|
||
|
def parseDate(self, dateString, sourceTime=None):
|
||
|
"""
|
||
|
Parse short-form date strings::
|
||
|
|
||
|
'05/28/2006' or '04.21'
|
||
|
|
||
|
@type dateString: string
|
||
|
@param dateString: text to convert to a C{datetime}
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: struct_time
|
||
|
@return: calculated C{struct_time} value of dateString
|
||
|
"""
|
||
|
if sourceTime is None:
|
||
|
yr, mth, dy, hr, mn, sec, wd, yd, isdst = time.localtime()
|
||
|
else:
|
||
|
yr, mth, dy, hr, mn, sec, wd, yd, isdst = sourceTime
|
||
|
|
||
|
# values pulled from regex's will be stored here and later
|
||
|
# assigned to mth, dy, yr based on information from the locale
|
||
|
# -1 is used as the marker value because we want zero values
|
||
|
# to be passed thru so they can be flagged as errors later
|
||
|
v1 = -1
|
||
|
v2 = -1
|
||
|
v3 = -1
|
||
|
accuracy = []
|
||
|
|
||
|
s = dateString
|
||
|
m = self.ptc.CRE_DATE2.search(s)
|
||
|
if m is not None:
|
||
|
index = m.start()
|
||
|
v1 = int(s[:index])
|
||
|
s = s[index + 1:]
|
||
|
|
||
|
m = self.ptc.CRE_DATE2.search(s)
|
||
|
if m is not None:
|
||
|
index = m.start()
|
||
|
v2 = int(s[:index])
|
||
|
v3 = int(s[index + 1:])
|
||
|
else:
|
||
|
v2 = int(s.strip())
|
||
|
|
||
|
v = [v1, v2, v3]
|
||
|
d = {'m': mth, 'd': dy, 'y': yr}
|
||
|
|
||
|
# yyyy/mm/dd format
|
||
|
dp_order = self.ptc.dp_order if v1 <= 31 else ['y', 'm', 'd']
|
||
|
|
||
|
for i in range(0, 3):
|
||
|
n = v[i]
|
||
|
c = dp_order[i]
|
||
|
if n >= 0:
|
||
|
d[c] = n
|
||
|
accuracy.append({'m': pdtContext.ACU_MONTH,
|
||
|
'd': pdtContext.ACU_DAY,
|
||
|
'y': pdtContext.ACU_YEAR}[c])
|
||
|
|
||
|
# if the year is not specified and the date has already
|
||
|
# passed, increment the year
|
||
|
if v3 == -1 and ((mth > d['m']) or (mth == d['m'] and dy > d['d'])):
|
||
|
yr = d['y'] + self.ptc.YearParseStyle
|
||
|
else:
|
||
|
yr = d['y']
|
||
|
|
||
|
mth = d['m']
|
||
|
dy = d['d']
|
||
|
|
||
|
# birthday epoch constraint
|
||
|
if yr < self.ptc.BirthdayEpoch:
|
||
|
yr += 2000
|
||
|
elif yr < 100:
|
||
|
yr += 1900
|
||
|
|
||
|
daysInCurrentMonth = self.ptc.daysInMonth(mth, yr)
|
||
|
debug and log.debug('parseDate: %s %s %s %s',
|
||
|
yr, mth, dy, daysInCurrentMonth)
|
||
|
|
||
|
with self.context() as ctx:
|
||
|
if mth > 0 and mth <= 12 and dy > 0 and \
|
||
|
dy <= daysInCurrentMonth:
|
||
|
sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
|
||
|
ctx.updateAccuracy(*accuracy)
|
||
|
else:
|
||
|
# return current time if date string is invalid
|
||
|
sourceTime = time.localtime()
|
||
|
|
||
|
return sourceTime
|
||
|
|
||
|
def parseDateText(self, dateString, sourceTime=None):
|
||
|
"""
|
||
|
Parse long-form date strings::
|
||
|
|
||
|
'May 31st, 2006'
|
||
|
'Jan 1st'
|
||
|
'July 2006'
|
||
|
|
||
|
@type dateString: string
|
||
|
@param dateString: text to convert to a datetime
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: struct_time
|
||
|
@return: calculated C{struct_time} value of dateString
|
||
|
"""
|
||
|
if sourceTime is None:
|
||
|
yr, mth, dy, hr, mn, sec, wd, yd, isdst = time.localtime()
|
||
|
else:
|
||
|
yr, mth, dy, hr, mn, sec, wd, yd, isdst = sourceTime
|
||
|
|
||
|
currentMth = mth
|
||
|
currentDy = dy
|
||
|
accuracy = []
|
||
|
|
||
|
debug and log.debug('parseDateText currentMth %s currentDy %s',
|
||
|
mth, dy)
|
||
|
|
||
|
s = dateString.lower()
|
||
|
m = self.ptc.CRE_DATE3.search(s)
|
||
|
mth = m.group('mthname')
|
||
|
mth = self.ptc.MonthOffsets[mth]
|
||
|
accuracy.append('month')
|
||
|
|
||
|
if m.group('day') is not None:
|
||
|
dy = int(m.group('day'))
|
||
|
accuracy.append('day')
|
||
|
else:
|
||
|
dy = 1
|
||
|
|
||
|
if m.group('year') is not None:
|
||
|
yr = int(m.group('year'))
|
||
|
accuracy.append('year')
|
||
|
|
||
|
# birthday epoch constraint
|
||
|
if yr < self.ptc.BirthdayEpoch:
|
||
|
yr += 2000
|
||
|
elif yr < 100:
|
||
|
yr += 1900
|
||
|
|
||
|
elif (mth < currentMth) or (mth == currentMth and dy < currentDy):
|
||
|
# if that day and month have already passed in this year,
|
||
|
# then increment the year by 1
|
||
|
yr += self.ptc.YearParseStyle
|
||
|
|
||
|
with self.context() as ctx:
|
||
|
if dy > 0 and dy <= self.ptc.daysInMonth(mth, yr):
|
||
|
sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
|
||
|
ctx.updateAccuracy(*accuracy)
|
||
|
else:
|
||
|
# Return current time if date string is invalid
|
||
|
sourceTime = time.localtime()
|
||
|
|
||
|
debug and log.debug('parseDateText returned '
|
||
|
'mth %d dy %d yr %d sourceTime %s',
|
||
|
mth, dy, yr, sourceTime)
|
||
|
|
||
|
return sourceTime
|
||
|
|
||
|
def evalRanges(self, datetimeString, sourceTime=None):
|
||
|
"""
|
||
|
Evaluate the C{datetimeString} text and determine if
|
||
|
it represents a date or time range.
|
||
|
|
||
|
@type datetimeString: string
|
||
|
@param datetimeString: datetime text to evaluate
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: tuple
|
||
|
@return: tuple of: start datetime, end datetime and the invalid flag
|
||
|
"""
|
||
|
rangeFlag = retFlag = 0
|
||
|
startStr = endStr = ''
|
||
|
|
||
|
s = datetimeString.strip().lower()
|
||
|
|
||
|
if self.ptc.rangeSep in s:
|
||
|
s = s.replace(self.ptc.rangeSep, ' %s ' % self.ptc.rangeSep)
|
||
|
s = s.replace(' ', ' ')
|
||
|
|
||
|
for cre, rflag in [(self.ptc.CRE_TIMERNG1, 1),
|
||
|
(self.ptc.CRE_TIMERNG2, 2),
|
||
|
(self.ptc.CRE_TIMERNG4, 7),
|
||
|
(self.ptc.CRE_TIMERNG3, 3),
|
||
|
(self.ptc.CRE_DATERNG1, 4),
|
||
|
(self.ptc.CRE_DATERNG2, 5),
|
||
|
(self.ptc.CRE_DATERNG3, 6)]:
|
||
|
m = cre.search(s)
|
||
|
if m is not None:
|
||
|
rangeFlag = rflag
|
||
|
break
|
||
|
|
||
|
debug and log.debug('evalRanges: rangeFlag = %s [%s]', rangeFlag, s)
|
||
|
|
||
|
if m is not None:
|
||
|
if (m.group() != s):
|
||
|
# capture remaining string
|
||
|
parseStr = m.group()
|
||
|
chunk1 = s[:m.start()]
|
||
|
chunk2 = s[m.end():]
|
||
|
s = '%s %s' % (chunk1, chunk2)
|
||
|
|
||
|
sourceTime, ctx = self.parse(s, sourceTime,
|
||
|
VERSION_CONTEXT_STYLE)
|
||
|
|
||
|
if not ctx.hasDateOrTime:
|
||
|
sourceTime = None
|
||
|
else:
|
||
|
parseStr = s
|
||
|
|
||
|
if rangeFlag in (1, 2):
|
||
|
m = re.search(self.ptc.rangeSep, parseStr)
|
||
|
startStr = parseStr[:m.start()]
|
||
|
endStr = parseStr[m.start() + 1:]
|
||
|
retFlag = 2
|
||
|
|
||
|
elif rangeFlag in (3, 7):
|
||
|
m = re.search(self.ptc.rangeSep, parseStr)
|
||
|
# capturing the meridian from the end time
|
||
|
if self.ptc.usesMeridian:
|
||
|
ampm = re.search(self.ptc.am[0], parseStr)
|
||
|
|
||
|
# appending the meridian to the start time
|
||
|
if ampm is not None:
|
||
|
startStr = parseStr[:m.start()] + self.ptc.meridian[0]
|
||
|
else:
|
||
|
startStr = parseStr[:m.start()] + self.ptc.meridian[1]
|
||
|
else:
|
||
|
startStr = parseStr[:m.start()]
|
||
|
|
||
|
endStr = parseStr[m.start() + 1:]
|
||
|
retFlag = 2
|
||
|
|
||
|
elif rangeFlag == 4:
|
||
|
m = re.search(self.ptc.rangeSep, parseStr)
|
||
|
startStr = parseStr[:m.start()]
|
||
|
endStr = parseStr[m.start() + 1:]
|
||
|
retFlag = 1
|
||
|
|
||
|
elif rangeFlag == 5:
|
||
|
m = re.search(self.ptc.rangeSep, parseStr)
|
||
|
endStr = parseStr[m.start() + 1:]
|
||
|
|
||
|
# capturing the year from the end date
|
||
|
date = self.ptc.CRE_DATE3.search(endStr)
|
||
|
endYear = date.group('year')
|
||
|
|
||
|
# appending the year to the start date if the start date
|
||
|
# does not have year information and the end date does.
|
||
|
# eg : "Aug 21 - Sep 4, 2007"
|
||
|
if endYear is not None:
|
||
|
startStr = (parseStr[:m.start()]).strip()
|
||
|
date = self.ptc.CRE_DATE3.search(startStr)
|
||
|
startYear = date.group('year')
|
||
|
|
||
|
if startYear is None:
|
||
|
startStr = startStr + ', ' + endYear
|
||
|
else:
|
||
|
startStr = parseStr[:m.start()]
|
||
|
|
||
|
retFlag = 1
|
||
|
|
||
|
elif rangeFlag == 6:
|
||
|
m = re.search(self.ptc.rangeSep, parseStr)
|
||
|
|
||
|
startStr = parseStr[:m.start()]
|
||
|
|
||
|
# capturing the month from the start date
|
||
|
mth = self.ptc.CRE_DATE3.search(startStr)
|
||
|
mth = mth.group('mthname')
|
||
|
|
||
|
# appending the month name to the end date
|
||
|
endStr = mth + parseStr[(m.start() + 1):]
|
||
|
|
||
|
retFlag = 1
|
||
|
|
||
|
else:
|
||
|
# if range is not found
|
||
|
startDT = endDT = time.localtime()
|
||
|
|
||
|
if retFlag:
|
||
|
startDT, sctx = self.parse(startStr, sourceTime,
|
||
|
VERSION_CONTEXT_STYLE)
|
||
|
endDT, ectx = self.parse(endStr, sourceTime,
|
||
|
VERSION_CONTEXT_STYLE)
|
||
|
|
||
|
if not sctx.hasDateOrTime or not ectx.hasDateOrTime:
|
||
|
retFlag = 0
|
||
|
|
||
|
return startDT, endDT, retFlag
|
||
|
|
||
|
def _CalculateDOWDelta(self, wd, wkdy, offset, style, currentDayStyle):
|
||
|
"""
|
||
|
Based on the C{style} and C{currentDayStyle} determine what
|
||
|
day-of-week value is to be returned.
|
||
|
|
||
|
@type wd: integer
|
||
|
@param wd: day-of-week value for the current day
|
||
|
@type wkdy: integer
|
||
|
@param wkdy: day-of-week value for the parsed day
|
||
|
@type offset: integer
|
||
|
@param offset: offset direction for any modifiers (-1, 0, 1)
|
||
|
@type style: integer
|
||
|
@param style: normally the value
|
||
|
set in C{Constants.DOWParseStyle}
|
||
|
@type currentDayStyle: integer
|
||
|
@param currentDayStyle: normally the value
|
||
|
set in C{Constants.CurrentDOWParseStyle}
|
||
|
|
||
|
@rtype: integer
|
||
|
@return: calculated day-of-week
|
||
|
"""
|
||
|
diffBase = wkdy - wd
|
||
|
origOffset = offset
|
||
|
|
||
|
if offset == 2:
|
||
|
# no modifier is present.
|
||
|
# i.e. string to be parsed is just DOW
|
||
|
if wkdy * style > wd * style or \
|
||
|
currentDayStyle and wkdy == wd:
|
||
|
# wkdy located in current week
|
||
|
offset = 0
|
||
|
elif style in (-1, 1):
|
||
|
# wkdy located in last (-1) or next (1) week
|
||
|
offset = style
|
||
|
else:
|
||
|
# invalid style, or should raise error?
|
||
|
offset = 0
|
||
|
|
||
|
# offset = -1 means last week
|
||
|
# offset = 0 means current week
|
||
|
# offset = 1 means next week
|
||
|
diff = diffBase + 7 * offset
|
||
|
if style == 1 and diff < -7:
|
||
|
diff += 7
|
||
|
elif style == -1 and diff > 7:
|
||
|
diff -= 7
|
||
|
|
||
|
debug and log.debug("wd %s, wkdy %s, offset %d, "
|
||
|
"style %d, currentDayStyle %d",
|
||
|
wd, wkdy, origOffset, style, currentDayStyle)
|
||
|
|
||
|
return diff
|
||
|
|
||
|
def _quantityToReal(self, quantity):
|
||
|
"""
|
||
|
Convert a quantity, either spelled-out or numeric, to a float
|
||
|
|
||
|
@type quantity: string
|
||
|
@param quantity: quantity to parse to float
|
||
|
@rtype: int
|
||
|
@return: the quantity as an float, defaulting to 0.0
|
||
|
"""
|
||
|
if not quantity:
|
||
|
return 1.0
|
||
|
|
||
|
try:
|
||
|
return float(quantity.replace(',', '.'))
|
||
|
except ValueError:
|
||
|
pass
|
||
|
|
||
|
try:
|
||
|
return float(self.ptc.numbers[quantity])
|
||
|
except KeyError:
|
||
|
pass
|
||
|
|
||
|
return 0.0
|
||
|
|
||
|
def _evalModifier(self, modifier, chunk1, chunk2, sourceTime):
|
||
|
"""
|
||
|
Evaluate the C{modifier} string and following text (passed in
|
||
|
as C{chunk1} and C{chunk2}) and if they match any known modifiers
|
||
|
calculate the delta and apply it to C{sourceTime}.
|
||
|
|
||
|
@type modifier: string
|
||
|
@param modifier: modifier text to apply to sourceTime
|
||
|
@type chunk1: string
|
||
|
@param chunk1: text chunk that preceded modifier (if any)
|
||
|
@type chunk2: string
|
||
|
@param chunk2: text chunk that followed modifier (if any)
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: tuple
|
||
|
@return: tuple of: remaining text and the modified sourceTime
|
||
|
"""
|
||
|
ctx = self.currentContext
|
||
|
offset = self.ptc.Modifiers[modifier]
|
||
|
|
||
|
if sourceTime is not None:
|
||
|
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
|
||
|
else:
|
||
|
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = time.localtime()
|
||
|
|
||
|
if self.ptc.StartTimeFromSourceTime:
|
||
|
startHour = hr
|
||
|
startMinute = mn
|
||
|
startSecond = sec
|
||
|
else:
|
||
|
startHour = self.ptc.StartHour
|
||
|
startMinute = 0
|
||
|
startSecond = 0
|
||
|
|
||
|
# capture the units after the modifier and the remaining
|
||
|
# string after the unit
|
||
|
m = self.ptc.CRE_REMAINING.search(chunk2)
|
||
|
if m is not None:
|
||
|
index = m.start() + 1
|
||
|
unit = chunk2[:m.start()]
|
||
|
chunk2 = chunk2[index:]
|
||
|
else:
|
||
|
unit = chunk2
|
||
|
chunk2 = ''
|
||
|
|
||
|
debug and log.debug("modifier [%s] chunk1 [%s] "
|
||
|
"chunk2 [%s] unit [%s]",
|
||
|
modifier, chunk1, chunk2, unit)
|
||
|
|
||
|
if unit in self.ptc.units['months']:
|
||
|
currentDaysInMonth = self.ptc.daysInMonth(mth, yr)
|
||
|
if offset == 0:
|
||
|
dy = currentDaysInMonth
|
||
|
sourceTime = (yr, mth, dy, startHour, startMinute,
|
||
|
startSecond, wd, yd, isdst)
|
||
|
elif offset == 2:
|
||
|
# if day is the last day of the month, calculate the last day
|
||
|
# of the next month
|
||
|
if dy == currentDaysInMonth:
|
||
|
dy = self.ptc.daysInMonth(mth + 1, yr)
|
||
|
|
||
|
start = datetime.datetime(yr, mth, dy, startHour,
|
||
|
startMinute, startSecond)
|
||
|
target = self.inc(start, month=1)
|
||
|
sourceTime = target.timetuple()
|
||
|
else:
|
||
|
start = datetime.datetime(yr, mth, 1, startHour,
|
||
|
startMinute, startSecond)
|
||
|
target = self.inc(start, month=offset)
|
||
|
sourceTime = target.timetuple()
|
||
|
ctx.updateAccuracy(ctx.ACU_MONTH)
|
||
|
|
||
|
elif unit in self.ptc.units['weeks']:
|
||
|
if offset == 0:
|
||
|
start = datetime.datetime(yr, mth, dy, 17, 0, 0)
|
||
|
target = start + datetime.timedelta(days=(4 - wd))
|
||
|
sourceTime = target.timetuple()
|
||
|
elif offset == 2:
|
||
|
start = datetime.datetime(yr, mth, dy, startHour,
|
||
|
startMinute, startSecond)
|
||
|
target = start + datetime.timedelta(days=7)
|
||
|
sourceTime = target.timetuple()
|
||
|
else:
|
||
|
start = datetime.datetime(yr, mth, dy, startHour,
|
||
|
startMinute, startSecond)
|
||
|
target = start + offset * datetime.timedelta(weeks=1)
|
||
|
sourceTime = target.timetuple()
|
||
|
ctx.updateAccuracy(ctx.ACU_WEEK)
|
||
|
|
||
|
elif unit in self.ptc.units['days']:
|
||
|
if offset == 0:
|
||
|
sourceTime = (yr, mth, dy, 17, 0, 0, wd, yd, isdst)
|
||
|
ctx.updateAccuracy(ctx.ACU_HALFDAY)
|
||
|
elif offset == 2:
|
||
|
start = datetime.datetime(yr, mth, dy, hr, mn, sec)
|
||
|
target = start + datetime.timedelta(days=1)
|
||
|
sourceTime = target.timetuple()
|
||
|
else:
|
||
|
start = datetime.datetime(yr, mth, dy, startHour,
|
||
|
startMinute, startSecond)
|
||
|
target = start + datetime.timedelta(days=offset)
|
||
|
sourceTime = target.timetuple()
|
||
|
ctx.updateAccuracy(ctx.ACU_DAY)
|
||
|
|
||
|
elif unit in self.ptc.units['hours']:
|
||
|
if offset == 0:
|
||
|
sourceTime = (yr, mth, dy, hr, 0, 0, wd, yd, isdst)
|
||
|
else:
|
||
|
start = datetime.datetime(yr, mth, dy, hr, 0, 0)
|
||
|
target = start + datetime.timedelta(hours=offset)
|
||
|
sourceTime = target.timetuple()
|
||
|
ctx.updateAccuracy(ctx.ACU_HOUR)
|
||
|
|
||
|
elif unit in self.ptc.units['years']:
|
||
|
if offset == 0:
|
||
|
sourceTime = (yr, 12, 31, hr, mn, sec, wd, yd, isdst)
|
||
|
elif offset == 2:
|
||
|
sourceTime = (yr + 1, mth, dy, hr, mn, sec, wd, yd, isdst)
|
||
|
else:
|
||
|
sourceTime = (yr + offset, 1, 1, startHour, startMinute,
|
||
|
startSecond, wd, yd, isdst)
|
||
|
ctx.updateAccuracy(ctx.ACU_YEAR)
|
||
|
|
||
|
elif modifier == 'eom':
|
||
|
dy = self.ptc.daysInMonth(mth, yr)
|
||
|
sourceTime = (yr, mth, dy, startHour, startMinute,
|
||
|
startSecond, wd, yd, isdst)
|
||
|
ctx.updateAccuracy(ctx.ACU_DAY)
|
||
|
|
||
|
elif modifier == 'eoy':
|
||
|
mth = 12
|
||
|
dy = self.ptc.daysInMonth(mth, yr)
|
||
|
sourceTime = (yr, mth, dy, startHour, startMinute,
|
||
|
startSecond, wd, yd, isdst)
|
||
|
ctx.updateAccuracy(ctx.ACU_MONTH)
|
||
|
|
||
|
elif self.ptc.CRE_WEEKDAY.match(unit):
|
||
|
m = self.ptc.CRE_WEEKDAY.match(unit)
|
||
|
debug and log.debug('CRE_WEEKDAY matched')
|
||
|
wkdy = m.group()
|
||
|
|
||
|
if modifier == 'eod':
|
||
|
ctx.updateAccuracy(ctx.ACU_HOUR)
|
||
|
# Calculate the upcoming weekday
|
||
|
sourceTime, subctx = self.parse(wkdy, sourceTime,
|
||
|
VERSION_CONTEXT_STYLE)
|
||
|
sTime = self.ptc.getSource(modifier, sourceTime)
|
||
|
if sTime is not None:
|
||
|
sourceTime = sTime
|
||
|
ctx.updateAccuracy(ctx.ACU_HALFDAY)
|
||
|
else:
|
||
|
# unless one of these modifiers is being applied to the
|
||
|
# day-of-week, we want to start with target as the day
|
||
|
# in the current week.
|
||
|
dowOffset = offset
|
||
|
relativeModifier = modifier not in ['this', 'next', 'last', 'prior', 'previous']
|
||
|
if relativeModifier:
|
||
|
dowOffset = 0
|
||
|
|
||
|
wkdy = self.ptc.WeekdayOffsets[wkdy]
|
||
|
diff = self._CalculateDOWDelta(
|
||
|
wd, wkdy, dowOffset, self.ptc.DOWParseStyle,
|
||
|
self.ptc.CurrentDOWParseStyle)
|
||
|
start = datetime.datetime(yr, mth, dy, startHour,
|
||
|
startMinute, startSecond)
|
||
|
target = start + datetime.timedelta(days=diff)
|
||
|
|
||
|
if chunk1 != '' and relativeModifier:
|
||
|
# consider "one day before thursday": we need to parse chunk1 ("one day")
|
||
|
# and apply according to the offset ("before"), rather than allowing the
|
||
|
# remaining parse step to apply "one day" without the offset direction.
|
||
|
t, subctx = self.parse(chunk1, sourceTime, VERSION_CONTEXT_STYLE)
|
||
|
if subctx.hasDateOrTime:
|
||
|
delta = time.mktime(t) - time.mktime(sourceTime)
|
||
|
target = start + datetime.timedelta(days=diff) + datetime.timedelta(seconds=delta * offset)
|
||
|
chunk1 = ''
|
||
|
|
||
|
sourceTime = target.timetuple()
|
||
|
ctx.updateAccuracy(ctx.ACU_DAY)
|
||
|
|
||
|
elif chunk1 == '' and chunk2 == '' and self.ptc.CRE_TIME.match(unit):
|
||
|
m = self.ptc.CRE_TIME.match(unit)
|
||
|
debug and log.debug('CRE_TIME matched')
|
||
|
(yr, mth, dy, hr, mn, sec, wd, yd, isdst), subctx = \
|
||
|
self.parse(unit, None, VERSION_CONTEXT_STYLE)
|
||
|
|
||
|
start = datetime.datetime(yr, mth, dy, hr, mn, sec)
|
||
|
target = start + datetime.timedelta(days=offset)
|
||
|
sourceTime = target.timetuple()
|
||
|
|
||
|
else:
|
||
|
# check if the remaining text is parsable and if so,
|
||
|
# use it as the base time for the modifier source time
|
||
|
|
||
|
debug and log.debug('check for modifications '
|
||
|
'to source time [%s] [%s]',
|
||
|
chunk1, unit)
|
||
|
|
||
|
unit = unit.strip()
|
||
|
if unit:
|
||
|
s = '%s %s' % (unit, chunk2)
|
||
|
t, subctx = self.parse(s, sourceTime, VERSION_CONTEXT_STYLE)
|
||
|
|
||
|
if subctx.hasDate: # working with dates
|
||
|
u = unit.lower()
|
||
|
if u in self.ptc.Months or \
|
||
|
u in self.ptc.shortMonths:
|
||
|
yr, mth, dy, hr, mn, sec, wd, yd, isdst = t
|
||
|
start = datetime.datetime(
|
||
|
yr, mth, dy, hr, mn, sec)
|
||
|
t = self.inc(start, year=offset).timetuple()
|
||
|
elif u in self.ptc.Weekdays:
|
||
|
t = t + datetime.timedelta(weeks=offset)
|
||
|
|
||
|
if subctx.hasDateOrTime:
|
||
|
sourceTime = t
|
||
|
chunk2 = ''
|
||
|
|
||
|
chunk1 = chunk1.strip()
|
||
|
|
||
|
# if the word after next is a number, the string is more than
|
||
|
# likely to be "next 4 hrs" which we will have to combine the
|
||
|
# units with the rest of the string
|
||
|
if chunk1:
|
||
|
try:
|
||
|
m = list(self.ptc.CRE_NUMBER.finditer(chunk1))[-1]
|
||
|
except IndexError:
|
||
|
pass
|
||
|
else:
|
||
|
qty = None
|
||
|
debug and log.debug('CRE_NUMBER matched')
|
||
|
qty = self._quantityToReal(m.group()) * offset
|
||
|
chunk1 = '%s%s%s' % (chunk1[:m.start()],
|
||
|
qty, chunk1[m.end():])
|
||
|
t, subctx = self.parse(chunk1, sourceTime,
|
||
|
VERSION_CONTEXT_STYLE)
|
||
|
|
||
|
chunk1 = ''
|
||
|
|
||
|
if subctx.hasDateOrTime:
|
||
|
sourceTime = t
|
||
|
|
||
|
debug and log.debug('looking for modifier %s', modifier)
|
||
|
sTime = self.ptc.getSource(modifier, sourceTime)
|
||
|
if sTime is not None:
|
||
|
debug and log.debug('modifier found in sources')
|
||
|
sourceTime = sTime
|
||
|
ctx.updateAccuracy(ctx.ACU_HALFDAY)
|
||
|
|
||
|
debug and log.debug('returning chunk = "%s %s" and sourceTime = %s',
|
||
|
chunk1, chunk2, sourceTime)
|
||
|
|
||
|
return '%s %s' % (chunk1, chunk2), sourceTime
|
||
|
|
||
|
def _evalDT(self, datetimeString, sourceTime):
|
||
|
"""
|
||
|
Calculate the datetime from known format like RFC822 or W3CDTF
|
||
|
|
||
|
Examples handled::
|
||
|
RFC822, W3CDTF formatted dates
|
||
|
HH:MM[:SS][ am/pm]
|
||
|
MM/DD/YYYY
|
||
|
DD MMMM YYYY
|
||
|
|
||
|
@type datetimeString: string
|
||
|
@param datetimeString: text to try and parse as more "traditional"
|
||
|
date/time text
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: datetime
|
||
|
@return: calculated C{struct_time} value or current C{struct_time}
|
||
|
if not parsed
|
||
|
"""
|
||
|
ctx = self.currentContext
|
||
|
s = datetimeString.strip()
|
||
|
|
||
|
# Given string date is a RFC822 date
|
||
|
if sourceTime is None:
|
||
|
sourceTime = _parse_date_rfc822(s)
|
||
|
debug and log.debug(
|
||
|
'attempt to parse as rfc822 - %s', str(sourceTime))
|
||
|
|
||
|
if sourceTime is not None:
|
||
|
(yr, mth, dy, hr, mn, sec, wd, yd, isdst, _) = sourceTime
|
||
|
ctx.updateAccuracy(ctx.ACU_YEAR, ctx.ACU_MONTH, ctx.ACU_DAY)
|
||
|
|
||
|
if hr != 0 and mn != 0 and sec != 0:
|
||
|
ctx.updateAccuracy(ctx.ACU_HOUR, ctx.ACU_MIN, ctx.ACU_SEC)
|
||
|
|
||
|
sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
|
||
|
|
||
|
# Given string date is a W3CDTF date
|
||
|
if sourceTime is None:
|
||
|
sourceTime = _parse_date_w3dtf(s)
|
||
|
|
||
|
if sourceTime is not None:
|
||
|
ctx.updateAccuracy(ctx.ACU_YEAR, ctx.ACU_MONTH, ctx.ACU_DAY,
|
||
|
ctx.ACU_HOUR, ctx.ACU_MIN, ctx.ACU_SEC)
|
||
|
|
||
|
if sourceTime is None:
|
||
|
sourceTime = time.localtime()
|
||
|
|
||
|
return sourceTime
|
||
|
|
||
|
def _evalUnits(self, datetimeString, sourceTime):
|
||
|
"""
|
||
|
Evaluate text passed by L{_partialParseUnits()}
|
||
|
"""
|
||
|
s = datetimeString.strip()
|
||
|
sourceTime = self._evalDT(datetimeString, sourceTime)
|
||
|
|
||
|
# Given string is a time string with units like "5 hrs 30 min"
|
||
|
modifier = '' # TODO
|
||
|
|
||
|
m = self.ptc.CRE_UNITS.search(s)
|
||
|
if m is not None:
|
||
|
units = m.group('units')
|
||
|
quantity = s[:m.start('units')]
|
||
|
|
||
|
sourceTime = self._buildTime(sourceTime, quantity, modifier, units)
|
||
|
return sourceTime
|
||
|
|
||
|
def _evalQUnits(self, datetimeString, sourceTime):
|
||
|
"""
|
||
|
Evaluate text passed by L{_partialParseQUnits()}
|
||
|
"""
|
||
|
s = datetimeString.strip()
|
||
|
sourceTime = self._evalDT(datetimeString, sourceTime)
|
||
|
|
||
|
# Given string is a time string with single char units like "5 h 30 m"
|
||
|
modifier = '' # TODO
|
||
|
|
||
|
m = self.ptc.CRE_QUNITS.search(s)
|
||
|
if m is not None:
|
||
|
units = m.group('qunits')
|
||
|
quantity = s[:m.start('qunits')]
|
||
|
|
||
|
sourceTime = self._buildTime(sourceTime, quantity, modifier, units)
|
||
|
return sourceTime
|
||
|
|
||
|
def _evalDateStr(self, datetimeString, sourceTime):
|
||
|
"""
|
||
|
Evaluate text passed by L{_partialParseDateStr()}
|
||
|
"""
|
||
|
s = datetimeString.strip()
|
||
|
sourceTime = self._evalDT(datetimeString, sourceTime)
|
||
|
|
||
|
# Given string is in the format "May 23rd, 2005"
|
||
|
debug and log.debug('checking for MMM DD YYYY')
|
||
|
return self.parseDateText(s, sourceTime)
|
||
|
|
||
|
def _evalDateStd(self, datetimeString, sourceTime):
|
||
|
"""
|
||
|
Evaluate text passed by L{_partialParseDateStd()}
|
||
|
"""
|
||
|
s = datetimeString.strip()
|
||
|
sourceTime = self._evalDT(datetimeString, sourceTime)
|
||
|
|
||
|
# Given string is in the format 07/21/2006
|
||
|
return self.parseDate(s, sourceTime)
|
||
|
|
||
|
def _evalDayStr(self, datetimeString, sourceTime):
|
||
|
"""
|
||
|
Evaluate text passed by L{_partialParseDaystr()}
|
||
|
"""
|
||
|
s = datetimeString.strip()
|
||
|
sourceTime = self._evalDT(datetimeString, sourceTime)
|
||
|
|
||
|
# Given string is a natural language date string like today, tomorrow..
|
||
|
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
|
||
|
|
||
|
try:
|
||
|
offset = self.ptc.dayOffsets[s]
|
||
|
except KeyError:
|
||
|
offset = 0
|
||
|
|
||
|
if self.ptc.StartTimeFromSourceTime:
|
||
|
startHour = hr
|
||
|
startMinute = mn
|
||
|
startSecond = sec
|
||
|
else:
|
||
|
startHour = self.ptc.StartHour
|
||
|
startMinute = 0
|
||
|
startSecond = 0
|
||
|
|
||
|
self.currentContext.updateAccuracy(pdtContext.ACU_DAY)
|
||
|
start = datetime.datetime(yr, mth, dy, startHour,
|
||
|
startMinute, startSecond)
|
||
|
target = start + datetime.timedelta(days=offset)
|
||
|
return target.timetuple()
|
||
|
|
||
|
def _evalWeekday(self, datetimeString, sourceTime):
|
||
|
"""
|
||
|
Evaluate text passed by L{_partialParseWeekday()}
|
||
|
"""
|
||
|
s = datetimeString.strip()
|
||
|
sourceTime = self._evalDT(datetimeString, sourceTime)
|
||
|
|
||
|
# Given string is a weekday
|
||
|
yr, mth, dy, hr, mn, sec, wd, yd, isdst = sourceTime
|
||
|
|
||
|
start = datetime.datetime(yr, mth, dy, hr, mn, sec)
|
||
|
wkdy = self.ptc.WeekdayOffsets[s]
|
||
|
|
||
|
if wkdy > wd:
|
||
|
qty = self._CalculateDOWDelta(wd, wkdy, 2,
|
||
|
self.ptc.DOWParseStyle,
|
||
|
self.ptc.CurrentDOWParseStyle)
|
||
|
else:
|
||
|
qty = self._CalculateDOWDelta(wd, wkdy, 2,
|
||
|
self.ptc.DOWParseStyle,
|
||
|
self.ptc.CurrentDOWParseStyle)
|
||
|
|
||
|
self.currentContext.updateAccuracy(pdtContext.ACU_DAY)
|
||
|
target = start + datetime.timedelta(days=qty)
|
||
|
return target.timetuple()
|
||
|
|
||
|
def _evalTimeStr(self, datetimeString, sourceTime):
|
||
|
"""
|
||
|
Evaluate text passed by L{_partialParseTimeStr()}
|
||
|
"""
|
||
|
s = datetimeString.strip()
|
||
|
sourceTime = self._evalDT(datetimeString, sourceTime)
|
||
|
|
||
|
if s in self.ptc.re_values['now']:
|
||
|
self.currentContext.updateAccuracy(pdtContext.ACU_NOW)
|
||
|
else:
|
||
|
# Given string is a natural language time string like
|
||
|
# lunch, midnight, etc
|
||
|
sTime = self.ptc.getSource(s, sourceTime)
|
||
|
if sTime:
|
||
|
sourceTime = sTime
|
||
|
self.currentContext.updateAccuracy(pdtContext.ACU_HALFDAY)
|
||
|
|
||
|
return sourceTime
|
||
|
|
||
|
def _evalMeridian(self, datetimeString, sourceTime):
|
||
|
"""
|
||
|
Evaluate text passed by L{_partialParseMeridian()}
|
||
|
"""
|
||
|
s = datetimeString.strip()
|
||
|
sourceTime = self._evalDT(datetimeString, sourceTime)
|
||
|
|
||
|
# Given string is in the format HH:MM(:SS)(am/pm)
|
||
|
yr, mth, dy, hr, mn, sec, wd, yd, isdst = sourceTime
|
||
|
|
||
|
m = self.ptc.CRE_TIMEHMS2.search(s)
|
||
|
if m is not None:
|
||
|
dt = s[:m.start('meridian')].strip()
|
||
|
if len(dt) <= 2:
|
||
|
hr = int(dt)
|
||
|
mn = 0
|
||
|
sec = 0
|
||
|
else:
|
||
|
hr, mn, sec = _extract_time(m)
|
||
|
|
||
|
if hr == 24:
|
||
|
hr = 0
|
||
|
|
||
|
meridian = m.group('meridian').lower()
|
||
|
|
||
|
# if 'am' found and hour is 12 - force hour to 0 (midnight)
|
||
|
if (meridian in self.ptc.am) and hr == 12:
|
||
|
hr = 0
|
||
|
|
||
|
# if 'pm' found and hour < 12, add 12 to shift to evening
|
||
|
if (meridian in self.ptc.pm) and hr < 12:
|
||
|
hr += 12
|
||
|
|
||
|
# time validation
|
||
|
if hr < 24 and mn < 60 and sec < 60:
|
||
|
sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
|
||
|
_pop_time_accuracy(m, self.currentContext)
|
||
|
|
||
|
return sourceTime
|
||
|
|
||
|
def _evalTimeStd(self, datetimeString, sourceTime):
|
||
|
"""
|
||
|
Evaluate text passed by L{_partialParseTimeStd()}
|
||
|
"""
|
||
|
s = datetimeString.strip()
|
||
|
sourceTime = self._evalDT(datetimeString, sourceTime)
|
||
|
|
||
|
# Given string is in the format HH:MM(:SS)
|
||
|
yr, mth, dy, hr, mn, sec, wd, yd, isdst = sourceTime
|
||
|
|
||
|
m = self.ptc.CRE_TIMEHMS.search(s)
|
||
|
if m is not None:
|
||
|
hr, mn, sec = _extract_time(m)
|
||
|
if hr == 24:
|
||
|
hr = 0
|
||
|
|
||
|
# time validation
|
||
|
if hr < 24 and mn < 60 and sec < 60:
|
||
|
sourceTime = (yr, mth, dy, hr, mn, sec, wd, yd, isdst)
|
||
|
_pop_time_accuracy(m, self.currentContext)
|
||
|
|
||
|
return sourceTime
|
||
|
|
||
|
def _UnitsTrapped(self, s, m, key):
|
||
|
# check if a day suffix got trapped by a unit match
|
||
|
# for example Dec 31st would match for 31s (aka 31 seconds)
|
||
|
# Dec 31st
|
||
|
# ^ ^
|
||
|
# | +-- m.start('units')
|
||
|
# | and also m2.start('suffix')
|
||
|
# +---- m.start('qty')
|
||
|
# and also m2.start('day')
|
||
|
m2 = self.ptc.CRE_DAY2.search(s)
|
||
|
if m2 is not None:
|
||
|
t = '%s%s' % (m2.group('day'), m.group(key))
|
||
|
if m.start(key) == m2.start('suffix') and \
|
||
|
m.start('qty') == m2.start('day') and \
|
||
|
m.group('qty') == t:
|
||
|
return True
|
||
|
else:
|
||
|
return False
|
||
|
else:
|
||
|
return False
|
||
|
|
||
|
def _partialParseModifier(self, s, sourceTime):
|
||
|
"""
|
||
|
test if giving C{s} matched CRE_MODIFIER, used by L{parse()}
|
||
|
|
||
|
@type s: string
|
||
|
@param s: date/time text to evaluate
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: tuple
|
||
|
@return: tuple of remained date/time text, datetime object and
|
||
|
an boolean value to describ if matched or not
|
||
|
|
||
|
"""
|
||
|
parseStr = None
|
||
|
chunk1 = chunk2 = ''
|
||
|
|
||
|
# Modifier like next/prev/from/after/prior..
|
||
|
m = self.ptc.CRE_MODIFIER.search(s)
|
||
|
if m is not None:
|
||
|
if m.group() != s:
|
||
|
# capture remaining string
|
||
|
parseStr = m.group()
|
||
|
chunk1 = s[:m.start()].strip()
|
||
|
chunk2 = s[m.end():].strip()
|
||
|
else:
|
||
|
parseStr = s
|
||
|
|
||
|
if parseStr:
|
||
|
debug and log.debug('found (modifier) [%s][%s][%s]',
|
||
|
parseStr, chunk1, chunk2)
|
||
|
s, sourceTime = self._evalModifier(parseStr, chunk1,
|
||
|
chunk2, sourceTime)
|
||
|
|
||
|
return s, sourceTime, bool(parseStr)
|
||
|
|
||
|
def _partialParseUnits(self, s, sourceTime):
|
||
|
"""
|
||
|
test if giving C{s} matched CRE_UNITS, used by L{parse()}
|
||
|
|
||
|
@type s: string
|
||
|
@param s: date/time text to evaluate
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: tuple
|
||
|
@return: tuple of remained date/time text, datetime object and
|
||
|
an boolean value to describ if matched or not
|
||
|
|
||
|
"""
|
||
|
parseStr = None
|
||
|
chunk1 = chunk2 = ''
|
||
|
|
||
|
# Quantity + Units
|
||
|
m = self.ptc.CRE_UNITS.search(s)
|
||
|
if m is not None:
|
||
|
debug and log.debug('CRE_UNITS matched')
|
||
|
if self._UnitsTrapped(s, m, 'units'):
|
||
|
debug and log.debug('day suffix trapped by unit match')
|
||
|
else:
|
||
|
if (m.group('qty') != s):
|
||
|
# capture remaining string
|
||
|
parseStr = m.group('qty')
|
||
|
chunk1 = s[:m.start('qty')].strip()
|
||
|
chunk2 = s[m.end('qty'):].strip()
|
||
|
|
||
|
if chunk1[-1:] == '-':
|
||
|
parseStr = '-%s' % parseStr
|
||
|
chunk1 = chunk1[:-1]
|
||
|
|
||
|
s = '%s %s' % (chunk1, chunk2)
|
||
|
else:
|
||
|
parseStr = s
|
||
|
s = ''
|
||
|
|
||
|
if parseStr:
|
||
|
debug and log.debug('found (units) [%s][%s][%s]',
|
||
|
parseStr, chunk1, chunk2)
|
||
|
sourceTime = self._evalUnits(parseStr, sourceTime)
|
||
|
|
||
|
return s, sourceTime, bool(parseStr)
|
||
|
|
||
|
def _partialParseQUnits(self, s, sourceTime):
|
||
|
"""
|
||
|
test if giving C{s} matched CRE_QUNITS, used by L{parse()}
|
||
|
|
||
|
@type s: string
|
||
|
@param s: date/time text to evaluate
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: tuple
|
||
|
@return: tuple of remained date/time text, datetime object and
|
||
|
an boolean value to describ if matched or not
|
||
|
|
||
|
"""
|
||
|
parseStr = None
|
||
|
chunk1 = chunk2 = ''
|
||
|
|
||
|
# Quantity + Units
|
||
|
m = self.ptc.CRE_QUNITS.search(s)
|
||
|
if m is not None:
|
||
|
debug and log.debug('CRE_QUNITS matched')
|
||
|
if self._UnitsTrapped(s, m, 'qunits'):
|
||
|
debug and log.debug(
|
||
|
'day suffix trapped by qunit match')
|
||
|
else:
|
||
|
if (m.group('qty') != s):
|
||
|
# capture remaining string
|
||
|
parseStr = m.group('qty')
|
||
|
chunk1 = s[:m.start('qty')].strip()
|
||
|
chunk2 = s[m.end('qty'):].strip()
|
||
|
|
||
|
if chunk1[-1:] == '-':
|
||
|
parseStr = '-%s' % parseStr
|
||
|
chunk1 = chunk1[:-1]
|
||
|
|
||
|
s = '%s %s' % (chunk1, chunk2)
|
||
|
else:
|
||
|
parseStr = s
|
||
|
s = ''
|
||
|
|
||
|
if parseStr:
|
||
|
debug and log.debug('found (qunits) [%s][%s][%s]',
|
||
|
parseStr, chunk1, chunk2)
|
||
|
sourceTime = self._evalQUnits(parseStr, sourceTime)
|
||
|
|
||
|
return s, sourceTime, bool(parseStr)
|
||
|
|
||
|
def _partialParseDateStr(self, s, sourceTime):
|
||
|
"""
|
||
|
test if giving C{s} matched CRE_DATE3, used by L{parse()}
|
||
|
|
||
|
@type s: string
|
||
|
@param s: date/time text to evaluate
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: tuple
|
||
|
@return: tuple of remained date/time text, datetime object and
|
||
|
an boolean value to describ if matched or not
|
||
|
|
||
|
"""
|
||
|
parseStr = None
|
||
|
chunk1 = chunk2 = ''
|
||
|
|
||
|
m = self.ptc.CRE_DATE3.search(s)
|
||
|
# NO LONGER NEEDED, THE REGEXP HANDLED MTHNAME NOW
|
||
|
# for match in self.ptc.CRE_DATE3.finditer(s):
|
||
|
# to prevent "HH:MM(:SS) time strings" expressions from
|
||
|
# triggering this regex, we checks if the month field
|
||
|
# exists in the searched expression, if it doesn't exist,
|
||
|
# the date field is not valid
|
||
|
# if match.group('mthname'):
|
||
|
# m = self.ptc.CRE_DATE3.search(s, match.start())
|
||
|
# valid_date = True
|
||
|
# break
|
||
|
|
||
|
# String date format
|
||
|
if m is not None:
|
||
|
|
||
|
if (m.group('date') != s):
|
||
|
# capture remaining string
|
||
|
mStart = m.start('date')
|
||
|
mEnd = m.end('date')
|
||
|
|
||
|
# we need to check that anything following the parsed
|
||
|
# date is a time expression because it is often picked
|
||
|
# up as a valid year if the hour is 2 digits
|
||
|
fTime = False
|
||
|
mm = self.ptc.CRE_TIMEHMS2.search(s)
|
||
|
# "February 24th 1PM" doesn't get caught
|
||
|
# "February 24th 12PM" does
|
||
|
mYear = m.group('year')
|
||
|
if mm is not None and mYear is not None:
|
||
|
fTime = True
|
||
|
else:
|
||
|
# "February 24th 12:00"
|
||
|
mm = self.ptc.CRE_TIMEHMS.search(s)
|
||
|
if mm is not None and mYear is None:
|
||
|
fTime = True
|
||
|
if fTime:
|
||
|
hoursStart = mm.start('hours')
|
||
|
|
||
|
if hoursStart < m.end('year'):
|
||
|
mEnd = hoursStart
|
||
|
|
||
|
parseStr = s[mStart:mEnd]
|
||
|
chunk1 = s[:mStart]
|
||
|
chunk2 = s[mEnd:]
|
||
|
|
||
|
s = '%s %s' % (chunk1, chunk2)
|
||
|
else:
|
||
|
parseStr = s
|
||
|
s = ''
|
||
|
|
||
|
if parseStr:
|
||
|
debug and log.debug(
|
||
|
'found (date3) [%s][%s][%s]', parseStr, chunk1, chunk2)
|
||
|
sourceTime = self._evalDateStr(parseStr, sourceTime)
|
||
|
|
||
|
return s, sourceTime, bool(parseStr)
|
||
|
|
||
|
def _partialParseDateStd(self, s, sourceTime):
|
||
|
"""
|
||
|
test if giving C{s} matched CRE_DATE, used by L{parse()}
|
||
|
|
||
|
@type s: string
|
||
|
@param s: date/time text to evaluate
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: tuple
|
||
|
@return: tuple of remained date/time text, datetime object and
|
||
|
an boolean value to describ if matched or not
|
||
|
|
||
|
"""
|
||
|
parseStr = None
|
||
|
chunk1 = chunk2 = ''
|
||
|
|
||
|
# Standard date format
|
||
|
m = self.ptc.CRE_DATE.search(s)
|
||
|
if m is not None:
|
||
|
|
||
|
if (m.group('date') != s):
|
||
|
# capture remaining string
|
||
|
parseStr = m.group('date')
|
||
|
chunk1 = s[:m.start('date')]
|
||
|
chunk2 = s[m.end('date'):]
|
||
|
s = '%s %s' % (chunk1, chunk2)
|
||
|
else:
|
||
|
parseStr = s
|
||
|
s = ''
|
||
|
|
||
|
if parseStr:
|
||
|
debug and log.debug(
|
||
|
'found (date) [%s][%s][%s]', parseStr, chunk1, chunk2)
|
||
|
sourceTime = self._evalDateStd(parseStr, sourceTime)
|
||
|
|
||
|
return s, sourceTime, bool(parseStr)
|
||
|
|
||
|
def _partialParseDayStr(self, s, sourceTime):
|
||
|
"""
|
||
|
test if giving C{s} matched CRE_DAY, used by L{parse()}
|
||
|
|
||
|
@type s: string
|
||
|
@param s: date/time text to evaluate
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: tuple
|
||
|
@return: tuple of remained date/time text, datetime object and
|
||
|
an boolean value to describ if matched or not
|
||
|
|
||
|
"""
|
||
|
parseStr = None
|
||
|
chunk1 = chunk2 = ''
|
||
|
|
||
|
# Natural language day strings
|
||
|
m = self.ptc.CRE_DAY.search(s)
|
||
|
if m is not None:
|
||
|
|
||
|
if (m.group() != s):
|
||
|
# capture remaining string
|
||
|
parseStr = m.group()
|
||
|
chunk1 = s[:m.start()]
|
||
|
chunk2 = s[m.end():]
|
||
|
s = '%s %s' % (chunk1, chunk2)
|
||
|
else:
|
||
|
parseStr = s
|
||
|
s = ''
|
||
|
|
||
|
if parseStr:
|
||
|
debug and log.debug(
|
||
|
'found (day) [%s][%s][%s]', parseStr, chunk1, chunk2)
|
||
|
sourceTime = self._evalDayStr(parseStr, sourceTime)
|
||
|
|
||
|
return s, sourceTime, bool(parseStr)
|
||
|
|
||
|
def _partialParseWeekday(self, s, sourceTime):
|
||
|
"""
|
||
|
test if giving C{s} matched CRE_WEEKDAY, used by L{parse()}
|
||
|
|
||
|
@type s: string
|
||
|
@param s: date/time text to evaluate
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: tuple
|
||
|
@return: tuple of remained date/time text, datetime object and
|
||
|
an boolean value to describ if matched or not
|
||
|
|
||
|
"""
|
||
|
parseStr = None
|
||
|
chunk1 = chunk2 = ''
|
||
|
|
||
|
ctx = self.currentContext
|
||
|
log.debug('eval %s with context - %s, %s', s, ctx.hasDate, ctx.hasTime)
|
||
|
|
||
|
# Weekday
|
||
|
m = self.ptc.CRE_WEEKDAY.search(s)
|
||
|
if m is not None:
|
||
|
gv = m.group()
|
||
|
if s not in self.ptc.dayOffsets:
|
||
|
|
||
|
if (gv != s):
|
||
|
# capture remaining string
|
||
|
parseStr = gv
|
||
|
chunk1 = s[:m.start()]
|
||
|
chunk2 = s[m.end():]
|
||
|
s = '%s %s' % (chunk1, chunk2)
|
||
|
else:
|
||
|
parseStr = s
|
||
|
s = ''
|
||
|
|
||
|
if parseStr and not ctx.hasDate:
|
||
|
debug and log.debug(
|
||
|
'found (weekday) [%s][%s][%s]', parseStr, chunk1, chunk2)
|
||
|
sourceTime = self._evalWeekday(parseStr, sourceTime)
|
||
|
|
||
|
return s, sourceTime, bool(parseStr)
|
||
|
|
||
|
def _partialParseTimeStr(self, s, sourceTime):
|
||
|
"""
|
||
|
test if giving C{s} matched CRE_TIME, used by L{parse()}
|
||
|
|
||
|
@type s: string
|
||
|
@param s: date/time text to evaluate
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: tuple
|
||
|
@return: tuple of remained date/time text, datetime object and
|
||
|
an boolean value to describ if matched or not
|
||
|
|
||
|
"""
|
||
|
parseStr = None
|
||
|
chunk1 = chunk2 = ''
|
||
|
|
||
|
# Natural language time strings
|
||
|
m = self.ptc.CRE_TIME.search(s)
|
||
|
if m is not None or s in self.ptc.re_values['now']:
|
||
|
|
||
|
if (m and m.group() != s):
|
||
|
# capture remaining string
|
||
|
parseStr = m.group()
|
||
|
chunk1 = s[:m.start()]
|
||
|
chunk2 = s[m.end():]
|
||
|
s = '%s %s' % (chunk1, chunk2)
|
||
|
else:
|
||
|
parseStr = s
|
||
|
s = ''
|
||
|
|
||
|
if parseStr:
|
||
|
debug and log.debug(
|
||
|
'found (time) [%s][%s][%s]', parseStr, chunk1, chunk2)
|
||
|
sourceTime = self._evalTimeStr(parseStr, sourceTime)
|
||
|
|
||
|
return s, sourceTime, bool(parseStr)
|
||
|
|
||
|
def _partialParseMeridian(self, s, sourceTime):
|
||
|
"""
|
||
|
test if giving C{s} matched CRE_TIMEHMS2, used by L{parse()}
|
||
|
|
||
|
@type s: string
|
||
|
@param s: date/time text to evaluate
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: tuple
|
||
|
@return: tuple of remained date/time text, datetime object and
|
||
|
an boolean value to describ if matched or not
|
||
|
|
||
|
"""
|
||
|
parseStr = None
|
||
|
chunk1 = chunk2 = ''
|
||
|
|
||
|
# HH:MM(:SS) am/pm time strings
|
||
|
m = self.ptc.CRE_TIMEHMS2.search(s)
|
||
|
if m is not None:
|
||
|
|
||
|
if m.group('minutes') is not None:
|
||
|
if m.group('seconds') is not None:
|
||
|
parseStr = '%s:%s:%s' % (m.group('hours'),
|
||
|
m.group('minutes'),
|
||
|
m.group('seconds'))
|
||
|
else:
|
||
|
parseStr = '%s:%s' % (m.group('hours'),
|
||
|
m.group('minutes'))
|
||
|
else:
|
||
|
parseStr = m.group('hours')
|
||
|
parseStr += ' ' + m.group('meridian')
|
||
|
|
||
|
chunk1 = s[:m.start()]
|
||
|
chunk2 = s[m.end():]
|
||
|
|
||
|
s = '%s %s' % (chunk1, chunk2)
|
||
|
|
||
|
if parseStr:
|
||
|
debug and log.debug('found (meridian) [%s][%s][%s]',
|
||
|
parseStr, chunk1, chunk2)
|
||
|
sourceTime = self._evalMeridian(parseStr, sourceTime)
|
||
|
|
||
|
return s, sourceTime, bool(parseStr)
|
||
|
|
||
|
def _partialParseTimeStd(self, s, sourceTime):
|
||
|
"""
|
||
|
test if giving C{s} matched CRE_TIMEHMS, used by L{parse()}
|
||
|
|
||
|
@type s: string
|
||
|
@param s: date/time text to evaluate
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
|
||
|
@rtype: tuple
|
||
|
@return: tuple of remained date/time text, datetime object and
|
||
|
an boolean value to describ if matched or not
|
||
|
|
||
|
"""
|
||
|
parseStr = None
|
||
|
chunk1 = chunk2 = ''
|
||
|
|
||
|
# HH:MM(:SS) time strings
|
||
|
m = self.ptc.CRE_TIMEHMS.search(s)
|
||
|
if m is not None:
|
||
|
|
||
|
if m.group('seconds') is not None:
|
||
|
parseStr = '%s:%s:%s' % (m.group('hours'),
|
||
|
m.group('minutes'),
|
||
|
m.group('seconds'))
|
||
|
chunk1 = s[:m.start('hours')]
|
||
|
chunk2 = s[m.end('seconds'):]
|
||
|
else:
|
||
|
parseStr = '%s:%s' % (m.group('hours'),
|
||
|
m.group('minutes'))
|
||
|
chunk1 = s[:m.start('hours')]
|
||
|
chunk2 = s[m.end('minutes'):]
|
||
|
|
||
|
s = '%s %s' % (chunk1, chunk2)
|
||
|
|
||
|
if parseStr:
|
||
|
debug and log.debug(
|
||
|
'found (hms) [%s][%s][%s]', parseStr, chunk1, chunk2)
|
||
|
sourceTime = self._evalTimeStd(parseStr, sourceTime)
|
||
|
|
||
|
return s, sourceTime, bool(parseStr)
|
||
|
|
||
|
def parseDT(self, datetimeString, sourceTime=None,
|
||
|
tzinfo=None, version=None):
|
||
|
"""
|
||
|
C{datetimeString} is as C{.parse}, C{sourceTime} has the same semantic
|
||
|
meaning as C{.parse}, but now also accepts datetime objects. C{tzinfo}
|
||
|
accepts a tzinfo object. It is advisable to use pytz.
|
||
|
|
||
|
|
||
|
@type datetimeString: string
|
||
|
@param datetimeString: date/time text to evaluate
|
||
|
@type sourceTime: struct_time, datetime, date, time
|
||
|
@param sourceTime: time value to use as the base
|
||
|
@type tzinfo: tzinfo
|
||
|
@param tzinfo: Timezone to apply to generated datetime objs.
|
||
|
@type version: integer
|
||
|
@param version: style version, default will use L{Calendar}
|
||
|
parameter version value
|
||
|
|
||
|
@rtype: tuple
|
||
|
@return: tuple of: modified C{sourceTime} and the result flag/context
|
||
|
|
||
|
see .parse for return code details.
|
||
|
"""
|
||
|
# if sourceTime has a timetuple method, use thet, else, just pass the
|
||
|
# entire thing to parse and prey the user knows what the hell they are
|
||
|
# doing.
|
||
|
sourceTime = getattr(sourceTime, 'timetuple', (lambda: sourceTime))()
|
||
|
# You REALLY SHOULD be using pytz. Using localize if available,
|
||
|
# hacking if not. Note, None is a valid tzinfo object in the case of
|
||
|
# the ugly hack.
|
||
|
localize = getattr(
|
||
|
tzinfo,
|
||
|
'localize',
|
||
|
(lambda dt: dt.replace(tzinfo=tzinfo)), # ugly hack is ugly :(
|
||
|
)
|
||
|
|
||
|
# Punt
|
||
|
time_struct, ret_code = self.parse(
|
||
|
datetimeString,
|
||
|
sourceTime=sourceTime,
|
||
|
version=version)
|
||
|
|
||
|
# Comments from GHI indicate that it is desired to have the same return
|
||
|
# signature on this method as that one it punts to, with the exception
|
||
|
# of using datetime objects instead of time_structs.
|
||
|
dt = localize(datetime.datetime(*time_struct[:6]))
|
||
|
return dt, ret_code
|
||
|
|
||
|
def parse(self, datetimeString, sourceTime=None, version=None):
|
||
|
"""
|
||
|
Splits the given C{datetimeString} into tokens, finds the regex
|
||
|
patterns that match and then calculates a C{struct_time} value from
|
||
|
the chunks.
|
||
|
|
||
|
If C{sourceTime} is given then the C{struct_time} value will be
|
||
|
calculated from that value, otherwise from the current date/time.
|
||
|
|
||
|
If the C{datetimeString} is parsed and date/time value found, then::
|
||
|
|
||
|
If C{version} equals to L{VERSION_FLAG_STYLE}, the second item of
|
||
|
the returned tuple will be a flag to let you know what kind of
|
||
|
C{struct_time} value is being returned::
|
||
|
|
||
|
0 = not parsed at all
|
||
|
1 = parsed as a C{date}
|
||
|
2 = parsed as a C{time}
|
||
|
3 = parsed as a C{datetime}
|
||
|
|
||
|
If C{version} equals to L{VERSION_CONTEXT_STYLE}, the second value
|
||
|
will be an instance of L{pdtContext}
|
||
|
|
||
|
@type datetimeString: string
|
||
|
@param datetimeString: date/time text to evaluate
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
@type version: integer
|
||
|
@param version: style version, default will use L{Calendar}
|
||
|
parameter version value
|
||
|
|
||
|
@rtype: tuple
|
||
|
@return: tuple of: modified C{sourceTime} and the result flag/context
|
||
|
"""
|
||
|
debug and log.debug('parse()')
|
||
|
|
||
|
datetimeString = re.sub(r'(\w)\.(\s)', r'\1\2', datetimeString)
|
||
|
datetimeString = re.sub(r'(\w)[\'"](\s|$)', r'\1 \2', datetimeString)
|
||
|
datetimeString = re.sub(r'(\s|^)[\'"](\w)', r'\1 \2', datetimeString)
|
||
|
|
||
|
if sourceTime:
|
||
|
if isinstance(sourceTime, datetime.datetime):
|
||
|
debug and log.debug('coercing datetime to timetuple')
|
||
|
sourceTime = sourceTime.timetuple()
|
||
|
else:
|
||
|
if not isinstance(sourceTime, time.struct_time) and \
|
||
|
not isinstance(sourceTime, tuple):
|
||
|
raise ValueError('sourceTime is not a struct_time')
|
||
|
else:
|
||
|
sourceTime = time.localtime()
|
||
|
|
||
|
with self.context() as ctx:
|
||
|
s = datetimeString.lower().strip()
|
||
|
debug and log.debug('remainedString (before parsing): [%s]', s)
|
||
|
|
||
|
while s:
|
||
|
for parseMeth in (self._partialParseModifier,
|
||
|
self._partialParseUnits,
|
||
|
self._partialParseQUnits,
|
||
|
self._partialParseDateStr,
|
||
|
self._partialParseDateStd,
|
||
|
self._partialParseDayStr,
|
||
|
self._partialParseWeekday,
|
||
|
self._partialParseTimeStr,
|
||
|
self._partialParseMeridian,
|
||
|
self._partialParseTimeStd):
|
||
|
retS, retTime, matched = parseMeth(s, sourceTime)
|
||
|
if matched:
|
||
|
s, sourceTime = retS.strip(), retTime
|
||
|
break
|
||
|
else:
|
||
|
# nothing matched
|
||
|
s = ''
|
||
|
|
||
|
debug and log.debug('hasDate: [%s], hasTime: [%s]',
|
||
|
ctx.hasDate, ctx.hasTime)
|
||
|
debug and log.debug('remainedString: [%s]', s)
|
||
|
|
||
|
# String is not parsed at all
|
||
|
if sourceTime is None:
|
||
|
debug and log.debug('not parsed [%s]', str(sourceTime))
|
||
|
sourceTime = time.localtime()
|
||
|
|
||
|
if not isinstance(sourceTime, time.struct_time):
|
||
|
sourceTime = time.struct_time(sourceTime)
|
||
|
|
||
|
version = self.version if version is None else version
|
||
|
if version == VERSION_CONTEXT_STYLE:
|
||
|
return sourceTime, ctx
|
||
|
else:
|
||
|
return sourceTime, ctx.dateTimeFlag
|
||
|
|
||
|
def inc(self, source, month=None, year=None):
|
||
|
"""
|
||
|
Takes the given C{source} date, or current date if none is
|
||
|
passed, and increments it according to the values passed in
|
||
|
by month and/or year.
|
||
|
|
||
|
This routine is needed because Python's C{timedelta()} function
|
||
|
does not allow for month or year increments.
|
||
|
|
||
|
@type source: struct_time
|
||
|
@param source: C{struct_time} value to increment
|
||
|
@type month: float or integer
|
||
|
@param month: optional number of months to increment
|
||
|
@type year: float or integer
|
||
|
@param year: optional number of years to increment
|
||
|
|
||
|
@rtype: datetime
|
||
|
@return: C{source} incremented by the number of months and/or years
|
||
|
"""
|
||
|
yr = source.year
|
||
|
mth = source.month
|
||
|
dy = source.day
|
||
|
|
||
|
try:
|
||
|
month = float(month)
|
||
|
except (TypeError, ValueError):
|
||
|
month = 0
|
||
|
|
||
|
try:
|
||
|
year = float(year)
|
||
|
except (TypeError, ValueError):
|
||
|
year = 0
|
||
|
finally:
|
||
|
month += year * 12
|
||
|
year = 0
|
||
|
|
||
|
subMi = 0.0
|
||
|
maxDay = 0
|
||
|
if month:
|
||
|
mi = int(month)
|
||
|
subMi = month - mi
|
||
|
|
||
|
y = int(mi / 12.0)
|
||
|
m = mi - y * 12
|
||
|
|
||
|
mth = mth + m
|
||
|
if mth < 1: # cross start-of-year?
|
||
|
y -= 1 # yes - decrement year
|
||
|
mth += 12 # and fix month
|
||
|
elif mth > 12: # cross end-of-year?
|
||
|
y += 1 # yes - increment year
|
||
|
mth -= 12 # and fix month
|
||
|
|
||
|
yr += y
|
||
|
|
||
|
# if the day ends up past the last day of
|
||
|
# the new month, set it to the last day
|
||
|
maxDay = self.ptc.daysInMonth(mth, yr)
|
||
|
if dy > maxDay:
|
||
|
dy = maxDay
|
||
|
|
||
|
if yr > datetime.MAXYEAR or yr < datetime.MINYEAR:
|
||
|
raise OverflowError('year is out of range')
|
||
|
|
||
|
d = source.replace(year=yr, month=mth, day=dy)
|
||
|
if subMi:
|
||
|
d += datetime.timedelta(days=subMi * maxDay)
|
||
|
return source + (d - source)
|
||
|
|
||
|
def nlp(self, inputString, sourceTime=None, version=None):
|
||
|
"""Utilizes parse() after making judgements about what datetime
|
||
|
information belongs together.
|
||
|
|
||
|
It makes logical groupings based on proximity and returns a parsed
|
||
|
datetime for each matched grouping of datetime text, along with
|
||
|
location info within the given inputString.
|
||
|
|
||
|
@type inputString: string
|
||
|
@param inputString: natural language text to evaluate
|
||
|
@type sourceTime: struct_time
|
||
|
@param sourceTime: C{struct_time} value to use as the base
|
||
|
@type version: integer
|
||
|
@param version: style version, default will use L{Calendar}
|
||
|
parameter version value
|
||
|
|
||
|
@rtype: tuple or None
|
||
|
@return: tuple of tuples in the format (parsed_datetime as
|
||
|
datetime.datetime, flags as int, start_pos as int,
|
||
|
end_pos as int, matched_text as string) or None if there
|
||
|
were no matches
|
||
|
"""
|
||
|
|
||
|
orig_inputstring = inputString
|
||
|
|
||
|
# replace periods at the end of sentences w/ spaces
|
||
|
# opposed to removing them altogether in order to
|
||
|
# retain relative positions (identified by alpha, period, space).
|
||
|
# this is required for some of the regex patterns to match
|
||
|
inputString = re.sub(r'(\w)(\.)(\s)', r'\1 \3', inputString).lower()
|
||
|
inputString = re.sub(r'(\w)(\'|")(\s|$)', r'\1 \3', inputString)
|
||
|
inputString = re.sub(r'(\s|^)(\'|")(\w)', r'\1 \3', inputString)
|
||
|
|
||
|
startpos = 0 # the start position in the inputString during the loop
|
||
|
|
||
|
# list of lists in format:
|
||
|
# [startpos, endpos, matchedstring, flags, type]
|
||
|
matches = []
|
||
|
|
||
|
while startpos < len(inputString):
|
||
|
|
||
|
# empty match
|
||
|
leftmost_match = [0, 0, None, 0, None]
|
||
|
|
||
|
# Modifier like next\prev..
|
||
|
m = self.ptc.CRE_MODIFIER.search(inputString[startpos:])
|
||
|
if m is not None:
|
||
|
if leftmost_match[1] == 0 or \
|
||
|
leftmost_match[0] > m.start() + startpos:
|
||
|
leftmost_match[0] = m.start() + startpos
|
||
|
leftmost_match[1] = m.end() + startpos
|
||
|
leftmost_match[2] = m.group()
|
||
|
leftmost_match[3] = 0
|
||
|
leftmost_match[4] = 'modifier'
|
||
|
|
||
|
# Quantity + Units
|
||
|
m = self.ptc.CRE_UNITS.search(inputString[startpos:])
|
||
|
if m is not None:
|
||
|
debug and log.debug('CRE_UNITS matched')
|
||
|
if self._UnitsTrapped(inputString[startpos:], m, 'units'):
|
||
|
debug and log.debug('day suffix trapped by unit match')
|
||
|
else:
|
||
|
|
||
|
if leftmost_match[1] == 0 or \
|
||
|
leftmost_match[0] > m.start('qty') + startpos:
|
||
|
leftmost_match[0] = m.start('qty') + startpos
|
||
|
leftmost_match[1] = m.end('qty') + startpos
|
||
|
leftmost_match[2] = m.group('qty')
|
||
|
leftmost_match[3] = 3
|
||
|
leftmost_match[4] = 'units'
|
||
|
|
||
|
if m.start('qty') > 0 and \
|
||
|
inputString[m.start('qty') - 1] == '-':
|
||
|
leftmost_match[0] = leftmost_match[0] - 1
|
||
|
leftmost_match[2] = '-' + leftmost_match[2]
|
||
|
|
||
|
# Quantity + Units
|
||
|
m = self.ptc.CRE_QUNITS.search(inputString[startpos:])
|
||
|
if m is not None:
|
||
|
debug and log.debug('CRE_QUNITS matched')
|
||
|
if self._UnitsTrapped(inputString[startpos:], m, 'qunits'):
|
||
|
debug and log.debug('day suffix trapped by qunit match')
|
||
|
else:
|
||
|
if leftmost_match[1] == 0 or \
|
||
|
leftmost_match[0] > m.start('qty') + startpos:
|
||
|
leftmost_match[0] = m.start('qty') + startpos
|
||
|
leftmost_match[1] = m.end('qty') + startpos
|
||
|
leftmost_match[2] = m.group('qty')
|
||
|
leftmost_match[3] = 3
|
||
|
leftmost_match[4] = 'qunits'
|
||
|
|
||
|
if m.start('qty') > 0 and \
|
||
|
inputString[m.start('qty') - 1] == '-':
|
||
|
leftmost_match[0] = leftmost_match[0] - 1
|
||
|
leftmost_match[2] = '-' + leftmost_match[2]
|
||
|
|
||
|
m = self.ptc.CRE_DATE3.search(inputString[startpos:])
|
||
|
# NO LONGER NEEDED, THE REGEXP HANDLED MTHNAME NOW
|
||
|
# for match in self.ptc.CRE_DATE3.finditer(inputString[startpos:]):
|
||
|
# to prevent "HH:MM(:SS) time strings" expressions from
|
||
|
# triggering this regex, we checks if the month field exists
|
||
|
# in the searched expression, if it doesn't exist, the date
|
||
|
# field is not valid
|
||
|
# if match.group('mthname'):
|
||
|
# m = self.ptc.CRE_DATE3.search(inputString[startpos:],
|
||
|
# match.start())
|
||
|
# break
|
||
|
|
||
|
# String date format
|
||
|
if m is not None:
|
||
|
if leftmost_match[1] == 0 or \
|
||
|
leftmost_match[0] > m.start('date') + startpos:
|
||
|
leftmost_match[0] = m.start('date') + startpos
|
||
|
leftmost_match[1] = m.end('date') + startpos
|
||
|
leftmost_match[2] = m.group('date')
|
||
|
leftmost_match[3] = 1
|
||
|
leftmost_match[4] = 'dateStr'
|
||
|
|
||
|
# Standard date format
|
||
|
m = self.ptc.CRE_DATE.search(inputString[startpos:])
|
||
|
if m is not None:
|
||
|
if leftmost_match[1] == 0 or \
|
||
|
leftmost_match[0] > m.start('date') + startpos:
|
||
|
leftmost_match[0] = m.start('date') + startpos
|
||
|
leftmost_match[1] = m.end('date') + startpos
|
||
|
leftmost_match[2] = m.group('date')
|
||
|
leftmost_match[3] = 1
|
||
|
leftmost_match[4] = 'dateStd'
|
||
|
|
||
|
# Natural language day strings
|
||
|
m = self.ptc.CRE_DAY.search(inputString[startpos:])
|
||
|
if m is not None:
|
||
|
if leftmost_match[1] == 0 or \
|
||
|
leftmost_match[0] > m.start() + startpos:
|
||
|
leftmost_match[0] = m.start() + startpos
|
||
|
leftmost_match[1] = m.end() + startpos
|
||
|
leftmost_match[2] = m.group()
|
||
|
leftmost_match[3] = 1
|
||
|
leftmost_match[4] = 'dayStr'
|
||
|
|
||
|
# Weekday
|
||
|
m = self.ptc.CRE_WEEKDAY.search(inputString[startpos:])
|
||
|
if m is not None:
|
||
|
if inputString[startpos:] not in self.ptc.dayOffsets:
|
||
|
if leftmost_match[1] == 0 or \
|
||
|
leftmost_match[0] > m.start() + startpos:
|
||
|
leftmost_match[0] = m.start() + startpos
|
||
|
leftmost_match[1] = m.end() + startpos
|
||
|
leftmost_match[2] = m.group()
|
||
|
leftmost_match[3] = 1
|
||
|
leftmost_match[4] = 'weekdy'
|
||
|
|
||
|
# Natural language time strings
|
||
|
m = self.ptc.CRE_TIME.search(inputString[startpos:])
|
||
|
if m is not None:
|
||
|
if leftmost_match[1] == 0 or \
|
||
|
leftmost_match[0] > m.start() + startpos:
|
||
|
leftmost_match[0] = m.start() + startpos
|
||
|
leftmost_match[1] = m.end() + startpos
|
||
|
leftmost_match[2] = m.group()
|
||
|
leftmost_match[3] = 2
|
||
|
leftmost_match[4] = 'timeStr'
|
||
|
|
||
|
# HH:MM(:SS) am/pm time strings
|
||
|
m = self.ptc.CRE_TIMEHMS2.search(inputString[startpos:])
|
||
|
if m is not None:
|
||
|
if leftmost_match[1] == 0 or \
|
||
|
leftmost_match[0] > m.start('hours') + startpos:
|
||
|
leftmost_match[0] = m.start('hours') + startpos
|
||
|
leftmost_match[1] = m.end('meridian') + startpos
|
||
|
leftmost_match[2] = inputString[leftmost_match[0]:
|
||
|
leftmost_match[1]]
|
||
|
leftmost_match[3] = 2
|
||
|
leftmost_match[4] = 'meridian'
|
||
|
|
||
|
# HH:MM(:SS) time strings
|
||
|
m = self.ptc.CRE_TIMEHMS.search(inputString[startpos:])
|
||
|
if m is not None:
|
||
|
if leftmost_match[1] == 0 or \
|
||
|
leftmost_match[0] > m.start('hours') + startpos:
|
||
|
leftmost_match[0] = m.start('hours') + startpos
|
||
|
if m.group('seconds') is not None:
|
||
|
leftmost_match[1] = m.end('seconds') + startpos
|
||
|
else:
|
||
|
leftmost_match[1] = m.end('minutes') + startpos
|
||
|
leftmost_match[2] = inputString[leftmost_match[0]:
|
||
|
leftmost_match[1]]
|
||
|
leftmost_match[3] = 2
|
||
|
leftmost_match[4] = 'timeStd'
|
||
|
|
||
|
# Units only; must be preceded by a modifier
|
||
|
if len(matches) > 0 and matches[-1][3] == 0:
|
||
|
m = self.ptc.CRE_UNITS_ONLY.search(inputString[startpos:])
|
||
|
# Ensure that any match is immediately proceded by the
|
||
|
# modifier. "Next is the word 'month'" should not parse as a
|
||
|
# date while "next month" should
|
||
|
if m is not None and \
|
||
|
inputString[startpos:startpos + m.start()].strip() == '':
|
||
|
debug and log.debug('CRE_UNITS_ONLY matched [%s]',
|
||
|
m.group())
|
||
|
if leftmost_match[1] == 0 or \
|
||
|
leftmost_match[0] > m.start() + startpos:
|
||
|
leftmost_match[0] = m.start() + startpos
|
||
|
leftmost_match[1] = m.end() + startpos
|
||
|
leftmost_match[2] = m.group()
|
||
|
leftmost_match[3] = 3
|
||
|
leftmost_match[4] = 'unitsOnly'
|
||
|
|
||
|
# set the start position to the end pos of the leftmost match
|
||
|
startpos = leftmost_match[1]
|
||
|
|
||
|
# nothing was detected
|
||
|
# so break out of the loop
|
||
|
if startpos == 0:
|
||
|
startpos = len(inputString)
|
||
|
else:
|
||
|
if leftmost_match[3] > 0:
|
||
|
m = self.ptc.CRE_NLP_PREFIX.search(
|
||
|
inputString[:leftmost_match[0]] + ' ' + str(leftmost_match[3]))
|
||
|
if m is not None:
|
||
|
leftmost_match[0] = m.start('nlp_prefix')
|
||
|
leftmost_match[2] = inputString[leftmost_match[0]:
|
||
|
leftmost_match[1]]
|
||
|
matches.append(leftmost_match)
|
||
|
|
||
|
# find matches in proximity with one another and
|
||
|
# return all the parsed values
|
||
|
proximity_matches = []
|
||
|
if len(matches) > 1:
|
||
|
combined = ''
|
||
|
from_match_index = 0
|
||
|
date = matches[0][3] == 1
|
||
|
time = matches[0][3] == 2
|
||
|
units = matches[0][3] == 3
|
||
|
for i in range(1, len(matches)):
|
||
|
|
||
|
# test proximity (are there characters between matches?)
|
||
|
endofprevious = matches[i - 1][1]
|
||
|
begofcurrent = matches[i][0]
|
||
|
if orig_inputstring[endofprevious:
|
||
|
begofcurrent].lower().strip() != '':
|
||
|
# this one isn't in proximity, but maybe
|
||
|
# we have enough to make a datetime
|
||
|
# TODO: make sure the combination of
|
||
|
# formats (modifier, dateStd, etc) makes logical sense
|
||
|
# before parsing together
|
||
|
if date or time or units:
|
||
|
combined = orig_inputstring[matches[from_match_index]
|
||
|
[0]:matches[i - 1][1]]
|
||
|
parsed_datetime, flags = self.parse(combined,
|
||
|
sourceTime,
|
||
|
version)
|
||
|
proximity_matches.append((
|
||
|
datetime.datetime(*parsed_datetime[:6]),
|
||
|
flags,
|
||
|
matches[from_match_index][0],
|
||
|
matches[i - 1][1],
|
||
|
combined))
|
||
|
# not in proximity, reset starting from current
|
||
|
from_match_index = i
|
||
|
date = matches[i][3] == 1
|
||
|
time = matches[i][3] == 2
|
||
|
units = matches[i][3] == 3
|
||
|
continue
|
||
|
else:
|
||
|
if matches[i][3] == 1:
|
||
|
date = True
|
||
|
if matches[i][3] == 2:
|
||
|
time = True
|
||
|
if matches[i][3] == 3:
|
||
|
units = True
|
||
|
|
||
|
# check last
|
||
|
# we have enough to make a datetime
|
||
|
if date or time or units:
|
||
|
combined = orig_inputstring[matches[from_match_index][0]:
|
||
|
matches[len(matches) - 1][1]]
|
||
|
parsed_datetime, flags = self.parse(combined, sourceTime,
|
||
|
version)
|
||
|
proximity_matches.append((
|
||
|
datetime.datetime(*parsed_datetime[:6]),
|
||
|
flags,
|
||
|
matches[from_match_index][0],
|
||
|
matches[len(matches) - 1][1],
|
||
|
combined))
|
||
|
|
||
|
elif len(matches) == 0:
|
||
|
return None
|
||
|
else:
|
||
|
if matches[0][3] == 0: # not enough info to parse
|
||
|
return None
|
||
|
else:
|
||
|
combined = orig_inputstring[matches[0][0]:matches[0][1]]
|
||
|
parsed_datetime, flags = self.parse(matches[0][2], sourceTime,
|
||
|
version)
|
||
|
proximity_matches.append((
|
||
|
datetime.datetime(*parsed_datetime[:6]),
|
||
|
flags,
|
||
|
matches[0][0],
|
||
|
matches[0][1],
|
||
|
combined))
|
||
|
|
||
|
return tuple(proximity_matches)
|
||
|
|
||
|
|
||
|
def _initSymbols(ptc):
|
||
|
"""
|
||
|
Initialize symbols and single character constants.
|
||
|
"""
|
||
|
# build am and pm lists to contain
|
||
|
# original case, lowercase, first-char and dotted
|
||
|
# versions of the meridian text
|
||
|
ptc.am = ['', '']
|
||
|
ptc.pm = ['', '']
|
||
|
for idx, xm in enumerate(ptc.locale.meridian[:2]):
|
||
|
# 0: am
|
||
|
# 1: pm
|
||
|
target = ['am', 'pm'][idx]
|
||
|
setattr(ptc, target, [xm])
|
||
|
target = getattr(ptc, target)
|
||
|
if xm:
|
||
|
lxm = xm.lower()
|
||
|
target.extend((xm[0], '{0}.{1}.'.format(*xm),
|
||
|
lxm, lxm[0], '{0}.{1}.'.format(*lxm)))
|
||
|
|
||
|
|
||
|
class Constants(object):
|
||
|
|
||
|
"""
|
||
|
Default set of constants for parsedatetime.
|
||
|
|
||
|
If PyICU is present, then the class will first try to get PyICU
|
||
|
to return a locale specified by C{localeID}. If either C{localeID} is
|
||
|
None or if the locale does not exist within PyICU, then each of the
|
||
|
locales defined in C{fallbackLocales} is tried in order.
|
||
|
|
||
|
If PyICU is not present or none of the specified locales can be used,
|
||
|
then the class will initialize itself to the en_US locale.
|
||
|
|
||
|
if PyICU is not present or not requested, only the locales defined by
|
||
|
C{pdtLocales} will be searched.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, localeID=None, usePyICU=True,
|
||
|
fallbackLocales=['en_US']):
|
||
|
self.localeID = localeID
|
||
|
self.fallbackLocales = fallbackLocales[:]
|
||
|
|
||
|
if 'en_US' not in self.fallbackLocales:
|
||
|
self.fallbackLocales.append('en_US')
|
||
|
|
||
|
# define non-locale specific constants
|
||
|
self.locale = None
|
||
|
self.usePyICU = usePyICU
|
||
|
|
||
|
# starting cache of leap years
|
||
|
# daysInMonth will add to this if during
|
||
|
# runtime it gets a request for a year not found
|
||
|
self._leapYears = list(range(1904, 2097, 4))
|
||
|
|
||
|
self.Second = 1
|
||
|
self.Minute = 60 # 60 * self.Second
|
||
|
self.Hour = 3600 # 60 * self.Minute
|
||
|
self.Day = 86400 # 24 * self.Hour
|
||
|
self.Week = 604800 # 7 * self.Day
|
||
|
self.Month = 2592000 # 30 * self.Day
|
||
|
self.Year = 31536000 # 365 * self.Day
|
||
|
|
||
|
self._DaysInMonthList = (31, 28, 31, 30, 31, 30,
|
||
|
31, 31, 30, 31, 30, 31)
|
||
|
self.rangeSep = '-'
|
||
|
self.BirthdayEpoch = 50
|
||
|
|
||
|
# When True the starting time for all relative calculations will come
|
||
|
# from the given SourceTime, otherwise it will be self.StartHour
|
||
|
|
||
|
self.StartTimeFromSourceTime = False
|
||
|
|
||
|
# The hour of the day that will be used as the starting time for all
|
||
|
# relative calculations when self.StartTimeFromSourceTime is False
|
||
|
|
||
|
self.StartHour = 9
|
||
|
|
||
|
# YearParseStyle controls how we parse "Jun 12", i.e. dates that do
|
||
|
# not have a year present. The default is to compare the date given
|
||
|
# to the current date, and if prior, then assume the next year.
|
||
|
# Setting this to 0 will prevent that.
|
||
|
|
||
|
self.YearParseStyle = 1
|
||
|
|
||
|
# DOWParseStyle controls how we parse "Tuesday"
|
||
|
# If the current day was Thursday and the text to parse is "Tuesday"
|
||
|
# then the following table shows how each style would be returned
|
||
|
# -1, 0, +1
|
||
|
#
|
||
|
# Current day marked as ***
|
||
|
#
|
||
|
# Sun Mon Tue Wed Thu Fri Sat
|
||
|
# week -1
|
||
|
# current -1,0 ***
|
||
|
# week +1 +1
|
||
|
#
|
||
|
# If the current day was Monday and the text to parse is "Tuesday"
|
||
|
# then the following table shows how each style would be returned
|
||
|
# -1, 0, +1
|
||
|
#
|
||
|
# Sun Mon Tue Wed Thu Fri Sat
|
||
|
# week -1 -1
|
||
|
# current *** 0,+1
|
||
|
# week +1
|
||
|
|
||
|
self.DOWParseStyle = 1
|
||
|
|
||
|
# CurrentDOWParseStyle controls how we parse "Friday"
|
||
|
# If the current day was Friday and the text to parse is "Friday"
|
||
|
# then the following table shows how each style would be returned
|
||
|
# True/False. This also depends on DOWParseStyle.
|
||
|
#
|
||
|
# Current day marked as ***
|
||
|
#
|
||
|
# DOWParseStyle = 0
|
||
|
# Sun Mon Tue Wed Thu Fri Sat
|
||
|
# week -1
|
||
|
# current T,F
|
||
|
# week +1
|
||
|
#
|
||
|
# DOWParseStyle = -1
|
||
|
# Sun Mon Tue Wed Thu Fri Sat
|
||
|
# week -1 F
|
||
|
# current T
|
||
|
# week +1
|
||
|
#
|
||
|
# DOWParseStyle = +1
|
||
|
#
|
||
|
# Sun Mon Tue Wed Thu Fri Sat
|
||
|
# week -1
|
||
|
# current T
|
||
|
# week +1 F
|
||
|
|
||
|
self.CurrentDOWParseStyle = False
|
||
|
|
||
|
if self.usePyICU:
|
||
|
self.locale = get_icu(self.localeID)
|
||
|
|
||
|
if self.locale.icu is None:
|
||
|
self.usePyICU = False
|
||
|
self.locale = None
|
||
|
|
||
|
if self.locale is None:
|
||
|
if self.localeID not in pdtLocales:
|
||
|
for localeId in range(0, len(self.fallbackLocales)):
|
||
|
self.localeID = self.fallbackLocales[localeId]
|
||
|
if self.localeID in pdtLocales:
|
||
|
break
|
||
|
|
||
|
self.locale = pdtLocales[self.localeID]
|
||
|
|
||
|
if self.locale is not None:
|
||
|
|
||
|
def _getLocaleDataAdjusted(localeData):
|
||
|
"""
|
||
|
If localeData is defined as ["mon|mnd", 'tu|tues'...] then this
|
||
|
function splits those definitions on |
|
||
|
"""
|
||
|
adjusted = []
|
||
|
for d in localeData:
|
||
|
if '|' in d:
|
||
|
adjusted += d.split("|")
|
||
|
else:
|
||
|
adjusted.append(d)
|
||
|
return adjusted
|
||
|
|
||
|
def re_join(g):
|
||
|
return '|'.join(re.escape(i) for i in g)
|
||
|
|
||
|
mths = _getLocaleDataAdjusted(self.locale.Months)
|
||
|
smths = _getLocaleDataAdjusted(self.locale.shortMonths)
|
||
|
swds = _getLocaleDataAdjusted(self.locale.shortWeekdays)
|
||
|
wds = _getLocaleDataAdjusted(self.locale.Weekdays)
|
||
|
|
||
|
# escape any regex special characters that may be found
|
||
|
self.locale.re_values['months'] = re_join(mths)
|
||
|
self.locale.re_values['shortmonths'] = re_join(smths)
|
||
|
self.locale.re_values['days'] = re_join(wds)
|
||
|
self.locale.re_values['shortdays'] = re_join(swds)
|
||
|
self.locale.re_values['dayoffsets'] = \
|
||
|
re_join(self.locale.dayOffsets)
|
||
|
self.locale.re_values['numbers'] = \
|
||
|
re_join(self.locale.numbers)
|
||
|
self.locale.re_values['decimal_mark'] = \
|
||
|
re.escape(self.locale.decimal_mark)
|
||
|
|
||
|
units = [unit for units in self.locale.units.values()
|
||
|
for unit in units] # flatten
|
||
|
units.sort(key=len, reverse=True) # longest first
|
||
|
self.locale.re_values['units'] = re_join(units)
|
||
|
self.locale.re_values['modifiers'] = re_join(self.locale.Modifiers)
|
||
|
self.locale.re_values['sources'] = re_join(self.locale.re_sources)
|
||
|
|
||
|
# For distinguishing numeric dates from times, look for timeSep
|
||
|
# and meridian, if specified in the locale
|
||
|
self.locale.re_values['timecomponents'] = \
|
||
|
re_join(self.locale.timeSep + self.locale.meridian)
|
||
|
|
||
|
# build weekday offsets - yes, it assumes the Weekday and
|
||
|
# shortWeekday lists are in the same order and Mon..Sun
|
||
|
# (Python style)
|
||
|
def _buildOffsets(offsetDict, localeData, indexStart):
|
||
|
o = indexStart
|
||
|
for key in localeData:
|
||
|
if '|' in key:
|
||
|
for k in key.split('|'):
|
||
|
offsetDict[k] = o
|
||
|
else:
|
||
|
offsetDict[key] = o
|
||
|
o += 1
|
||
|
|
||
|
_buildOffsets(self.locale.WeekdayOffsets,
|
||
|
self.locale.Weekdays, 0)
|
||
|
_buildOffsets(self.locale.WeekdayOffsets,
|
||
|
self.locale.shortWeekdays, 0)
|
||
|
|
||
|
# build month offsets - yes, it assumes the Months and shortMonths
|
||
|
# lists are in the same order and Jan..Dec
|
||
|
_buildOffsets(self.locale.MonthOffsets,
|
||
|
self.locale.Months, 1)
|
||
|
_buildOffsets(self.locale.MonthOffsets,
|
||
|
self.locale.shortMonths, 1)
|
||
|
|
||
|
_initSymbols(self)
|
||
|
|
||
|
# TODO: add code to parse the date formats and build the regexes up
|
||
|
# from sub-parts, find all hard-coded uses of date/time separators
|
||
|
|
||
|
# not being used in code, but kept in case others are manually
|
||
|
# utilizing this regex for their own purposes
|
||
|
self.RE_DATE4 = r'''(?P<date>
|
||
|
(
|
||
|
(
|
||
|
(?P<day>\d\d?)
|
||
|
(?P<suffix>{daysuffix})?
|
||
|
(,)?
|
||
|
(\s)*
|
||
|
)
|
||
|
(?P<mthname>
|
||
|
\b({months}|{shortmonths})\b
|
||
|
)\s*
|
||
|
(?P<year>\d\d
|
||
|
(\d\d)?
|
||
|
)?
|
||
|
)
|
||
|
)'''.format(**self.locale.re_values)
|
||
|
|
||
|
# still not completely sure of the behavior of the regex and
|
||
|
# whether it would be best to consume all possible irrelevant
|
||
|
# characters before the option groups (but within the {1,3} repetition
|
||
|
# group or inside of each option group, as it currently does
|
||
|
# however, right now, all tests are passing that were,
|
||
|
# including fixing the bug of matching a 4-digit year as ddyy
|
||
|
# when the day is absent from the string
|
||
|
self.RE_DATE3 = r'''(?P<date>
|
||
|
(?:
|
||
|
(?:^|\s+)
|
||
|
(?P<mthname>
|
||
|
{months}|{shortmonths}
|
||
|
)\b
|
||
|
|
|
||
|
(?:^|\s+)
|
||
|
(?P<day>[1-9]|[012]\d|3[01])
|
||
|
(?P<suffix>{daysuffix}|)\b
|
||
|
(?!\s*(?:{timecomponents}))
|
||
|
|
|
||
|
,?\s+
|
||
|
(?P<year>\d\d(?:\d\d|))\b
|
||
|
(?!\s*(?:{timecomponents}))
|
||
|
){{1,3}}
|
||
|
(?(mthname)|$-^)
|
||
|
)'''.format(**self.locale.re_values)
|
||
|
|
||
|
# not being used in code, but kept in case others are manually
|
||
|
# utilizing this regex for their own purposes
|
||
|
self.RE_MONTH = r'''(\s+|^)
|
||
|
(?P<month>
|
||
|
(
|
||
|
(?P<mthname>
|
||
|
\b({months}|{shortmonths})\b
|
||
|
)
|
||
|
(\s*
|
||
|
(?P<year>(\d{{4}}))
|
||
|
)?
|
||
|
)
|
||
|
)
|
||
|
(?=\s+|$|[^\w])'''.format(**self.locale.re_values)
|
||
|
|
||
|
self.RE_WEEKDAY = r'''\b
|
||
|
(?:
|
||
|
{days}|{shortdays}
|
||
|
)
|
||
|
\b'''.format(**self.locale.re_values)
|
||
|
|
||
|
self.RE_NUMBER = (r'(\b(?:{numbers})\b|\d+(?:{decimal_mark}\d+|))'
|
||
|
.format(**self.locale.re_values))
|
||
|
|
||
|
self.RE_SPECIAL = (r'(?P<special>^[{specials}]+)\s+'
|
||
|
.format(**self.locale.re_values))
|
||
|
|
||
|
self.RE_UNITS_ONLY = (r'''\b({units})\b'''
|
||
|
.format(**self.locale.re_values))
|
||
|
|
||
|
self.RE_UNITS = r'''\b(?P<qty>
|
||
|
-?
|
||
|
(?:\d+(?:{decimal_mark}\d+|)|(?:{numbers})\b)\s*
|
||
|
(?P<units>{units})
|
||
|
)\b'''.format(**self.locale.re_values)
|
||
|
|
||
|
self.RE_QUNITS = r'''\b(?P<qty>
|
||
|
-?
|
||
|
(?:\d+(?:{decimal_mark}\d+|)|(?:{numbers})\s+)\s*
|
||
|
(?P<qunits>{qunits})
|
||
|
)\b'''.format(**self.locale.re_values)
|
||
|
|
||
|
self.RE_MODIFIER = r'''\b(?:
|
||
|
{modifiers}
|
||
|
)\b'''.format(**self.locale.re_values)
|
||
|
|
||
|
self.RE_TIMEHMS = r'''([\s(\["'-]|^)
|
||
|
(?P<hours>\d\d?)
|
||
|
(?P<tsep>{timeseparator}|)
|
||
|
(?P<minutes>\d\d)
|
||
|
(?:(?P=tsep)
|
||
|
(?P<seconds>\d\d
|
||
|
(?:[\.,]\d+)?
|
||
|
)
|
||
|
)?\b'''.format(**self.locale.re_values)
|
||
|
|
||
|
self.RE_TIMEHMS2 = r'''([\s(\["'-]|^)
|
||
|
(?P<hours>\d\d?)
|
||
|
(?:
|
||
|
(?P<tsep>{timeseparator}|)
|
||
|
(?P<minutes>\d\d?)
|
||
|
(?:(?P=tsep)
|
||
|
(?P<seconds>\d\d?
|
||
|
(?:[\.,]\d+)?
|
||
|
)
|
||
|
)?
|
||
|
)?'''.format(**self.locale.re_values)
|
||
|
|
||
|
# 1, 2, and 3 here refer to the type of match date, time, or units
|
||
|
self.RE_NLP_PREFIX = r'''\b(?P<nlp_prefix>
|
||
|
(on)
|
||
|
(\s)+1
|
||
|
|
|
||
|
(at|in)
|
||
|
(\s)+2
|
||
|
|
|
||
|
(in)
|
||
|
(\s)+3
|
||
|
)'''
|
||
|
|
||
|
if 'meridian' in self.locale.re_values:
|
||
|
self.RE_TIMEHMS2 += (r'\s*(?P<meridian>{meridian})\b'
|
||
|
.format(**self.locale.re_values))
|
||
|
else:
|
||
|
self.RE_TIMEHMS2 += r'\b'
|
||
|
|
||
|
# Always support common . and - separators
|
||
|
dateSeps = ''.join(re.escape(s)
|
||
|
for s in self.locale.dateSep + ['-', '.'])
|
||
|
|
||
|
self.RE_DATE = r'''([\s(\["'-]|^)
|
||
|
(?P<date>
|
||
|
\d\d?[{0}]\d\d?(?:[{0}]\d\d(?:\d\d)?)?
|
||
|
|
|
||
|
\d{{4}}[{0}]\d\d?[{0}]\d\d?
|
||
|
)
|
||
|
\b'''.format(dateSeps)
|
||
|
|
||
|
self.RE_DATE2 = r'[{0}]'.format(dateSeps)
|
||
|
|
||
|
assert 'dayoffsets' in self.locale.re_values
|
||
|
|
||
|
self.RE_DAY = r'''\b
|
||
|
(?:
|
||
|
{dayoffsets}
|
||
|
)
|
||
|
\b'''.format(**self.locale.re_values)
|
||
|
|
||
|
self.RE_DAY2 = r'''(?P<day>\d\d?)
|
||
|
(?P<suffix>{daysuffix})?
|
||
|
'''.format(**self.locale.re_values)
|
||
|
|
||
|
self.RE_TIME = r'''\b
|
||
|
(?:
|
||
|
{sources}
|
||
|
)
|
||
|
\b'''.format(**self.locale.re_values)
|
||
|
|
||
|
self.RE_REMAINING = r'\s+'
|
||
|
|
||
|
# Regex for date/time ranges
|
||
|
self.RE_RTIMEHMS = r'''(\s*|^)
|
||
|
(\d\d?){timeseparator}
|
||
|
(\d\d)
|
||
|
({timeseparator}(\d\d))?
|
||
|
(\s*|$)'''.format(**self.locale.re_values)
|
||
|
|
||
|
self.RE_RTIMEHMS2 = (r'''(\s*|^)
|
||
|
(\d\d?)
|
||
|
({timeseparator}(\d\d?))?
|
||
|
({timeseparator}(\d\d?))?'''
|
||
|
.format(**self.locale.re_values))
|
||
|
|
||
|
if 'meridian' in self.locale.re_values:
|
||
|
self.RE_RTIMEHMS2 += (r'\s*({meridian})'
|
||
|
.format(**self.locale.re_values))
|
||
|
|
||
|
self.RE_RDATE = r'(\d+([%s]\d+)+)' % dateSeps
|
||
|
self.RE_RDATE3 = r'''(
|
||
|
(
|
||
|
(
|
||
|
\b({months})\b
|
||
|
)\s*
|
||
|
(
|
||
|
(\d\d?)
|
||
|
(\s?|{daysuffix}|$)+
|
||
|
)?
|
||
|
(,\s*\d{{4}})?
|
||
|
)
|
||
|
)'''.format(**self.locale.re_values)
|
||
|
|
||
|
# "06/07/06 - 08/09/06"
|
||
|
self.DATERNG1 = (r'{0}\s*{rangeseparator}\s*{0}'
|
||
|
.format(self.RE_RDATE, **self.locale.re_values))
|
||
|
|
||
|
# "march 31 - june 1st, 2006"
|
||
|
self.DATERNG2 = (r'{0}\s*{rangeseparator}\s*{0}'
|
||
|
.format(self.RE_RDATE3, **self.locale.re_values))
|
||
|
|
||
|
# "march 1rd -13th"
|
||
|
self.DATERNG3 = (r'{0}\s*{rangeseparator}\s*(\d\d?)\s*(rd|st|nd|th)?'
|
||
|
.format(self.RE_RDATE3, **self.locale.re_values))
|
||
|
|
||
|
# "4:00:55 pm - 5:90:44 am", '4p-5p'
|
||
|
self.TIMERNG1 = (r'{0}\s*{rangeseparator}\s*{0}'
|
||
|
.format(self.RE_RTIMEHMS2, **self.locale.re_values))
|
||
|
|
||
|
self.TIMERNG2 = (r'{0}\s*{rangeseparator}\s*{0}'
|
||
|
.format(self.RE_RTIMEHMS, **self.locale.re_values))
|
||
|
|
||
|
# "4-5pm "
|
||
|
self.TIMERNG3 = (r'\d\d?\s*{rangeseparator}\s*{0}'
|
||
|
.format(self.RE_RTIMEHMS2, **self.locale.re_values))
|
||
|
|
||
|
# "4:30-5pm "
|
||
|
self.TIMERNG4 = (r'{0}\s*{rangeseparator}\s*{1}'
|
||
|
.format(self.RE_RTIMEHMS, self.RE_RTIMEHMS2,
|
||
|
**self.locale.re_values))
|
||
|
|
||
|
self.re_option = re.IGNORECASE + re.VERBOSE
|
||
|
self.cre_source = {'CRE_SPECIAL': self.RE_SPECIAL,
|
||
|
'CRE_NUMBER': self.RE_NUMBER,
|
||
|
'CRE_UNITS': self.RE_UNITS,
|
||
|
'CRE_UNITS_ONLY': self.RE_UNITS_ONLY,
|
||
|
'CRE_QUNITS': self.RE_QUNITS,
|
||
|
'CRE_MODIFIER': self.RE_MODIFIER,
|
||
|
'CRE_TIMEHMS': self.RE_TIMEHMS,
|
||
|
'CRE_TIMEHMS2': self.RE_TIMEHMS2,
|
||
|
'CRE_DATE': self.RE_DATE,
|
||
|
'CRE_DATE2': self.RE_DATE2,
|
||
|
'CRE_DATE3': self.RE_DATE3,
|
||
|
'CRE_DATE4': self.RE_DATE4,
|
||
|
'CRE_MONTH': self.RE_MONTH,
|
||
|
'CRE_WEEKDAY': self.RE_WEEKDAY,
|
||
|
'CRE_DAY': self.RE_DAY,
|
||
|
'CRE_DAY2': self.RE_DAY2,
|
||
|
'CRE_TIME': self.RE_TIME,
|
||
|
'CRE_REMAINING': self.RE_REMAINING,
|
||
|
'CRE_RTIMEHMS': self.RE_RTIMEHMS,
|
||
|
'CRE_RTIMEHMS2': self.RE_RTIMEHMS2,
|
||
|
'CRE_RDATE': self.RE_RDATE,
|
||
|
'CRE_RDATE3': self.RE_RDATE3,
|
||
|
'CRE_TIMERNG1': self.TIMERNG1,
|
||
|
'CRE_TIMERNG2': self.TIMERNG2,
|
||
|
'CRE_TIMERNG3': self.TIMERNG3,
|
||
|
'CRE_TIMERNG4': self.TIMERNG4,
|
||
|
'CRE_DATERNG1': self.DATERNG1,
|
||
|
'CRE_DATERNG2': self.DATERNG2,
|
||
|
'CRE_DATERNG3': self.DATERNG3,
|
||
|
'CRE_NLP_PREFIX': self.RE_NLP_PREFIX}
|
||
|
self.cre_keys = set(self.cre_source.keys())
|
||
|
|
||
|
def __getattr__(self, name):
|
||
|
if name in self.cre_keys:
|
||
|
value = re.compile(self.cre_source[name], self.re_option)
|
||
|
setattr(self, name, value)
|
||
|
return value
|
||
|
elif name in self.locale.locale_keys:
|
||
|
return getattr(self.locale, name)
|
||
|
else:
|
||
|
raise AttributeError(name)
|
||
|
|
||
|
def daysInMonth(self, month, year):
|
||
|
"""
|
||
|
Take the given month (1-12) and a given year (4 digit) return
|
||
|
the number of days in the month adjusting for leap year as needed
|
||
|
"""
|
||
|
result = None
|
||
|
debug and log.debug('daysInMonth(%s, %s)', month, year)
|
||
|
if month > 0 and month <= 12:
|
||
|
result = self._DaysInMonthList[month - 1]
|
||
|
|
||
|
if month == 2:
|
||
|
if year in self._leapYears:
|
||
|
result += 1
|
||
|
else:
|
||
|
if calendar.isleap(year):
|
||
|
self._leapYears.append(year)
|
||
|
result += 1
|
||
|
|
||
|
return result
|
||
|
|
||
|
def getSource(self, sourceKey, sourceTime=None):
|
||
|
"""
|
||
|
GetReturn a date/time tuple based on the giving source key
|
||
|
and the corresponding key found in self.re_sources.
|
||
|
|
||
|
The current time is used as the default and any specified
|
||
|
item found in self.re_sources is inserted into the value
|
||
|
and the generated dictionary is returned.
|
||
|
"""
|
||
|
if sourceKey not in self.re_sources:
|
||
|
return None
|
||
|
|
||
|
if sourceTime is None:
|
||
|
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = time.localtime()
|
||
|
else:
|
||
|
(yr, mth, dy, hr, mn, sec, wd, yd, isdst) = sourceTime
|
||
|
|
||
|
defaults = {'yr': yr, 'mth': mth, 'dy': dy,
|
||
|
'hr': hr, 'mn': mn, 'sec': sec}
|
||
|
|
||
|
source = self.re_sources[sourceKey]
|
||
|
|
||
|
values = {}
|
||
|
|
||
|
for key, default in defaults.items():
|
||
|
values[key] = source.get(key, default)
|
||
|
|
||
|
return (values['yr'], values['mth'], values['dy'],
|
||
|
values['hr'], values['mn'], values['sec'],
|
||
|
wd, yd, isdst)
|