Source code for dateutil.parser

# -*- 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.
"""
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"]


# Some pointers:
#
# http://www.cl.cam.ac.uk/~mgk25/iso-time.html
# http://www.iso.ch/iso/en/prods-services/popstds/datesandtime.html
# http://www.w3.org/TR/NOTE-datetime
# http://ringmaster.arc.nasa.gov/tools/time_formats.html
# http://search.cpan.org/author/MUIR/Time-modules-2003.0211/lib/Time/ParseDate.pm
# http://stein.cshl.org/jade/distrib/docs/java.text.SimpleDateFormat.html


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):
        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:
            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:
                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':
                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 nextchar in numchars:
                    token += nextchar
                elif nextchar == '.':
                    token += nextchar
                    state = '0.'
                else:
                    self.charstack.append(nextchar)
                    break  # emit token
            elif state == 'a.':
                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 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__)


[docs]class parserinfo(object): # 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 in range(len(lst)): v = lst[i] if isinstance(v, tuple): for v in v: dct[v.lower()] = i else: dct[v.lower()] = i return dct
[docs] def jump(self, name): return name.lower() in self._jump
[docs] def weekday(self, name): if len(name) >= 3: try: return self._weekdays[name.lower()] except KeyError: pass return None
[docs] def month(self, name): if len(name) >= 3: try: return self._months[name.lower()]+1 except KeyError: pass return None
[docs] def hms(self, name): try: return self._hms[name.lower()] except KeyError: return None
[docs] def ampm(self, name): try: return self._ampm[name.lower()] except KeyError: return None
[docs] def pertain(self, name): return name.lower() in self._pertain
[docs] def utczone(self, name): return name.lower() in self._utczone
[docs] def tzoffset(self, name): if name in self._utczone: return 0 return self.TZOFFSET.get(name)
[docs] 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
[docs] 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): if not default: 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"] def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False, fuzzy_with_tokens=False): 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) # 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 (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: if value == 1 and res.hour < 12: res.hour += 12 elif value == 0 and res.hour == 12: res.hour = 0 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 if 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()
[docs]def parse(timestr, parserinfo=None, **kwargs): # 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): 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