Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dateutil/parser/_parser.py: 87%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

808 statements  

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