| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205 |
- # -*- coding:iso-8859-1 -*-
- """
- This module offers a generic date/time string parser which is able to parse
- most known formats to represent a date and/or time.
- Additional resources about date/time string formats can be found below:
- - `A summary of the international standard date and time notation
- <http://www.cl.cam.ac.uk/~mgk25/iso-time.html>`_
- - `W3C Date and Time Formats <http://www.w3.org/TR/NOTE-datetime>`_
- - `Time Formats (Planetary Rings Node) <http://pds-rings.seti.org/tools/time_formats.html>`_
- - `CPAN ParseDate module
- <http://search.cpan.org/~muir/Time-modules-2013.0912/lib/Time/ParseDate.pm>`_
- - `Java SimpleDateFormat Class
- <https://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>`_
- """
- from __future__ import unicode_literals
- import datetime
- import string
- import time
- import collections
- from io import StringIO
- from six import text_type, binary_type, integer_types
- from . import relativedelta
- from . import tz
- __all__ = ["parse", "parserinfo"]
- class _timelex(object):
- def __init__(self, instream):
- if isinstance(instream, text_type):
- instream = StringIO(instream)
- self.instream = instream
- self.wordchars = ('abcdfeghijklmnopqrstuvwxyz'
- 'ABCDEFGHIJKLMNOPQRSTUVWXYZ_'
- 'ßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ'
- 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ')
- self.numchars = '0123456789'
- self.whitespace = ' \t\r\n'
- self.charstack = []
- self.tokenstack = []
- self.eof = False
- def get_token(self):
- """
- This function breaks the time string into lexical units (tokens), which
- can be parsed by the parser. Lexical units are demarcated by changes in
- the character set, so any continuous string of letters is considered one
- unit, any continuous string of numbers is considered one unit.
- The main complication arises from the fact that dots ('.') can be used
- both as separators (e.g. "Sep.20.2009") or decimal points (e.g.
- "4:30:21.447"). As such, it is necessary to read the full context of
- any dot-separated strings before breaking it into tokens; as such, this
- function maintains a "token stack", for when the ambiguous context
- demands that multiple tokens be parsed at once.
- """
- if self.tokenstack:
- return self.tokenstack.pop(0)
- seenletters = False
- token = None
- state = None
- wordchars = self.wordchars
- numchars = self.numchars
- whitespace = self.whitespace
- while not self.eof:
- # We only realize that we've reached the end of a token when we find
- # a character that's not part of the current token - since that
- # character may be part of the next token, it's stored in the
- # charstack.
- if self.charstack:
- nextchar = self.charstack.pop(0)
- else:
- nextchar = self.instream.read(1)
- while nextchar == '\x00':
- nextchar = self.instream.read(1)
- if not nextchar:
- self.eof = True
- break
- elif not state:
- # First character of the token - determines if we're starting
- # to parse a word, a number or something else.
- token = nextchar
- if nextchar in wordchars:
- state = 'a'
- elif nextchar in numchars:
- state = '0'
- elif nextchar in whitespace:
- token = ' '
- break # emit token
- else:
- break # emit token
- elif state == 'a':
- # If we've already started reading a word, we keep reading
- # letters until we find something that's not part of a word.
- seenletters = True
- if nextchar in wordchars:
- token += nextchar
- elif nextchar == '.':
- token += nextchar
- state = 'a.'
- else:
- self.charstack.append(nextchar)
- break # emit token
- elif state == '0':
- # If we've already started reading a number, we keep reading
- # numbers until we find something that doesn't fit.
- if nextchar in numchars:
- token += nextchar
- elif nextchar == '.':
- token += nextchar
- state = '0.'
- else:
- self.charstack.append(nextchar)
- break # emit token
- elif state == 'a.':
- # If we've seen some letters and a dot separator, continue
- # parsing, and the tokens will be broken up later.
- seenletters = True
- if nextchar == '.' or nextchar in wordchars:
- token += nextchar
- elif nextchar in numchars and token[-1] == '.':
- token += nextchar
- state = '0.'
- else:
- self.charstack.append(nextchar)
- break # emit token
- elif state == '0.':
- # If we've seen at least one dot separator, keep going, we'll
- # break up the tokens later.
- if nextchar == '.' or nextchar in numchars:
- token += nextchar
- elif nextchar in wordchars and token[-1] == '.':
- token += nextchar
- state = 'a.'
- else:
- self.charstack.append(nextchar)
- break # emit token
- if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or
- token[-1] == '.')):
- l = token.split('.')
- token = l[0]
- for tok in l[1:]:
- self.tokenstack.append('.')
- if tok:
- self.tokenstack.append(tok)
- return token
- def __iter__(self):
- return self
- def __next__(self):
- token = self.get_token()
- if token is None:
- raise StopIteration
- return token
- def next(self):
- return self.__next__() # Python 2.x support
- def split(cls, s):
- return list(cls(s))
- split = classmethod(split)
- class _resultbase(object):
- def __init__(self):
- for attr in self.__slots__:
- setattr(self, attr, None)
- def _repr(self, classname):
- l = []
- for attr in self.__slots__:
- value = getattr(self, attr)
- if value is not None:
- l.append("%s=%s" % (attr, repr(value)))
- return "%s(%s)" % (classname, ", ".join(l))
- def __repr__(self):
- return self._repr(self.__class__.__name__)
- class parserinfo(object):
- """
- Class which handles what inputs are accepted. Subclass this to customize the
- language and acceptable values for each parameter.
- :param dayfirst:
- Whether to interpret the first value in an ambiguous 3-integer date
- (e.g. 01/05/09) as the day (`True`) or month (`False`). If
- `yearfirst` is set to `True`, this distinguishes between YDM and
- YMD. Default is `False`.
- :param yearfirst:
- Whether to interpret the first value in an ambiguous 3-integer date
- (e.g. 01/05/09) as the year. If `True`, the first number is taken to
- be the year, otherwise the last number is taken to be the year.
- Default is `False`.
- """
- # m from a.m/p.m, t from ISO T separator
- JUMP = [" ", ".", ",", ";", "-", "/", "'",
- "at", "on", "and", "ad", "m", "t", "of",
- "st", "nd", "rd", "th"]
- WEEKDAYS = [("Mon", "Monday"),
- ("Tue", "Tuesday"),
- ("Wed", "Wednesday"),
- ("Thu", "Thursday"),
- ("Fri", "Friday"),
- ("Sat", "Saturday"),
- ("Sun", "Sunday")]
- MONTHS = [("Jan", "January"),
- ("Feb", "February"),
- ("Mar", "March"),
- ("Apr", "April"),
- ("May", "May"),
- ("Jun", "June"),
- ("Jul", "July"),
- ("Aug", "August"),
- ("Sep", "Sept", "September"),
- ("Oct", "October"),
- ("Nov", "November"),
- ("Dec", "December")]
- HMS = [("h", "hour", "hours"),
- ("m", "minute", "minutes"),
- ("s", "second", "seconds")]
- AMPM = [("am", "a"),
- ("pm", "p")]
- UTCZONE = ["UTC", "GMT", "Z"]
- PERTAIN = ["of"]
- TZOFFSET = {}
- def __init__(self, dayfirst=False, yearfirst=False):
- self._jump = self._convert(self.JUMP)
- self._weekdays = self._convert(self.WEEKDAYS)
- self._months = self._convert(self.MONTHS)
- self._hms = self._convert(self.HMS)
- self._ampm = self._convert(self.AMPM)
- self._utczone = self._convert(self.UTCZONE)
- self._pertain = self._convert(self.PERTAIN)
- self.dayfirst = dayfirst
- self.yearfirst = yearfirst
- self._year = time.localtime().tm_year
- self._century = self._year // 100*100
- def _convert(self, lst):
- dct = {}
- for i, v in enumerate(lst):
- if isinstance(v, tuple):
- for v in v:
- dct[v.lower()] = i
- else:
- dct[v.lower()] = i
- return dct
- def jump(self, name):
- return name.lower() in self._jump
- def weekday(self, name):
- if len(name) >= 3:
- try:
- return self._weekdays[name.lower()]
- except KeyError:
- pass
- return None
- def month(self, name):
- if len(name) >= 3:
- try:
- return self._months[name.lower()]+1
- except KeyError:
- pass
- return None
- def hms(self, name):
- try:
- return self._hms[name.lower()]
- except KeyError:
- return None
- def ampm(self, name):
- try:
- return self._ampm[name.lower()]
- except KeyError:
- return None
- def pertain(self, name):
- return name.lower() in self._pertain
- def utczone(self, name):
- return name.lower() in self._utczone
- def tzoffset(self, name):
- if name in self._utczone:
- return 0
- return self.TZOFFSET.get(name)
- def convertyear(self, year):
- if year < 100:
- year += self._century
- if abs(year-self._year) >= 50:
- if year < self._year:
- year += 100
- else:
- year -= 100
- return year
- def validate(self, res):
- # move to info
- if res.year is not None:
- res.year = self.convertyear(res.year)
- if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z':
- res.tzname = "UTC"
- res.tzoffset = 0
- elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname):
- res.tzoffset = 0
- return True
- class parser(object):
- def __init__(self, info=None):
- self.info = info or parserinfo()
- def parse(self, timestr, default=None, ignoretz=False, tzinfos=None,
- **kwargs):
- """
- Parse the date/time string into a datetime object.
- :param timestr:
- Any date/time string using the supported formats.
- :param default:
- The default datetime object, if this is a datetime object and not
- `None`, elements specified in `timestr` replace elements in the
- default object.
- :param ignoretz:
- Whether or not to ignore the time zone.
- :param tzinfos:
- A time zone, to be applied to the date, if `ignoretz` is `True`.
- This can be either a subclass of `tzinfo`, a time zone string or an
- integer offset.
- :param **kwargs:
- Keyword arguments as passed to `_parse()`.
- :return:
- Returns a `datetime.datetime` object or, if the `fuzzy_with_tokens`
- option is `True`, returns a tuple, the first element being a
- `datetime.datetime` object, the second a tuple containing the
- fuzzy tokens.
- :raises ValueError:
- Raised for invalid or unknown string format, if the provided
- `tzinfo` is not in a valid format, or if an invalid date would
- be created.
- :raises OverFlowError:
- Raised if the parsed date exceeds the largest valid C integer on
- your system.
- """
- default_specified = default is not None
- if not default_specified:
- default = datetime.datetime.now().replace(hour=0, minute=0,
- second=0, microsecond=0)
- if kwargs.get('fuzzy_with_tokens', False):
- res, skipped_tokens = self._parse(timestr, **kwargs)
- else:
- res = self._parse(timestr, **kwargs)
- if res is None:
- raise ValueError("Unknown string format")
- repl = {}
- for attr in ["year", "month", "day", "hour",
- "minute", "second", "microsecond"]:
- value = getattr(res, attr)
- if value is not None:
- repl[attr] = value
- ret = default.replace(**repl)
- if res.weekday is not None and not res.day:
- ret = ret+relativedelta.relativedelta(weekday=res.weekday)
- if not ignoretz:
- if (isinstance(tzinfos, collections.Callable) or
- tzinfos and res.tzname in tzinfos):
- if isinstance(tzinfos, collections.Callable):
- tzdata = tzinfos(res.tzname, res.tzoffset)
- else:
- tzdata = tzinfos.get(res.tzname)
- if isinstance(tzdata, datetime.tzinfo):
- tzinfo = tzdata
- elif isinstance(tzdata, text_type):
- tzinfo = tz.tzstr(tzdata)
- elif isinstance(tzdata, integer_types):
- tzinfo = tz.tzoffset(res.tzname, tzdata)
- else:
- raise ValueError("Offset must be tzinfo subclass, "
- "tz string, or int offset.")
- ret = ret.replace(tzinfo=tzinfo)
- elif res.tzname and res.tzname in time.tzname:
- ret = ret.replace(tzinfo=tz.tzlocal())
- elif res.tzoffset == 0:
- ret = ret.replace(tzinfo=tz.tzutc())
- elif res.tzoffset:
- ret = ret.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset))
- if kwargs.get('fuzzy_with_tokens', False):
- return ret, skipped_tokens
- else:
- return ret
- class _result(_resultbase):
- __slots__ = ["year", "month", "day", "weekday",
- "hour", "minute", "second", "microsecond",
- "tzname", "tzoffset", "ampm"]
- def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False,
- fuzzy_with_tokens=False):
- """
- Private method which performs the heavy lifting of parsing, called from
- `parse()`, which passes on its `kwargs` to this function.
- :param timestr:
- The string to parse.
- :param dayfirst:
- Whether to interpret the first value in an ambiguous 3-integer date
- (e.g. 01/05/09) as the day (`True`) or month (`False`). If
- `yearfirst` is set to `True`, this distinguishes between YDM and
- YMD. If set to `None`, this value is retrieved from the current
- `parserinfo` object (which itself defaults to `False`).
- :param yearfirst:
- Whether to interpret the first value in an ambiguous 3-integer date
- (e.g. 01/05/09) as the year. If `True`, the first number is taken to
- be the year, otherwise the last number is taken to be the year. If
- this is set to `None`, the value is retrieved from the current
- `parserinfo` object (which itself defaults to `False`).
- :param fuzzy:
- Whether to allow fuzzy parsing, allowing for string like "Today is
- January 1, 2047 at 8:21:00AM".
- :param fuzzy_with_tokens:
- If `True`, `fuzzy` is automatically set to True, and the parser will
- return a tuple where the first element is the parsed
- `datetime.datetime` datetimestamp and the second element is a tuple
- containing the portions of the string which were ignored, e.g.
- "Today is January 1, 2047 at 8:21:00AM" should return
- `(datetime.datetime(2011, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))`
- """
- if fuzzy_with_tokens:
- fuzzy = True
- info = self.info
- if dayfirst is None:
- dayfirst = info.dayfirst
- if yearfirst is None:
- yearfirst = info.yearfirst
- res = self._result()
- l = _timelex.split(timestr) # Splits the timestr into tokens
- # keep up with the last token skipped so we can recombine
- # consecutively skipped tokens (-2 for when i begins at 0).
- last_skipped_token_i = -2
- skipped_tokens = list()
- try:
- # year/month/day list
- ymd = []
- # Index of the month string in ymd
- mstridx = -1
- len_l = len(l)
- i = 0
- while i < len_l:
- # Check if it's a number
- try:
- value_repr = l[i]
- value = float(value_repr)
- except ValueError:
- value = None
- if value is not None:
- # Token is a number
- len_li = len(l[i])
- i += 1
- if (len(ymd) == 3 and len_li in (2, 4)
- and res.hour is None and (i >= len_l or (l[i] != ':' and
- info.hms(l[i]) is None))):
- # 19990101T23[59]
- s = l[i-1]
- res.hour = int(s[:2])
- if len_li == 4:
- res.minute = int(s[2:])
- elif len_li == 6 or (len_li > 6 and l[i-1].find('.') == 6):
- # YYMMDD or HHMMSS[.ss]
- s = l[i-1]
- if not ymd and l[i-1].find('.') == -1:
- ymd.append(info.convertyear(int(s[:2])))
- ymd.append(int(s[2:4]))
- ymd.append(int(s[4:]))
- else:
- # 19990101T235959[.59]
- res.hour = int(s[:2])
- res.minute = int(s[2:4])
- res.second, res.microsecond = _parsems(s[4:])
- elif len_li == 8:
- # YYYYMMDD
- s = l[i-1]
- ymd.append(int(s[:4]))
- ymd.append(int(s[4:6]))
- ymd.append(int(s[6:]))
- elif len_li in (12, 14):
- # YYYYMMDDhhmm[ss]
- s = l[i-1]
- ymd.append(int(s[:4]))
- ymd.append(int(s[4:6]))
- ymd.append(int(s[6:8]))
- res.hour = int(s[8:10])
- res.minute = int(s[10:12])
- if len_li == 14:
- res.second = int(s[12:])
- elif ((i < len_l and info.hms(l[i]) is not None) or
- (i+1 < len_l and l[i] == ' ' and
- info.hms(l[i+1]) is not None)):
- # HH[ ]h or MM[ ]m or SS[.ss][ ]s
- if l[i] == ' ':
- i += 1
- idx = info.hms(l[i])
- while True:
- if idx == 0:
- res.hour = int(value)
- if value % 1:
- res.minute = int(60*(value % 1))
- elif idx == 1:
- res.minute = int(value)
- if value % 1:
- res.second = int(60*(value % 1))
- elif idx == 2:
- res.second, res.microsecond = \
- _parsems(value_repr)
- i += 1
- if i >= len_l or idx == 2:
- break
- # 12h00
- try:
- value_repr = l[i]
- value = float(value_repr)
- except ValueError:
- break
- else:
- i += 1
- idx += 1
- if i < len_l:
- newidx = info.hms(l[i])
- if newidx is not None:
- idx = newidx
- elif (i == len_l and l[i-2] == ' ' and
- info.hms(l[i-3]) is not None):
- # X h MM or X m SS
- idx = info.hms(l[i-3]) + 1
- if idx == 1:
- res.minute = int(value)
- if value % 1:
- res.second = int(60*(value % 1))
- elif idx == 2:
- res.second, res.microsecond = \
- _parsems(value_repr)
- i += 1
- elif i+1 < len_l and l[i] == ':':
- # HH:MM[:SS[.ss]]
- res.hour = int(value)
- i += 1
- value = float(l[i])
- res.minute = int(value)
- if value % 1:
- res.second = int(60*(value % 1))
- i += 1
- if i < len_l and l[i] == ':':
- res.second, res.microsecond = _parsems(l[i+1])
- i += 2
- elif i < len_l and l[i] in ('-', '/', '.'):
- sep = l[i]
- ymd.append(int(value))
- i += 1
- if i < len_l and not info.jump(l[i]):
- try:
- # 01-01[-01]
- ymd.append(int(l[i]))
- except ValueError:
- # 01-Jan[-01]
- value = info.month(l[i])
- if value is not None:
- ymd.append(value)
- assert mstridx == -1
- mstridx = len(ymd)-1
- else:
- return None
- i += 1
- if i < len_l and l[i] == sep:
- # We have three members
- i += 1
- value = info.month(l[i])
- if value is not None:
- ymd.append(value)
- mstridx = len(ymd)-1
- assert mstridx == -1
- else:
- ymd.append(int(l[i]))
- i += 1
- elif i >= len_l or info.jump(l[i]):
- if i+1 < len_l and info.ampm(l[i+1]) is not None:
- # 12 am
- res.hour = int(value)
- if res.hour < 12 and info.ampm(l[i+1]) == 1:
- res.hour += 12
- elif res.hour == 12 and info.ampm(l[i+1]) == 0:
- res.hour = 0
- i += 1
- else:
- # Year, month or day
- ymd.append(int(value))
- i += 1
- elif info.ampm(l[i]) is not None:
- # 12am
- res.hour = int(value)
- if res.hour < 12 and info.ampm(l[i]) == 1:
- res.hour += 12
- elif res.hour == 12 and info.ampm(l[i]) == 0:
- res.hour = 0
- i += 1
- elif not fuzzy:
- return None
- else:
- i += 1
- continue
- # Check weekday
- value = info.weekday(l[i])
- if value is not None:
- res.weekday = value
- i += 1
- continue
- # Check month name
- value = info.month(l[i])
- if value is not None:
- ymd.append(value)
- assert mstridx == -1
- mstridx = len(ymd)-1
- i += 1
- if i < len_l:
- if l[i] in ('-', '/'):
- # Jan-01[-99]
- sep = l[i]
- i += 1
- ymd.append(int(l[i]))
- i += 1
- if i < len_l and l[i] == sep:
- # Jan-01-99
- i += 1
- ymd.append(int(l[i]))
- i += 1
- elif (i+3 < len_l and l[i] == l[i+2] == ' '
- and info.pertain(l[i+1])):
- # Jan of 01
- # In this case, 01 is clearly year
- try:
- value = int(l[i+3])
- except ValueError:
- # Wrong guess
- pass
- else:
- # Convert it here to become unambiguous
- ymd.append(info.convertyear(value))
- i += 4
- continue
- # Check am/pm
- value = info.ampm(l[i])
- if value is not None:
- # For fuzzy parsing, 'a' or 'am' (both valid English words)
- # may erroneously trigger the AM/PM flag. Deal with that
- # here.
- val_is_ampm = True
- # If there's already an AM/PM flag, this one isn't one.
- if fuzzy and res.ampm is not None:
- val_is_ampm = False
- # If AM/PM is found and hour is not, raise a ValueError
- if res.hour is None:
- if fuzzy:
- val_is_ampm = False
- else:
- raise ValueError('No hour specified with ' +
- 'AM or PM flag.')
- elif not 0 <= res.hour <= 12:
- # If AM/PM is found, it's a 12 hour clock, so raise
- # an error for invalid range
- if fuzzy:
- val_is_ampm = False
- else:
- raise ValueError('Invalid hour specified for ' +
- '12-hour clock.')
- if val_is_ampm:
- if value == 1 and res.hour < 12:
- res.hour += 12
- elif value == 0 and res.hour == 12:
- res.hour = 0
- res.ampm = value
- i += 1
- continue
- # Check for a timezone name
- if (res.hour is not None and len(l[i]) <= 5 and
- res.tzname is None and res.tzoffset is None and
- not [x for x in l[i] if x not in
- string.ascii_uppercase]):
- res.tzname = l[i]
- res.tzoffset = info.tzoffset(res.tzname)
- i += 1
- # Check for something like GMT+3, or BRST+3. Notice
- # that it doesn't mean "I am 3 hours after GMT", but
- # "my time +3 is GMT". If found, we reverse the
- # logic so that timezone parsing code will get it
- # right.
- if i < len_l and l[i] in ('+', '-'):
- l[i] = ('+', '-')[l[i] == '+']
- res.tzoffset = None
- if info.utczone(res.tzname):
- # With something like GMT+3, the timezone
- # is *not* GMT.
- res.tzname = None
- continue
- # Check for a numbered timezone
- if res.hour is not None and l[i] in ('+', '-'):
- signal = (-1, 1)[l[i] == '+']
- i += 1
- len_li = len(l[i])
- if len_li == 4:
- # -0300
- res.tzoffset = int(l[i][:2])*3600+int(l[i][2:])*60
- elif i+1 < len_l and l[i+1] == ':':
- # -03:00
- res.tzoffset = int(l[i])*3600+int(l[i+2])*60
- i += 2
- elif len_li <= 2:
- # -[0]3
- res.tzoffset = int(l[i][:2])*3600
- else:
- return None
- i += 1
- res.tzoffset *= signal
- # Look for a timezone name between parenthesis
- if (i+3 < len_l and
- info.jump(l[i]) and l[i+1] == '(' and l[i+3] == ')' and
- 3 <= len(l[i+2]) <= 5 and
- not [x for x in l[i+2]
- if x not in string.ascii_uppercase]):
- # -0300 (BRST)
- res.tzname = l[i+2]
- i += 4
- continue
- # Check jumps
- if not (info.jump(l[i]) or fuzzy):
- return None
- if last_skipped_token_i == i - 1:
- # recombine the tokens
- skipped_tokens[-1] += l[i]
- else:
- # just append
- skipped_tokens.append(l[i])
- last_skipped_token_i = i
- i += 1
- # Process year/month/day
- len_ymd = len(ymd)
- if len_ymd > 3:
- # More than three members!?
- return None
- elif len_ymd == 1 or (mstridx != -1 and len_ymd == 2):
- # One member, or two members with a month string
- if mstridx != -1:
- res.month = ymd[mstridx]
- del ymd[mstridx]
- if len_ymd > 1 or mstridx == -1:
- if ymd[0] > 31:
- res.year = ymd[0]
- else:
- res.day = ymd[0]
- elif len_ymd == 2:
- # Two members with numbers
- if ymd[0] > 31:
- # 99-01
- res.year, res.month = ymd
- elif ymd[1] > 31:
- # 01-99
- res.month, res.year = ymd
- elif dayfirst and ymd[1] <= 12:
- # 13-01
- res.day, res.month = ymd
- else:
- # 01-13
- res.month, res.day = ymd
- elif len_ymd == 3:
- # Three members
- if mstridx == 0:
- res.month, res.day, res.year = ymd
- elif mstridx == 1:
- if ymd[0] > 31 or (yearfirst and ymd[2] <= 31):
- # 99-Jan-01
- res.year, res.month, res.day = ymd
- else:
- # 01-Jan-01
- # Give precendence to day-first, since
- # two-digit years is usually hand-written.
- res.day, res.month, res.year = ymd
- elif mstridx == 2:
- # WTF!?
- if ymd[1] > 31:
- # 01-99-Jan
- res.day, res.year, res.month = ymd
- else:
- # 99-01-Jan
- res.year, res.day, res.month = ymd
- else:
- if ymd[0] > 31 or \
- (yearfirst and ymd[1] <= 12 and ymd[2] <= 31):
- # 99-01-01
- res.year, res.month, res.day = ymd
- elif ymd[0] > 12 or (dayfirst and ymd[1] <= 12):
- # 13-01-01
- res.day, res.month, res.year = ymd
- else:
- # 01-13-01
- res.month, res.day, res.year = ymd
- except (IndexError, ValueError, AssertionError):
- return None
- if not info.validate(res):
- return None
- if fuzzy_with_tokens:
- return res, tuple(skipped_tokens)
- else:
- return res
- DEFAULTPARSER = parser()
- def parse(timestr, parserinfo=None, **kwargs):
- """
- Parse a string in one of the supported formats, using the `parserinfo`
- parameters.
- :param timestr:
- A string containing a date/time stamp.
- :param parserinfo:
- A :class:`parserinfo` object containing parameters for the parser.
- If `None`, the default arguments to the `parserinfo` constructor are
- used.
- The `**kwargs` parameter takes the following keyword arguments:
- :param default:
- The default datetime object, if this is a datetime object and not
- `None`, elements specified in `timestr` replace elements in the
- default object.
- :param ignoretz:
- Whether or not to ignore the time zone (boolean).
- :param tzinfos:
- A time zone, to be applied to the date, if `ignoretz` is `True`.
- This can be either a subclass of `tzinfo`, a time zone string or an
- integer offset.
- :param dayfirst:
- Whether to interpret the first value in an ambiguous 3-integer date
- (e.g. 01/05/09) as the day (`True`) or month (`False`). If
- `yearfirst` is set to `True`, this distinguishes between YDM and
- YMD. If set to `None`, this value is retrieved from the current
- :class:`parserinfo` object (which itself defaults to `False`).
- :param yearfirst:
- Whether to interpret the first value in an ambiguous 3-integer date
- (e.g. 01/05/09) as the year. If `True`, the first number is taken to
- be the year, otherwise the last number is taken to be the year. If
- this is set to `None`, the value is retrieved from the current
- :class:`parserinfo` object (which itself defaults to `False`).
- :param fuzzy:
- Whether to allow fuzzy parsing, allowing for string like "Today is
- January 1, 2047 at 8:21:00AM".
- :param fuzzy_with_tokens:
- If `True`, `fuzzy` is automatically set to True, and the parser will
- return a tuple where the first element is the parsed
- `datetime.datetime` datetimestamp and the second element is a tuple
- containing the portions of the string which were ignored, e.g.
- "Today is January 1, 2047 at 8:21:00AM" should return
- `(datetime.datetime(2011, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))`
- """
- # Python 2.x support: datetimes return their string presentation as
- # bytes in 2.x and unicode in 3.x, so it's reasonable to expect that
- # the parser will get both kinds. Internally we use unicode only.
- if isinstance(timestr, binary_type):
- timestr = timestr.decode()
- if parserinfo:
- return parser(parserinfo).parse(timestr, **kwargs)
- else:
- return DEFAULTPARSER.parse(timestr, **kwargs)
- class _tzparser(object):
- class _result(_resultbase):
- __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset",
- "start", "end"]
- class _attr(_resultbase):
- __slots__ = ["month", "week", "weekday",
- "yday", "jyday", "day", "time"]
- def __repr__(self):
- return self._repr("")
- def __init__(self):
- _resultbase.__init__(self)
- self.start = self._attr()
- self.end = self._attr()
- def parse(self, tzstr):
- # Python 2.x compatibility: tzstr should be converted to unicode before
- # being passed to _timelex.
- if isinstance(tzstr, binary_type):
- tzstr = tzstr.decode()
- res = self._result()
- l = _timelex.split(tzstr)
- try:
- len_l = len(l)
- i = 0
- while i < len_l:
- # BRST+3[BRDT[+2]]
- j = i
- while j < len_l and not [x for x in l[j]
- if x in "0123456789:,-+"]:
- j += 1
- if j != i:
- if not res.stdabbr:
- offattr = "stdoffset"
- res.stdabbr = "".join(l[i:j])
- else:
- offattr = "dstoffset"
- res.dstabbr = "".join(l[i:j])
- i = j
- if (i < len_l and (l[i] in ('+', '-') or l[i][0] in
- "0123456789")):
- if l[i] in ('+', '-'):
- # Yes, that's right. See the TZ variable
- # documentation.
- signal = (1, -1)[l[i] == '+']
- i += 1
- else:
- signal = -1
- len_li = len(l[i])
- if len_li == 4:
- # -0300
- setattr(res, offattr, (int(l[i][:2])*3600 +
- int(l[i][2:])*60)*signal)
- elif i+1 < len_l and l[i+1] == ':':
- # -03:00
- setattr(res, offattr,
- (int(l[i])*3600+int(l[i+2])*60)*signal)
- i += 2
- elif len_li <= 2:
- # -[0]3
- setattr(res, offattr,
- int(l[i][:2])*3600*signal)
- else:
- return None
- i += 1
- if res.dstabbr:
- break
- else:
- break
- if i < len_l:
- for j in range(i, len_l):
- if l[j] == ';':
- l[j] = ','
- assert l[i] == ','
- i += 1
- if i >= len_l:
- pass
- elif (8 <= l.count(',') <= 9 and
- not [y for x in l[i:] if x != ','
- for y in x if y not in "0123456789"]):
- # GMT0BST,3,0,30,3600,10,0,26,7200[,3600]
- for x in (res.start, res.end):
- x.month = int(l[i])
- i += 2
- if l[i] == '-':
- value = int(l[i+1])*-1
- i += 1
- else:
- value = int(l[i])
- i += 2
- if value:
- x.week = value
- x.weekday = (int(l[i])-1) % 7
- else:
- x.day = int(l[i])
- i += 2
- x.time = int(l[i])
- i += 2
- if i < len_l:
- if l[i] in ('-', '+'):
- signal = (-1, 1)[l[i] == "+"]
- i += 1
- else:
- signal = 1
- res.dstoffset = (res.stdoffset+int(l[i]))*signal
- elif (l.count(',') == 2 and l[i:].count('/') <= 2 and
- not [y for x in l[i:] if x not in (',', '/', 'J', 'M',
- '.', '-', ':')
- for y in x if y not in "0123456789"]):
- for x in (res.start, res.end):
- if l[i] == 'J':
- # non-leap year day (1 based)
- i += 1
- x.jyday = int(l[i])
- elif l[i] == 'M':
- # month[-.]week[-.]weekday
- i += 1
- x.month = int(l[i])
- i += 1
- assert l[i] in ('-', '.')
- i += 1
- x.week = int(l[i])
- if x.week == 5:
- x.week = -1
- i += 1
- assert l[i] in ('-', '.')
- i += 1
- x.weekday = (int(l[i])-1) % 7
- else:
- # year day (zero based)
- x.yday = int(l[i])+1
- i += 1
- if i < len_l and l[i] == '/':
- i += 1
- # start time
- len_li = len(l[i])
- if len_li == 4:
- # -0300
- x.time = (int(l[i][:2])*3600+int(l[i][2:])*60)
- elif i+1 < len_l and l[i+1] == ':':
- # -03:00
- x.time = int(l[i])*3600+int(l[i+2])*60
- i += 2
- if i+1 < len_l and l[i+1] == ':':
- i += 2
- x.time += int(l[i])
- elif len_li <= 2:
- # -[0]3
- x.time = (int(l[i][:2])*3600)
- else:
- return None
- i += 1
- assert i == len_l or l[i] == ','
- i += 1
- assert i >= len_l
- except (IndexError, ValueError, AssertionError):
- return None
- return res
- DEFAULTTZPARSER = _tzparser()
- def _parsetz(tzstr):
- return DEFAULTTZPARSER.parse(tzstr)
- def _parsems(value):
- """Parse a I[.F] seconds value into (seconds, microseconds)."""
- if "." not in value:
- return int(value), 0
- else:
- i, f = value.split(".")
- return int(i), int(f.ljust(6, "0")[:6])
- # vim:ts=4:sw=4:et
|