1"""
2This module offers a generic date/time string parser which is able to parse
3most known formats to represent a date and/or time.
4
5This module attempts to be forgiving with regards to unlikely input formats,
6returning a datetime object even for dates which are ambiguous. If an element
7of a date/time stamp is omitted, the following rules are applied:
8
9- If AM or PM is left unspecified, a 24-hour clock is assumed, however, an hour
10 on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is
11 specified.
12- If a time zone is omitted, a timezone-naive datetime is returned.
13
14If any other elements are missing, they are taken from the
15:class:`datetime.datetime` object passed to the parameter ``default``. If this
16results in a day number exceeding the valid number of days per month, the
17value falls back to the end of the month.
18
19Additional resources about date/time string formats can be found below:
20
21- `A summary of the international standard date and time notation
22 <https://www.cl.cam.ac.uk/~mgk25/iso-time.html>`_
23- `W3C Date and Time Formats <https://www.w3.org/TR/NOTE-datetime>`_
24- `Time Formats (Planetary Rings Node) <https://pds-rings.seti.org:443/tools/time_formats.html>`_
25- `CPAN ParseDate module
26 <https://metacpan.org/pod/release/MUIR/Time-modules-2013.0912/lib/Time/ParseDate.pm>`_
27- `Java SimpleDateFormat Class
28 <https://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>`_
29"""
30
31import datetime
32import re
33import string
34import time
35import warnings
36
37from calendar import monthrange
38from io import StringIO
39
40
41from decimal import Decimal
42
43from warnings import warn
44
45from .. import relativedelta
46from .. import tz
47
48__all__ = ["parse", "parserinfo", "ParserError"]
49
50
51# TODO: pandas.core.tools.datetimes imports this explicitly. Might be worth
52# making public and/or figuring out if there is something we can
53# take off their plate.
54class _timelex:
55 # Fractional seconds are sometimes split by a comma
56 _split_decimal = re.compile("([.,])")
57
58 def __init__(self, instream):
59 if isinstance(instream, (bytes, bytearray)):
60 instream = instream.decode()
61
62 if isinstance(instream, str):
63 instream = StringIO(instream)
64 elif getattr(instream, 'read', None) is None:
65 raise TypeError('Parser must be a string or character stream, not '
66 '{itype}'.format(itype=instream.__class__.__name__))
67
68 self.instream = instream
69 self.charstack = []
70 self.tokenstack = []
71 self.eof = False
72
73 def get_token(self):
74 """
75 This function breaks the time string into lexical units (tokens), which
76 can be parsed by the parser. Lexical units are demarcated by changes in
77 the character set, so any continuous string of letters is considered
78 one unit, any continuous string of numbers is considered one unit.
79
80 The main complication arises from the fact that dots ('.') can be used
81 both as separators (e.g. "Sep.20.2009") or decimal points (e.g.
82 "4:30:21.447"). As such, it is necessary to read the full context of
83 any dot-separated strings before breaking it into tokens; as such, this
84 function maintains a "token stack", for when the ambiguous context
85 demands that multiple tokens be parsed at once.
86 """
87 if self.tokenstack:
88 return self.tokenstack.pop(0)
89
90 seenletters = False
91 token = None
92 state = None
93
94 while not self.eof:
95 # We only realize that we've reached the end of a token when we
96 # find a character that's not part of the current token - since
97 # that character may be part of the next token, it's stored in the
98 # charstack.
99 if self.charstack:
100 nextchar = self.charstack.pop(0)
101 else:
102 nextchar = self.instream.read(1)
103 while nextchar == '\x00':
104 nextchar = self.instream.read(1)
105
106 if not nextchar:
107 self.eof = True
108 break
109 elif not state:
110 # First character of the token - determines if we're starting
111 # to parse a word, a number or something else.
112 token = nextchar
113 if self.isword(nextchar):
114 state = 'a'
115 elif self.isnum(nextchar):
116 state = '0'
117 elif self.isspace(nextchar):
118 token = ' '
119 break # emit token
120 else:
121 break # emit token
122 elif state == 'a':
123 # If we've already started reading a word, we keep reading
124 # letters until we find something that's not part of a word.
125 seenletters = True
126 if self.isword(nextchar):
127 token += nextchar
128 elif nextchar == '.':
129 token += nextchar
130 state = 'a.'
131 else:
132 self.charstack.append(nextchar)
133 break # emit token
134 elif state == '0':
135 # If we've already started reading a number, we keep reading
136 # numbers until we find something that doesn't fit.
137 if self.isnum(nextchar):
138 token += nextchar
139 elif nextchar == '.' or (nextchar == ',' and len(token) >= 2):
140 token += nextchar
141 state = '0.'
142 else:
143 self.charstack.append(nextchar)
144 break # emit token
145 elif state == 'a.':
146 # If we've seen some letters and a dot separator, continue
147 # parsing, and the tokens will be broken up later.
148 seenletters = True
149 if nextchar == '.' or self.isword(nextchar):
150 token += nextchar
151 elif self.isnum(nextchar) and token[-1] == '.':
152 token += nextchar
153 state = '0.'
154 else:
155 self.charstack.append(nextchar)
156 break # emit token
157 elif state == '0.':
158 # If we've seen at least one dot separator, keep going, we'll
159 # break up the tokens later.
160 if nextchar == '.' or self.isnum(nextchar):
161 token += nextchar
162 elif self.isword(nextchar) and token[-1] == '.':
163 token += nextchar
164 state = 'a.'
165 else:
166 self.charstack.append(nextchar)
167 break # emit token
168
169 if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or
170 token[-1] in '.,')):
171 l = self._split_decimal.split(token)
172 token = l[0]
173 for tok in l[1:]:
174 if tok:
175 self.tokenstack.append(tok)
176
177 if state == '0.' and token.count('.') == 0:
178 token = token.replace(',', '.')
179
180 return token
181
182 def __iter__(self):
183 return self
184
185 def __next__(self):
186 token = self.get_token()
187 if token is None:
188 raise StopIteration
189
190 return token
191
192
193 @classmethod
194 def split(cls, s):
195 return list(cls(s))
196
197 @classmethod
198 def isword(cls, nextchar):
199 """ Whether or not the next character is part of a word """
200 return nextchar.isalpha()
201
202 @classmethod
203 def isnum(cls, nextchar):
204 """ Whether the next character is part of a number """
205 return nextchar.isdigit()
206
207 @classmethod
208 def isspace(cls, nextchar):
209 """ Whether the next character is whitespace """
210 return nextchar.isspace()
211
212
213class _resultbase:
214
215 def __init__(self):
216 for attr in self.__slots__:
217 setattr(self, attr, None)
218
219 def _repr(self, classname):
220 l = []
221 for attr in self.__slots__:
222 value = getattr(self, attr)
223 if value is not None:
224 l.append("%s=%s" % (attr, repr(value)))
225 return "%s(%s)" % (classname, ", ".join(l))
226
227 def __len__(self):
228 return (sum(getattr(self, attr) is not None
229 for attr in self.__slots__))
230
231 def __repr__(self):
232 return self._repr(self.__class__.__name__)
233
234
235class parserinfo:
236 """
237 Class which handles what inputs are accepted. Subclass this to customize
238 the language and acceptable values for each parameter.
239
240 :param dayfirst:
241 Whether to interpret the first value in an ambiguous 3-integer date
242 (e.g. 01/05/09) as the day (``True``) or month (``False``). If
243 ``yearfirst`` is set to ``True``, this distinguishes between YDM
244 and YMD. Default is ``False``.
245
246 :param yearfirst:
247 Whether to interpret the first value in an ambiguous 3-integer date
248 (e.g. 01/05/09) as the year. If ``True``, the first number is taken
249 to be the year, otherwise the last number is taken to be the year.
250 Default is ``False``.
251 """
252
253 # m from a.m/p.m, t from ISO T separator
254 JUMP = [" ", ".", ",", ";", "-", "/", "'",
255 "at", "on", "and", "ad", "m", "t", "of",
256 "st", "nd", "rd", "th"]
257
258 WEEKDAYS = [("Mon", "Monday"),
259 ("Tue", "Tuesday"), # TODO: "Tues"
260 ("Wed", "Wednesday"),
261 ("Thu", "Thursday"), # TODO: "Thurs"
262 ("Fri", "Friday"),
263 ("Sat", "Saturday"),
264 ("Sun", "Sunday")]
265 MONTHS = [("Jan", "January"),
266 ("Feb", "February"), # TODO: "Febr"
267 ("Mar", "March"),
268 ("Apr", "April"),
269 ("May", "May"),
270 ("Jun", "June"),
271 ("Jul", "July"),
272 ("Aug", "August"),
273 ("Sep", "Sept", "September"),
274 ("Oct", "October"),
275 ("Nov", "November"),
276 ("Dec", "December")]
277 HMS = [("h", "hour", "hours"),
278 ("m", "minute", "minutes"),
279 ("s", "second", "seconds")]
280 AMPM = [("am", "a"),
281 ("pm", "p")]
282 UTCZONE = ["UTC", "GMT", "Z", "z"]
283 PERTAIN = ["of"]
284 TZOFFSET = {}
285 # TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate",
286 # "Anno Domini", "Year of Our Lord"]
287
288 def __init__(self, dayfirst=False, yearfirst=False):
289 self._jump = self._convert(self.JUMP)
290 self._weekdays = self._convert(self.WEEKDAYS)
291 self._months = self._convert(self.MONTHS)
292 self._hms = self._convert(self.HMS)
293 self._ampm = self._convert(self.AMPM)
294 self._utczone = self._convert(self.UTCZONE)
295 self._pertain = self._convert(self.PERTAIN)
296
297 self.dayfirst = dayfirst
298 self.yearfirst = yearfirst
299
300 self._year = time.localtime().tm_year
301 self._century = self._year // 100 * 100
302
303 def _convert(self, lst):
304 dct = {}
305 for i, v in enumerate(lst):
306 if isinstance(v, tuple):
307 for v in v:
308 dct[v.lower()] = i
309 else:
310 dct[v.lower()] = i
311 return dct
312
313 def jump(self, name):
314 return name.lower() in self._jump
315
316 def weekday(self, name):
317 try:
318 return self._weekdays[name.lower()]
319 except KeyError:
320 pass
321 return None
322
323 def month(self, name):
324 try:
325 return self._months[name.lower()] + 1
326 except KeyError:
327 pass
328 return None
329
330 def hms(self, name):
331 try:
332 return self._hms[name.lower()]
333 except KeyError:
334 return None
335
336 def ampm(self, name):
337 try:
338 return self._ampm[name.lower()]
339 except KeyError:
340 return None
341
342 def pertain(self, name):
343 return name.lower() in self._pertain
344
345 def utczone(self, name):
346 return name.lower() in self._utczone
347
348 def tzoffset(self, name):
349 if name in self._utczone:
350 return 0
351
352 return self.TZOFFSET.get(name)
353
354 def convertyear(self, year, century_specified=False):
355 """
356 Converts two-digit years to year within [-50, 49]
357 range of self._year (current local time)
358 """
359
360 # Function contract is that the year is always positive
361 assert year >= 0
362
363 if year < 100 and not century_specified:
364 # assume current century to start
365 year += self._century
366
367 if year >= self._year + 50: # if too far in future
368 year -= 100
369 elif year < self._year - 50: # if too far in past
370 year += 100
371
372 return year
373
374 def validate(self, res):
375 # move to info
376 if res.year is not None:
377 res.year = self.convertyear(res.year, res.century_specified)
378
379 if ((res.tzoffset == 0 and not res.tzname) or
380 (res.tzname == 'Z' or res.tzname == 'z')):
381 res.tzname = "UTC"
382 res.tzoffset = 0
383 elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname):
384 res.tzoffset = 0
385 return True
386
387
388class _ymd(list):
389 def __init__(self, *args, **kwargs):
390 super().__init__(*args, **kwargs)
391 self.century_specified = False
392 self.dstridx = None
393 self.mstridx = None
394 self.ystridx = None
395
396 @property
397 def has_year(self):
398 return self.ystridx is not None
399
400 @property
401 def has_month(self):
402 return self.mstridx is not None
403
404 @property
405 def has_day(self):
406 return self.dstridx is not None
407
408 def could_be_day(self, value):
409 if self.has_day:
410 return False
411 elif not self.has_month:
412 return 1 <= value <= 31
413 elif not self.has_year:
414 # Be permissive, assume leap year
415 month = self[self.mstridx]
416 return 1 <= value <= monthrange(2000, month)[1]
417 else:
418 month = self[self.mstridx]
419 year = self[self.ystridx]
420 return 1 <= value <= monthrange(year, month)[1]
421
422 def append(self, val, label=None):
423 if hasattr(val, '__len__'):
424 if val.isdigit() and len(val) > 2:
425 self.century_specified = True
426 if label not in [None, 'Y']: # pragma: no cover
427 raise ValueError(label)
428 label = 'Y'
429 elif val > 100:
430 self.century_specified = True
431 if label not in [None, 'Y']: # pragma: no cover
432 raise ValueError(label)
433 label = 'Y'
434
435 super().append(int(val))
436
437 if label == 'M':
438 if self.has_month:
439 raise ValueError('Month is already set')
440 self.mstridx = len(self) - 1
441 elif label == 'D':
442 if self.has_day:
443 raise ValueError('Day is already set')
444 self.dstridx = len(self) - 1
445 elif label == 'Y':
446 if self.has_year:
447 raise ValueError('Year is already set')
448 self.ystridx = len(self) - 1
449
450 def _resolve_from_stridxs(self, strids):
451 """
452 Try to resolve the identities of year/month/day elements using
453 ystridx, mstridx, and dstridx, if enough of these are specified.
454 """
455 if len(self) == 3 and len(strids) == 2:
456 # we can back out the remaining stridx value
457 missing = [x for x in range(3) if x not in strids.values()]
458 key = [x for x in ['y', 'm', 'd'] if x not in strids]
459 assert len(missing) == len(key) == 1
460 key = key[0]
461 val = missing[0]
462 strids[key] = val
463
464 assert len(self) == len(strids) # otherwise this should not be called
465 out = {key: self[strids[key]] for key in strids}
466 return (out.get('y'), out.get('m'), out.get('d'))
467
468 def resolve_ymd(self, yearfirst, dayfirst):
469 len_ymd = len(self)
470 year, month, day = (None, None, None)
471
472 strids = (('y', self.ystridx),
473 ('m', self.mstridx),
474 ('d', self.dstridx))
475
476 strids = {key: val for key, val in strids if val is not None}
477 if (len(self) == len(strids) > 0 or
478 (len(self) == 3 and len(strids) == 2)):
479 return self._resolve_from_stridxs(strids)
480
481 mstridx = self.mstridx
482
483 if len_ymd > 3:
484 raise ValueError("More than three YMD values")
485 elif len_ymd == 1 or (mstridx is not None and len_ymd == 2):
486 # One member, or two members with a month string
487 if mstridx is not None:
488 month = self[mstridx]
489 # since mstridx is 0 or 1, self[mstridx-1] always
490 # looks up the other element
491 other = self[mstridx - 1]
492 else:
493 other = self[0]
494
495 if len_ymd > 1 or mstridx is None:
496 if other > 31:
497 year = other
498 else:
499 day = other
500
501 elif len_ymd == 2:
502 # Two members with numbers
503 if self[0] > 31:
504 # 99-01
505 year, month = self
506 elif self[1] > 31:
507 # 01-99
508 month, year = self
509 elif dayfirst and self[1] <= 12:
510 # 13-01
511 day, month = self
512 else:
513 # 01-13
514 month, day = self
515
516 elif len_ymd == 3:
517 # Three members
518 if mstridx == 0:
519 if self[1] > 31:
520 # Apr-2003-25
521 month, year, day = self
522 else:
523 month, day, year = self
524 elif mstridx == 1:
525 if self[0] > 31 or (yearfirst and self[2] <= 31):
526 # 99-Jan-01
527 year, month, day = self
528 else:
529 # 01-Jan-01
530 # Give precedence to day-first, since
531 # two-digit years is usually hand-written.
532 day, month, year = self
533
534 elif mstridx == 2:
535 # WTF!?
536 if self[1] > 31:
537 # 01-99-Jan
538 day, year, month = self
539 else:
540 # 99-01-Jan
541 year, day, month = self
542
543 else:
544 if (self[0] > 31 or
545 self.ystridx == 0 or
546 (yearfirst and self[1] <= 12 and self[2] <= 31)):
547 # 99-01-01
548 if dayfirst and self[2] <= 12:
549 year, day, month = self
550 else:
551 year, month, day = self
552 elif self[0] > 12 or (dayfirst and self[1] <= 12):
553 # 13-01-01
554 day, month, year = self
555 else:
556 # 01-13-01
557 month, day, year = self
558
559 return year, month, day
560
561
562class parser:
563 def __init__(self, info=None):
564 self.info = info or parserinfo()
565
566 def parse(self, timestr, default=None,
567 ignoretz=False, tzinfos=None, **kwargs):
568 """
569 Parse the date/time string into a :class:`datetime.datetime` object.
570
571 :param timestr:
572 Any date/time string using the supported formats.
573
574 :param default:
575 The default datetime object, if this is a datetime object and not
576 ``None``, elements specified in ``timestr`` replace elements in the
577 default object.
578
579 :param ignoretz:
580 If set ``True``, time zones in parsed strings are ignored and a
581 naive :class:`datetime.datetime` object is returned.
582
583 :param tzinfos:
584 Additional time zone names / aliases which may be present in the
585 string. This argument maps time zone names (and optionally offsets
586 from those time zones) to time zones. This parameter can be a
587 dictionary with timezone aliases mapping time zone names to time
588 zones or a function taking two parameters (``tzname`` and
589 ``tzoffset``) and returning a time zone.
590
591 The timezones to which the names are mapped can be an integer
592 offset from UTC in seconds or a :class:`tzinfo` object.
593
594 .. doctest::
595 :options: +NORMALIZE_WHITESPACE
596
597 >>> from dateutil.parser import parse
598 >>> from dateutil.tz import gettz
599 >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")}
600 >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos)
601 datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200))
602 >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos)
603 datetime.datetime(2012, 1, 19, 17, 21,
604 tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago'))
605
606 This parameter is ignored if ``ignoretz`` is set.
607
608 :param \\*\\*kwargs:
609 Keyword arguments as passed to ``_parse()``.
610
611 :return:
612 Returns a :class:`datetime.datetime` object or, if the
613 ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the
614 first element being a :class:`datetime.datetime` object, the second
615 a tuple containing the fuzzy tokens.
616
617 :raises ParserError:
618 Raised for invalid or unknown string format, if the provided
619 :class:`tzinfo` is not in a valid format, or if an invalid date
620 would be created.
621
622 :raises TypeError:
623 Raised for non-string or character stream input.
624
625 :raises OverflowError:
626 Raised if the parsed date exceeds the largest valid C integer on
627 your system.
628 """
629
630 if default is None:
631 default = datetime.datetime.now().replace(hour=0, minute=0,
632 second=0, microsecond=0)
633
634 res, skipped_tokens = self._parse(timestr, **kwargs)
635
636 if res is None:
637 raise ParserError("Unknown string format: %s", timestr)
638
639 if len(res) == 0:
640 raise ParserError("String does not contain a date: %s", timestr)
641
642 try:
643 ret = self._build_naive(res, default)
644 except ValueError as e:
645 raise ParserError(str(e) + ": %s", timestr) from e
646
647 if not ignoretz:
648 ret = self._build_tzaware(ret, res, tzinfos)
649
650 if kwargs.get('fuzzy_with_tokens', False):
651 return ret, skipped_tokens
652 else:
653 return ret
654
655 class _result(_resultbase):
656 __slots__ = ["year", "month", "day", "weekday",
657 "hour", "minute", "second", "microsecond",
658 "tzname", "tzoffset", "ampm","any_unused_tokens"]
659
660 def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False,
661 fuzzy_with_tokens=False):
662 """
663 Private method which performs the heavy lifting of parsing, called from
664 ``parse()``, which passes on its ``kwargs`` to this function.
665
666 :param timestr:
667 The string to parse.
668
669 :param dayfirst:
670 Whether to interpret the first value in an ambiguous 3-integer date
671 (e.g. 01/05/09) as the day (``True``) or month (``False``). If
672 ``yearfirst`` is set to ``True``, this distinguishes between YDM
673 and YMD. If set to ``None``, this value is retrieved from the
674 current :class:`parserinfo` object (which itself defaults to
675 ``False``).
676
677 :param yearfirst:
678 Whether to interpret the first value in an ambiguous 3-integer date
679 (e.g. 01/05/09) as the year. If ``True``, the first number is taken
680 to be the year, otherwise the last number is taken to be the year.
681 If this is set to ``None``, the value is retrieved from the current
682 :class:`parserinfo` object (which itself defaults to ``False``).
683
684 :param fuzzy:
685 Whether to allow fuzzy parsing, allowing for string like "Today is
686 January 1, 2047 at 8:21:00AM".
687
688 :param fuzzy_with_tokens:
689 If ``True``, ``fuzzy`` is automatically set to True, and the parser
690 will return a tuple where the first element is the parsed
691 :class:`datetime.datetime` datetimestamp and the second element is
692 a tuple containing the portions of the string which were ignored:
693
694 .. doctest::
695
696 >>> from dateutil.parser import parse
697 >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True)
698 (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))
699
700 """
701 if fuzzy_with_tokens:
702 fuzzy = True
703
704 info = self.info
705
706 if dayfirst is None:
707 dayfirst = info.dayfirst
708
709 if yearfirst is None:
710 yearfirst = info.yearfirst
711
712 res = self._result()
713 l = _timelex.split(timestr) # Splits the timestr into tokens
714
715 skipped_idxs = []
716
717 # year/month/day list
718 ymd = _ymd()
719
720 len_l = len(l)
721 i = 0
722 try:
723 while i < len_l:
724
725 # Check if it's a number
726 value_repr = l[i]
727 try:
728 value = float(value_repr)
729 except ValueError:
730 value = None
731
732 if value is not None:
733 # Numeric token
734 i = self._parse_numeric_token(l, i, info, ymd, res, fuzzy)
735
736 # Check weekday
737 elif info.weekday(l[i]) is not None:
738 value = info.weekday(l[i])
739 res.weekday = value
740
741 # Check month name
742 elif info.month(l[i]) is not None:
743 value = info.month(l[i])
744 ymd.append(value, 'M')
745
746 if i + 1 < len_l:
747 if l[i + 1] in ('-', '/'):
748 # Jan-01[-99]
749 sep = l[i + 1]
750 ymd.append(l[i + 2])
751
752 if i + 3 < len_l and l[i + 3] == sep:
753 # Jan-01-99
754 ymd.append(l[i + 4])
755 i += 2
756
757 i += 2
758
759 elif (i + 4 < len_l and l[i + 1] == l[i + 3] == ' ' and
760 info.pertain(l[i + 2])):
761 # Jan of 01
762 # In this case, 01 is clearly year
763 if l[i + 4].isdigit():
764 # Convert it here to become unambiguous
765 value = int(l[i + 4])
766 year = str(info.convertyear(value))
767 ymd.append(year, 'Y')
768 else:
769 # Wrong guess
770 pass
771 # TODO: not hit in tests
772 i += 4
773
774 # Check am/pm
775 elif info.ampm(l[i]) is not None:
776 value = info.ampm(l[i])
777 val_is_ampm = self._ampm_valid(res.hour, res.ampm, fuzzy)
778
779 if val_is_ampm:
780 res.hour = self._adjust_ampm(res.hour, value)
781 res.ampm = value
782
783 elif fuzzy:
784 skipped_idxs.append(i)
785
786 # Check for a timezone name
787 elif self._could_be_tzname(res.hour, res.tzname, res.tzoffset, l[i]):
788 res.tzname = l[i]
789 res.tzoffset = info.tzoffset(res.tzname)
790
791 # Check for something like GMT+3, or BRST+3. Notice
792 # that it doesn't mean "I am 3 hours after GMT", but
793 # "my time +3 is GMT". If found, we reverse the
794 # logic so that timezone parsing code will get it
795 # right.
796 if i + 1 < len_l and l[i + 1] in ('+', '-'):
797 l[i + 1] = ('+', '-')[l[i + 1] == '+']
798 res.tzoffset = None
799 if info.utczone(res.tzname):
800 # With something like GMT+3, the timezone
801 # is *not* GMT.
802 res.tzname = None
803
804 # Check for a numbered timezone
805 elif res.hour is not None and l[i] in ('+', '-'):
806 signal = (-1, 1)[l[i] == '+']
807 len_li = len(l[i + 1])
808
809 # TODO: check that l[i + 1] is integer?
810 if len_li == 4:
811 # -0300
812 hour_offset = int(l[i + 1][:2])
813 min_offset = int(l[i + 1][2:])
814 elif i + 2 < len_l and l[i + 2] == ':':
815 # -03:00
816 hour_offset = int(l[i + 1])
817 min_offset = int(l[i + 3]) # TODO: Check that l[i+3] is minute-like?
818 i += 2
819 elif len_li <= 2:
820 # -[0]3
821 hour_offset = int(l[i + 1][:2])
822 min_offset = 0
823 else:
824 raise ValueError(timestr)
825
826 res.tzoffset = signal * (hour_offset * 3600 + min_offset * 60)
827
828 # Look for a timezone name between parenthesis
829 if (i + 5 < len_l and
830 info.jump(l[i + 2]) and l[i + 3] == '(' and
831 l[i + 5] == ')' and
832 3 <= len(l[i + 4]) and
833 self._could_be_tzname(res.hour, res.tzname,
834 None, l[i + 4])):
835 # -0300 (BRST)
836 res.tzname = l[i + 4]
837 i += 4
838
839 i += 1
840
841 # Check jumps
842 elif not (info.jump(l[i]) or fuzzy):
843 raise ValueError(timestr)
844
845 else:
846 skipped_idxs.append(i)
847 i += 1
848
849 # Process year/month/day
850 year, month, day = ymd.resolve_ymd(yearfirst, dayfirst)
851
852 res.century_specified = ymd.century_specified
853 res.year = year
854 res.month = month
855 res.day = day
856
857 except (IndexError, ValueError):
858 return None, None
859
860 if not info.validate(res):
861 return None, None
862
863 if fuzzy_with_tokens:
864 skipped_tokens = self._recombine_skipped(l, skipped_idxs)
865 return res, tuple(skipped_tokens)
866 else:
867 return res, None
868
869 def _parse_numeric_token(self, tokens, idx, info, ymd, res, fuzzy):
870 # Token is a number
871 value_repr = tokens[idx]
872 try:
873 value = self._to_decimal(value_repr)
874 except Exception as e:
875 raise ValueError("Unknown numeric token") from e
876
877 len_li = len(value_repr)
878
879 len_l = len(tokens)
880
881 if (len(ymd) == 3 and len_li in (2, 4) and
882 res.hour is None and
883 (idx + 1 >= len_l or
884 (tokens[idx + 1] != ':' and
885 info.hms(tokens[idx + 1]) is None))):
886 # 19990101T23[59]
887 s = tokens[idx]
888 res.hour = int(s[:2])
889
890 if len_li == 4:
891 res.minute = int(s[2:])
892
893 elif len_li == 6 or (len_li > 6 and tokens[idx].find('.') == 6):
894 # YYMMDD or HHMMSS[.ss]
895 s = tokens[idx]
896
897 if not ymd and '.' not in tokens[idx]:
898 ymd.append(s[:2])
899 ymd.append(s[2:4])
900 ymd.append(s[4:])
901 else:
902 # 19990101T235959[.59]
903
904 # TODO: Check if res attributes already set.
905 res.hour = int(s[:2])
906 res.minute = int(s[2:4])
907 res.second, res.microsecond = self._parsems(s[4:])
908
909 elif len_li in (8, 12, 14):
910 # YYYYMMDD
911 s = tokens[idx]
912 ymd.append(s[:4], 'Y')
913 ymd.append(s[4:6])
914 ymd.append(s[6:8])
915
916 if len_li > 8:
917 res.hour = int(s[8:10])
918 res.minute = int(s[10:12])
919
920 if len_li > 12:
921 res.second = int(s[12:])
922
923 elif self._find_hms_idx(idx, tokens, info, allow_jump=True) is not None:
924 # HH[ ]h or MM[ ]m or SS[.ss][ ]s
925 hms_idx = self._find_hms_idx(idx, tokens, info, allow_jump=True)
926 (idx, hms) = self._parse_hms(idx, tokens, info, hms_idx)
927 if hms is not None:
928 # TODO: checking that hour/minute/second are not
929 # already set?
930 self._assign_hms(res, value_repr, hms)
931
932 elif idx + 2 < len_l and tokens[idx + 1] == ':':
933 # HH:MM[:SS[.ss]]
934 res.hour = int(value)
935 value = self._to_decimal(tokens[idx + 2]) # TODO: try/except for this?
936 (res.minute, res.second) = self._parse_min_sec(value)
937
938 if idx + 4 < len_l and tokens[idx + 3] == ':':
939 res.second, res.microsecond = self._parsems(tokens[idx + 4])
940
941 idx += 2
942
943 idx += 2
944
945 elif idx + 1 < len_l and tokens[idx + 1] in ('-', '/', '.'):
946 sep = tokens[idx + 1]
947 ymd.append(value_repr)
948
949 if idx + 2 < len_l and not info.jump(tokens[idx + 2]):
950 if tokens[idx + 2].isdigit():
951 # 01-01[-01]
952 ymd.append(tokens[idx + 2])
953 else:
954 # 01-Jan[-01]
955 value = info.month(tokens[idx + 2])
956
957 if value is not None:
958 ymd.append(value, 'M')
959 else:
960 raise ValueError()
961
962 if idx + 3 < len_l and tokens[idx + 3] == sep:
963 # We have three members
964 value = info.month(tokens[idx + 4])
965
966 if value is not None:
967 ymd.append(value, 'M')
968 else:
969 ymd.append(tokens[idx + 4])
970 idx += 2
971
972 idx += 1
973 idx += 1
974
975 elif idx + 1 >= len_l or info.jump(tokens[idx + 1]):
976 if idx + 2 < len_l and info.ampm(tokens[idx + 2]) is not None:
977 # 12 am
978 hour = int(value)
979 res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 2]))
980 idx += 1
981 else:
982 # Year, month or day
983 ymd.append(value)
984 idx += 1
985
986 elif info.ampm(tokens[idx + 1]) is not None and (0 <= value < 24):
987 # 12am
988 hour = int(value)
989 res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 1]))
990 idx += 1
991
992 elif ymd.could_be_day(value):
993 ymd.append(value)
994
995 elif not fuzzy:
996 raise ValueError()
997
998 return idx
999
1000 def _find_hms_idx(self, idx, tokens, info, allow_jump):
1001 len_l = len(tokens)
1002
1003 if idx+1 < len_l and info.hms(tokens[idx+1]) is not None:
1004 # There is an "h", "m", or "s" label following this token. We take
1005 # assign the upcoming label to the current token.
1006 # e.g. the "12" in 12h"
1007 hms_idx = idx + 1
1008
1009 elif (allow_jump and idx+2 < len_l and tokens[idx+1] == ' ' and
1010 info.hms(tokens[idx+2]) is not None):
1011 # There is a space and then an "h", "m", or "s" label.
1012 # e.g. the "12" in "12 h"
1013 hms_idx = idx + 2
1014
1015 elif idx > 0 and info.hms(tokens[idx-1]) is not None:
1016 # There is a "h", "m", or "s" preceding this token. Since neither
1017 # of the previous cases was hit, there is no label following this
1018 # token, so we use the previous label.
1019 # e.g. the "04" in "12h04"
1020 hms_idx = idx-1
1021
1022 elif (1 < idx == len_l-1 and tokens[idx-1] == ' ' and
1023 info.hms(tokens[idx-2]) is not None):
1024 # If we are looking at the final token, we allow for a
1025 # backward-looking check to skip over a space.
1026 # TODO: Are we sure this is the right condition here?
1027 hms_idx = idx - 2
1028
1029 else:
1030 hms_idx = None
1031
1032 return hms_idx
1033
1034 def _assign_hms(self, res, value_repr, hms):
1035 # See GH issue #427, fixing float rounding
1036 value = self._to_decimal(value_repr)
1037
1038 if hms == 0:
1039 # Hour
1040 res.hour = int(value)
1041 if value % 1:
1042 res.minute = int(60*(value % 1))
1043
1044 elif hms == 1:
1045 (res.minute, res.second) = self._parse_min_sec(value)
1046
1047 elif hms == 2:
1048 (res.second, res.microsecond) = self._parsems(value_repr)
1049
1050 def _could_be_tzname(self, hour, tzname, tzoffset, token):
1051 return (hour is not None and
1052 tzname is None and
1053 tzoffset is None and
1054 len(token) <= 5 and
1055 (all(x in string.ascii_uppercase for x in token)
1056 or token in self.info.UTCZONE))
1057
1058 def _ampm_valid(self, hour, ampm, fuzzy):
1059 """
1060 For fuzzy parsing, 'a' or 'am' (both valid English words)
1061 may erroneously trigger the AM/PM flag. Deal with that
1062 here.
1063 """
1064 val_is_ampm = True
1065
1066 # If there's already an AM/PM flag, this one isn't one.
1067 if fuzzy and ampm is not None:
1068 val_is_ampm = False
1069
1070 # If AM/PM is found and hour is not, raise a ValueError
1071 if hour is None:
1072 if fuzzy:
1073 val_is_ampm = False
1074 else:
1075 raise ValueError('No hour specified with AM or PM flag.')
1076 elif not 0 <= hour <= 12:
1077 # If AM/PM is found, it's a 12 hour clock, so raise
1078 # an error for invalid range
1079 if fuzzy:
1080 val_is_ampm = False
1081 else:
1082 raise ValueError('Invalid hour specified for 12-hour clock.')
1083
1084 return val_is_ampm
1085
1086 def _adjust_ampm(self, hour, ampm):
1087 if hour < 12 and ampm == 1:
1088 hour += 12
1089 elif hour == 12 and ampm == 0:
1090 hour = 0
1091 return hour
1092
1093 def _parse_min_sec(self, value):
1094 # TODO: Every usage of this function sets res.second to the return
1095 # value. Are there any cases where second will be returned as None and
1096 # we *don't* want to set res.second = None?
1097 minute = int(value)
1098 second = None
1099
1100 sec_remainder = value % 1
1101 if sec_remainder:
1102 second = int(60 * sec_remainder)
1103 return (minute, second)
1104
1105 def _parse_hms(self, idx, tokens, info, hms_idx):
1106 # TODO: Is this going to admit a lot of false-positives for when we
1107 # just happen to have digits and "h", "m" or "s" characters in non-date
1108 # text? I guess hex hashes won't have that problem, but there's plenty
1109 # of random junk out there.
1110 if hms_idx is None:
1111 hms = None
1112 new_idx = idx
1113 elif hms_idx > idx:
1114 hms = info.hms(tokens[hms_idx])
1115 new_idx = hms_idx
1116 else:
1117 # Looking backwards, increment one.
1118 hms = info.hms(tokens[hms_idx]) + 1
1119 new_idx = idx
1120
1121 return (new_idx, hms)
1122
1123 # ------------------------------------------------------------------
1124 # Handling for individual tokens. These are kept as methods instead
1125 # of functions for the sake of customizability via subclassing.
1126
1127 def _parsems(self, value):
1128 """Parse a I[.F] seconds value into (seconds, microseconds)."""
1129 if "." not in value:
1130 return int(value), 0
1131 else:
1132 i, f = value.split(".")
1133 return int(i), int(f.ljust(6, "0")[:6])
1134
1135 def _to_decimal(self, val):
1136 try:
1137 decimal_value = Decimal(val)
1138 # See GH 662, edge case, infinite value should not be converted
1139 # via `_to_decimal`
1140 if not decimal_value.is_finite():
1141 raise ValueError("Converted decimal value is infinite or NaN")
1142 except Exception as e:
1143 msg = "Could not convert %s to decimal" % val
1144 raise ValueError(msg) from e
1145 else:
1146 return decimal_value
1147
1148 # ------------------------------------------------------------------
1149 # Post-Parsing construction of datetime output. These are kept as
1150 # methods instead of functions for the sake of customizability via
1151 # subclassing.
1152
1153 def _build_tzinfo(self, tzinfos, tzname, tzoffset):
1154 if callable(tzinfos):
1155 tzdata = tzinfos(tzname, tzoffset)
1156 else:
1157 tzdata = tzinfos.get(tzname)
1158 # handle case where tzinfo is paased an options that returns None
1159 # eg tzinfos = {'BRST' : None}
1160 if isinstance(tzdata, datetime.tzinfo) or tzdata is None:
1161 tzinfo = tzdata
1162 elif isinstance(tzdata, str):
1163 tzinfo = tz.tzstr(tzdata)
1164 elif isinstance(tzdata, int):
1165 tzinfo = tz.tzoffset(tzname, tzdata)
1166 else:
1167 raise TypeError("Offset must be tzinfo subclass, tz string, "
1168 "or int offset.")
1169 return tzinfo
1170
1171 def _build_tzaware(self, naive, res, tzinfos):
1172 if (callable(tzinfos) or (tzinfos and res.tzname in tzinfos)):
1173 tzinfo = self._build_tzinfo(tzinfos, res.tzname, res.tzoffset)
1174 aware = naive.replace(tzinfo=tzinfo)
1175 aware = self._assign_tzname(aware, res.tzname)
1176
1177 elif res.tzname and res.tzname in time.tzname:
1178 aware = naive.replace(tzinfo=tz.tzlocal())
1179
1180 # Handle ambiguous local datetime
1181 aware = self._assign_tzname(aware, res.tzname)
1182
1183 # This is mostly relevant for winter GMT zones parsed in the UK
1184 if (aware.tzname() != res.tzname and
1185 res.tzname in self.info.UTCZONE):
1186 aware = aware.replace(tzinfo=tz.UTC)
1187
1188 elif res.tzoffset == 0:
1189 aware = naive.replace(tzinfo=tz.UTC)
1190
1191 elif res.tzoffset:
1192 aware = naive.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset))
1193
1194 elif not res.tzname and not res.tzoffset:
1195 # i.e. no timezone information was found.
1196 aware = naive
1197
1198 elif res.tzname:
1199 # tz-like string was parsed but we don't know what to do
1200 # with it
1201 warnings.warn("tzname {tzname} identified but not understood. "
1202 "Pass `tzinfos` argument in order to correctly "
1203 "return a timezone-aware datetime. In a future "
1204 "version, this will raise an "
1205 "exception.".format(tzname=res.tzname),
1206 category=UnknownTimezoneWarning)
1207 aware = naive
1208
1209 return aware
1210
1211 def _build_naive(self, res, default):
1212 repl = {}
1213 for attr in ("year", "month", "day", "hour",
1214 "minute", "second", "microsecond"):
1215 value = getattr(res, attr)
1216 if value is not None:
1217 repl[attr] = value
1218
1219 if 'day' not in repl:
1220 # If the default day exceeds the last day of the month, fall back
1221 # to the end of the month.
1222 cyear = default.year if res.year is None else res.year
1223 cmonth = default.month if res.month is None else res.month
1224 cday = default.day if res.day is None else res.day
1225
1226 if cday > monthrange(cyear, cmonth)[1]:
1227 repl['day'] = monthrange(cyear, cmonth)[1]
1228
1229 naive = default.replace(**repl)
1230
1231 if res.weekday is not None and not res.day:
1232 naive = naive + relativedelta.relativedelta(weekday=res.weekday)
1233
1234 return naive
1235
1236 def _assign_tzname(self, dt, tzname):
1237 if dt.tzname() != tzname:
1238 new_dt = tz.enfold(dt, fold=1)
1239 if new_dt.tzname() == tzname:
1240 return new_dt
1241
1242 return dt
1243
1244 def _recombine_skipped(self, tokens, skipped_idxs):
1245 """
1246 >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"]
1247 >>> skipped_idxs = [0, 1, 2, 5]
1248 >>> _recombine_skipped(tokens, skipped_idxs)
1249 ["foo bar", "baz"]
1250 """
1251 skipped_tokens = []
1252 for i, idx in enumerate(sorted(skipped_idxs)):
1253 if i > 0 and idx - 1 == skipped_idxs[i - 1]:
1254 skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx]
1255 else:
1256 skipped_tokens.append(tokens[idx])
1257
1258 return skipped_tokens
1259
1260
1261DEFAULTPARSER = parser()
1262
1263
1264def parse(timestr, parserinfo=None, **kwargs):
1265 """
1266
1267 Parse a string in one of the supported formats, using the
1268 ``parserinfo`` parameters.
1269
1270 :param timestr:
1271 A string containing a date/time stamp.
1272
1273 :param parserinfo:
1274 A :class:`parserinfo` object containing parameters for the parser.
1275 If ``None``, the default arguments to the :class:`parserinfo`
1276 constructor are used.
1277
1278 The ``**kwargs`` parameter takes the following keyword arguments:
1279
1280 :param default:
1281 The default datetime object, if this is a datetime object and not
1282 ``None``, elements specified in ``timestr`` replace elements in the
1283 default object.
1284
1285 :param ignoretz:
1286 If set ``True``, time zones in parsed strings are ignored and a naive
1287 :class:`datetime` object is returned.
1288
1289 :param tzinfos:
1290 Additional time zone names / aliases which may be present in the
1291 string. This argument maps time zone names (and optionally offsets
1292 from those time zones) to time zones. This parameter can be a
1293 dictionary with timezone aliases mapping time zone names to time
1294 zones or a function taking two parameters (``tzname`` and
1295 ``tzoffset``) and returning a time zone.
1296
1297 The timezones to which the names are mapped can be an integer
1298 offset from UTC in seconds or a :class:`tzinfo` object.
1299
1300 .. doctest::
1301 :options: +NORMALIZE_WHITESPACE
1302
1303 >>> from dateutil.parser import parse
1304 >>> from dateutil.tz import gettz
1305 >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")}
1306 >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos)
1307 datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200))
1308 >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos)
1309 datetime.datetime(2012, 1, 19, 17, 21,
1310 tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago'))
1311
1312 This parameter is ignored if ``ignoretz`` is set.
1313
1314 :param dayfirst:
1315 Whether to interpret the first value in an ambiguous 3-integer date
1316 (e.g. 01/05/09) as the day (``True``) or month (``False``). If
1317 ``yearfirst`` is set to ``True``, this distinguishes between YDM and
1318 YMD. If set to ``None``, this value is retrieved from the current
1319 :class:`parserinfo` object (which itself defaults to ``False``).
1320
1321 :param yearfirst:
1322 Whether to interpret the first value in an ambiguous 3-integer date
1323 (e.g. 01/05/09) as the year. If ``True``, the first number is taken to
1324 be the year, otherwise the last number is taken to be the year. If
1325 this is set to ``None``, the value is retrieved from the current
1326 :class:`parserinfo` object (which itself defaults to ``False``).
1327
1328 :param fuzzy:
1329 Whether to allow fuzzy parsing, allowing for string like "Today is
1330 January 1, 2047 at 8:21:00AM".
1331
1332 :param fuzzy_with_tokens:
1333 If ``True``, ``fuzzy`` is automatically set to True, and the parser
1334 will return a tuple where the first element is the parsed
1335 :class:`datetime.datetime` datetimestamp and the second element is
1336 a tuple containing the portions of the string which were ignored:
1337
1338 .. doctest::
1339
1340 >>> from dateutil.parser import parse
1341 >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True)
1342 (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))
1343
1344 :return:
1345 Returns a :class:`datetime.datetime` object or, if the
1346 ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the
1347 first element being a :class:`datetime.datetime` object, the second
1348 a tuple containing the fuzzy tokens.
1349
1350 :raises ParserError:
1351 Raised for invalid or unknown string formats, if the provided
1352 :class:`tzinfo` is not in a valid format, or if an invalid date would
1353 be created.
1354
1355 :raises OverflowError:
1356 Raised if the parsed date exceeds the largest valid C integer on
1357 your system.
1358 """
1359 if parserinfo:
1360 return parser(parserinfo).parse(timestr, **kwargs)
1361 else:
1362 return DEFAULTPARSER.parse(timestr, **kwargs)
1363
1364
1365class _tzparser:
1366
1367 class _result(_resultbase):
1368
1369 __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset",
1370 "start", "end"]
1371
1372 class _attr(_resultbase):
1373 __slots__ = ["month", "week", "weekday",
1374 "yday", "jyday", "day", "time"]
1375
1376 def __repr__(self):
1377 return self._repr("")
1378
1379 def __init__(self):
1380 _resultbase.__init__(self)
1381 self.start = self._attr()
1382 self.end = self._attr()
1383
1384 def parse(self, tzstr):
1385 res = self._result()
1386 l = [x for x in re.split(r'([,:.]|[a-zA-Z]+|[0-9]+)',tzstr) if x]
1387 used_idxs = list()
1388 try:
1389
1390 len_l = len(l)
1391
1392 i = 0
1393 while i < len_l:
1394 # BRST+3[BRDT[+2]]
1395 j = i
1396 while j < len_l and not [x for x in l[j]
1397 if x in "0123456789:,-+"]:
1398 j += 1
1399 if j != i:
1400 if not res.stdabbr:
1401 offattr = "stdoffset"
1402 res.stdabbr = "".join(l[i:j])
1403 else:
1404 offattr = "dstoffset"
1405 res.dstabbr = "".join(l[i:j])
1406
1407 for ii in range(j):
1408 used_idxs.append(ii)
1409 i = j
1410 if (i < len_l and (l[i] in ('+', '-') or l[i][0] in
1411 "0123456789")):
1412 if l[i] in ('+', '-'):
1413 # Yes, that's right. See the TZ variable
1414 # documentation.
1415 signal = (1, -1)[l[i] == '+']
1416 used_idxs.append(i)
1417 i += 1
1418 else:
1419 signal = -1
1420 len_li = len(l[i])
1421 if len_li == 4:
1422 # -0300
1423 setattr(res, offattr, (int(l[i][:2]) * 3600 +
1424 int(l[i][2:]) * 60) * signal)
1425 elif i + 1 < len_l and l[i + 1] == ':':
1426 # -03:00
1427 setattr(res, offattr,
1428 (int(l[i]) * 3600 +
1429 int(l[i + 2]) * 60) * signal)
1430 used_idxs.append(i)
1431 i += 2
1432 elif len_li <= 2:
1433 # -[0]3
1434 setattr(res, offattr,
1435 int(l[i][:2]) * 3600 * signal)
1436 else:
1437 return None
1438 used_idxs.append(i)
1439 i += 1
1440 if res.dstabbr:
1441 break
1442 else:
1443 break
1444
1445
1446 if i < len_l:
1447 for j in range(i, len_l):
1448 if l[j] == ';':
1449 l[j] = ','
1450
1451 assert l[i] == ','
1452
1453 i += 1
1454
1455 if i >= len_l:
1456 pass
1457 elif (8 <= l.count(',') <= 9 and
1458 not [y for x in l[i:] if x != ','
1459 for y in x if y not in "0123456789+-"]):
1460 # GMT0BST,3,0,30,3600,10,0,26,7200[,3600]
1461 for x in (res.start, res.end):
1462 x.month = int(l[i])
1463 used_idxs.append(i)
1464 i += 2
1465 if l[i] == '-':
1466 value = int(l[i + 1]) * -1
1467 used_idxs.append(i)
1468 i += 1
1469 else:
1470 value = int(l[i])
1471 used_idxs.append(i)
1472 i += 2
1473 if value:
1474 x.week = value
1475 x.weekday = (int(l[i]) - 1) % 7
1476 else:
1477 x.day = int(l[i])
1478 used_idxs.append(i)
1479 i += 2
1480 x.time = int(l[i])
1481 used_idxs.append(i)
1482 i += 2
1483 if i < len_l:
1484 if l[i] in ('-', '+'):
1485 signal = (-1, 1)[l[i] == "+"]
1486 used_idxs.append(i)
1487 i += 1
1488 else:
1489 signal = 1
1490 used_idxs.append(i)
1491 res.dstoffset = (res.stdoffset + int(l[i]) * signal)
1492
1493 # This was a made-up format that is not in normal use
1494 warn(('Parsed time zone "%s"' % tzstr) +
1495 'is in a non-standard dateutil-specific format, which ' +
1496 'is now deprecated; support for parsing this format ' +
1497 'will be removed in future versions. It is recommended ' +
1498 'that you switch to a standard format like the GNU ' +
1499 'TZ variable format.', tz.DeprecatedTzFormatWarning)
1500 elif (l.count(',') == 2 and l[i:].count('/') <= 2 and
1501 not [y for x in l[i:] if x not in (',', '/', 'J', 'M',
1502 '.', '-', ':')
1503 for y in x if y not in "0123456789"]):
1504 for x in (res.start, res.end):
1505 if l[i] == 'J':
1506 # non-leap year day (1 based)
1507 used_idxs.append(i)
1508 i += 1
1509 x.jyday = int(l[i])
1510 elif l[i] == 'M':
1511 # month[-.]week[-.]weekday
1512 used_idxs.append(i)
1513 i += 1
1514 x.month = int(l[i])
1515 used_idxs.append(i)
1516 i += 1
1517 assert l[i] in ('-', '.')
1518 used_idxs.append(i)
1519 i += 1
1520 x.week = int(l[i])
1521 if x.week == 5:
1522 x.week = -1
1523 used_idxs.append(i)
1524 i += 1
1525 assert l[i] in ('-', '.')
1526 used_idxs.append(i)
1527 i += 1
1528 x.weekday = (int(l[i]) - 1) % 7
1529 else:
1530 # year day (zero based)
1531 x.yday = int(l[i]) + 1
1532
1533 used_idxs.append(i)
1534 i += 1
1535
1536 if i < len_l and l[i] == '/':
1537 used_idxs.append(i)
1538 i += 1
1539 # start time
1540 len_li = len(l[i])
1541 if len_li == 4:
1542 # -0300
1543 x.time = (int(l[i][:2]) * 3600 +
1544 int(l[i][2:]) * 60)
1545 elif i + 1 < len_l and l[i + 1] == ':':
1546 # -03:00
1547 x.time = int(l[i]) * 3600 + int(l[i + 2]) * 60
1548 used_idxs.append(i)
1549 i += 2
1550 if i + 1 < len_l and l[i + 1] == ':':
1551 used_idxs.append(i)
1552 i += 2
1553 x.time += int(l[i])
1554 elif len_li <= 2:
1555 # -[0]3
1556 x.time = (int(l[i][:2]) * 3600)
1557 else:
1558 return None
1559 used_idxs.append(i)
1560 i += 1
1561
1562 assert i == len_l or l[i] == ','
1563
1564 i += 1
1565
1566 assert i >= len_l
1567
1568 except (IndexError, ValueError, AssertionError):
1569 return None
1570
1571 unused_idxs = set(range(len_l)).difference(used_idxs)
1572 res.any_unused_tokens = not {l[n] for n in unused_idxs}.issubset({",",":"})
1573 return res
1574
1575
1576DEFAULTTZPARSER = _tzparser()
1577
1578
1579def _parsetz(tzstr):
1580 return DEFAULTTZPARSER.parse(tzstr)
1581
1582
1583class ParserError(ValueError):
1584 """Exception subclass used for any failure to parse a datetime string.
1585
1586 This is a subclass of :py:exc:`ValueError`, and should be raised any time
1587 earlier versions of ``dateutil`` would have raised ``ValueError``.
1588
1589 .. versionadded:: 2.8.1
1590 """
1591 def __str__(self):
1592 try:
1593 return self.args[0] % self.args[1:]
1594 except (TypeError, IndexError):
1595 return super().__str__()
1596
1597 def __repr__(self):
1598 args = ", ".join("'%s'" % arg for arg in self.args)
1599 return "%s(%s)" % (self.__class__.__name__, args)
1600
1601
1602class UnknownTimezoneWarning(RuntimeWarning):
1603 """Raised when the parser finds a timezone it cannot parse into a tzinfo.
1604
1605 .. versionadded:: 2.7.0
1606 """
1607# vim:ts=4:sw=4:et