dbt-selly/dbt-env/lib/python3.8/site-packages/parsedatetime/__init__.py

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)