Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dateparser/parser.py: 93%
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
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
1import calendar
2from collections import OrderedDict
3from datetime import datetime, timedelta, timezone
4from io import StringIO
6import pytz
7import regex as re
9from dateparser.utils import (
10 _get_missing_parts,
11 get_last_day_of_month,
12 get_next_leap_year,
13 get_previous_leap_year,
14 get_timezone_from_tz_string,
15 set_correct_day_from_settings,
16 set_correct_month_from_settings,
17)
18from dateparser.utils.strptime import strptime
20NSP_COMPATIBLE = re.compile(r"\D+")
21MERIDIAN = re.compile(r"am|pm")
22MICROSECOND = re.compile(r"\d{1,6}")
23EIGHT_DIGIT = re.compile(r"^\d{8}$")
24HOUR_MINUTE_REGEX = re.compile(r"^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$")
27def no_space_parser_eligibile(datestring):
28 src = NSP_COMPATIBLE.search(datestring)
29 if not src or ":" == src.group():
30 return True
31 return False
34def get_unresolved_attrs(parser_object):
35 attrs = ["year", "month", "day"]
36 seen = []
37 unseen = []
38 for attr in attrs:
39 if getattr(parser_object, attr, None) is not None:
40 seen.append(attr)
41 else:
42 unseen.append(attr)
43 return seen, unseen
46date_order_chart = {
47 "DMY": "%d%m%y",
48 "DYM": "%d%y%m",
49 "MDY": "%m%d%y",
50 "MYD": "%m%y%d",
51 "YDM": "%y%d%m",
52 "YMD": "%y%m%d",
53}
56def resolve_date_order(order, lst=None):
57 chart_list = {
58 "DMY": ["day", "month", "year"],
59 "DYM": ["day", "year", "month"],
60 "MDY": ["month", "day", "year"],
61 "MYD": ["month", "year", "day"],
62 "YDM": ["year", "day", "month"],
63 "YMD": ["year", "month", "day"],
64 }
66 return chart_list[order] if lst else date_order_chart[order]
69def _parse_absolute(datestring, settings, tz=None):
70 return _parser.parse(datestring, settings, tz)
73def _parse_nospaces(datestring, settings, tz=None):
74 return _no_spaces_parser.parse(datestring, settings)
77class _time_parser:
78 time_directives = [
79 "%H:%M:%S",
80 "%I:%M:%S %p",
81 "%H:%M",
82 "%I:%M %p",
83 "%I %p",
84 "%H:%M:%S.%f",
85 "%I:%M:%S.%f %p",
86 "%H:%M %p",
87 ]
89 def __call__(self, timestring):
90 _timestring = timestring
91 for directive in self.time_directives:
92 try:
93 return strptime(timestring.strip(), directive).time()
94 except ValueError:
95 pass
96 else:
97 raise ValueError("%s does not seem to be a valid time string" % _timestring)
100time_parser = _time_parser()
103class _no_spaces_parser:
104 _dateformats = [
105 "%Y%m%d",
106 "%Y%d%m",
107 "%m%Y%d",
108 "%m%d%Y",
109 "%d%Y%m",
110 "%d%m%Y",
111 "%y%m%d",
112 "%y%d%m",
113 "%m%y%d",
114 "%m%d%y",
115 "%d%y%m",
116 "%d%m%y",
117 ]
119 _preferred_formats = ["%Y%m%d%H%M", "%Y%m%d%H%M%S", "%Y%m%d%H%M%S.%f"]
121 _preferred_formats_ordered_8_digit = [
122 "%m%d%Y",
123 "%d%m%Y",
124 "%Y%m%d",
125 "%Y%d%m",
126 "%m%Y%d",
127 "%d%Y%m",
128 ]
130 _timeformats = ["%H%M%S.%f", "%H%M%S", "%H%M", "%H"]
132 period = {"day": ["%d", "%H", "%M", "%S"], "month": ["%m"]}
134 _default_order = resolve_date_order("MDY")
136 def __init__(self, *args, **kwargs):
137 self._all = (
138 self._dateformats
139 + [x + y for x in self._dateformats for y in self._timeformats]
140 + self._timeformats
141 )
143 self.date_formats = {
144 "%m%d%y": (
145 self._preferred_formats
146 + sorted(
147 self._all,
148 key=lambda x: x.lower().startswith("%m%d%y"),
149 reverse=True,
150 )
151 ),
152 "%m%y%d": sorted(
153 self._all, key=lambda x: x.lower().startswith("%m%y%d"), reverse=True
154 ),
155 "%y%m%d": sorted(
156 self._all, key=lambda x: x.lower().startswith("%y%m%d"), reverse=True
157 ),
158 "%y%d%m": sorted(
159 self._all, key=lambda x: x.lower().startswith("%y%d%m"), reverse=True
160 ),
161 "%d%m%y": sorted(
162 self._all, key=lambda x: x.lower().startswith("%d%m%y"), reverse=True
163 ),
164 "%d%y%m": sorted(
165 self._all, key=lambda x: x.lower().startswith("%d%y%m"), reverse=True
166 ),
167 }
169 @classmethod
170 def _get_period(cls, format_string):
171 for pname, pdrv in sorted(cls.period.items(), key=lambda x: x[0]):
172 for drv in pdrv:
173 if drv in format_string:
174 return pname
175 else:
176 return "year"
178 @classmethod
179 def _find_best_matching_date(cls, datestring):
180 for fmt in cls._preferred_formats_ordered_8_digit:
181 try:
182 dt = strptime(datestring, fmt), cls._get_period(fmt)
183 if len(str(dt[0].year)) == 4:
184 return dt
185 except Exception:
186 pass
187 return None
189 @classmethod
190 def parse(cls, datestring, settings):
191 if not no_space_parser_eligibile(datestring):
192 raise ValueError("Unable to parse date from: %s" % datestring)
194 datestring = datestring.replace(":", "")
195 if not datestring:
196 raise ValueError("Empty string")
197 tokens = tokenizer(datestring)
198 if settings.DATE_ORDER:
199 order = resolve_date_order(settings.DATE_ORDER)
200 else:
201 order = cls._default_order
202 if EIGHT_DIGIT.match(datestring):
203 dt = cls._find_best_matching_date(datestring)
204 if dt is not None:
205 return dt
206 nsp = cls()
207 ambiguous_date = None
208 for token, _ in tokens.tokenize():
209 for fmt in nsp.date_formats[order]:
210 try:
211 dt = strptime(token, fmt), cls._get_period(fmt)
212 if len(str(dt[0].year)) < 4:
213 ambiguous_date = dt
214 continue
216 missing = _get_missing_parts(fmt)
217 _check_strict_parsing(missing, settings)
218 return dt
219 except Exception:
220 pass
221 else:
222 if ambiguous_date:
223 return ambiguous_date
224 else:
225 raise ValueError("Unable to parse date from: %s" % datestring)
228def _get_missing_error(missing):
229 return "Fields missing from the date string: {}".format(", ".join(missing))
232def _check_strict_parsing(missing, settings):
233 if settings.STRICT_PARSING and missing:
234 raise ValueError(_get_missing_error(missing))
235 elif settings.REQUIRE_PARTS and missing:
236 errors = [part for part in settings.REQUIRE_PARTS if part in missing]
237 if errors:
238 raise ValueError(_get_missing_error(errors))
241class _parser:
242 alpha_directives = OrderedDict(
243 [
244 ("weekday", ["%A", "%a"]),
245 ("month", ["%B", "%b"]),
246 ]
247 )
249 num_directives = {
250 "month": ["%m"],
251 "day": ["%d"],
252 "year": ["%y", "%Y"],
253 }
255 def __init__(self, tokens, settings):
256 self.settings = settings
257 self.tokens = [(t[0].strip(), t[1]) for t in list(tokens)]
258 self.filtered_tokens = [
259 (t[0], t[1], i) for i, t in enumerate(self.tokens) if t[1] <= 1
260 ]
262 self.unset_tokens = []
264 self.day = None
265 self.month = None
266 self.year = None
267 self.time = None
269 self.auto_order = []
271 self._token_day = None
272 self._token_month = None
273 self._token_year = None
274 self._token_time = None
276 self.ordered_num_directives = OrderedDict(
277 (k, self.num_directives[k])
278 for k in (resolve_date_order(settings.DATE_ORDER, lst=True))
279 )
281 skip_index = []
282 skip_component = None
283 skip_tokens = ["t", "year", "hour", "minute"]
285 for index, token_type_original_index in enumerate(self.filtered_tokens):
286 if index in skip_index:
287 continue
289 token, type, original_index = token_type_original_index
291 if token in skip_tokens:
292 continue
294 if self.time is None:
295 meridian_index = index + 1
297 try:
298 # try case where hours and minutes are separated by a period. Example: 13.20.
299 _is_before_period = self.tokens[original_index + 1][0] == "."
300 _is_after_period = (
301 original_index != 0
302 and self.tokens[original_index - 1][0] == "."
303 )
305 if _is_before_period and not _is_after_period:
306 index_next_token = index + 1
307 next_token = self.filtered_tokens[index_next_token][0]
308 index_in_tokens_for_next_token = self.filtered_tokens[
309 index_next_token
310 ][2]
312 next_token_is_last = (
313 index_next_token == len(self.filtered_tokens) - 1
314 )
315 if (
316 next_token_is_last
317 or self.tokens[index_in_tokens_for_next_token + 1][0] != "."
318 ):
319 new_token = token + ":" + next_token
320 if re.match(HOUR_MINUTE_REGEX, new_token):
321 token = new_token
322 skip_index.append(index + 1)
323 meridian_index += 1
324 except Exception:
325 pass
327 try:
328 microsecond = MICROSECOND.search(
329 self.filtered_tokens[index + 1][0]
330 ).group()
331 # Is after time token? raise ValueError if ':' can't be found:
332 token.index(":")
333 # Is after period? raise ValueError if '.' can't be found:
334 self.tokens[self.tokens.index((token, 0)) + 1][0].index(".")
335 except Exception:
336 microsecond = None
338 if microsecond:
339 meridian_index += 1
341 try:
342 meridian = MERIDIAN.search(
343 self.filtered_tokens[meridian_index][0]
344 ).group()
345 except Exception:
346 meridian = None
348 if any([":" in token, meridian, microsecond]):
349 if meridian and not microsecond:
350 self._token_time = "%s %s" % (token, meridian)
351 skip_index.append(meridian_index)
352 elif microsecond and not meridian:
353 self._token_time = "%s.%s" % (token, microsecond)
354 skip_index.append(index + 1)
355 elif meridian and microsecond:
356 self._token_time = "%s.%s %s" % (token, microsecond, meridian)
357 skip_index.append(index + 1)
358 skip_index.append(meridian_index)
359 else:
360 self._token_time = token
361 self.time = lambda: time_parser(self._token_time)
362 continue
364 results = self._parse(type, token, skip_component=skip_component)
365 for res in results:
366 if len(token) == 4 and res[0] == "year":
367 skip_component = "year"
368 setattr(self, *res)
370 known, unknown = get_unresolved_attrs(self)
371 params = {}
372 for attr in known:
373 params.update({attr: getattr(self, attr)})
374 for attr in unknown:
375 for token, type, _ in self.unset_tokens:
376 if type == 0:
377 params.update({attr: int(token)})
378 setattr(self, "_token_%s" % attr, token)
379 setattr(self, attr, int(token))
381 def _get_period(self):
382 if self.settings.RETURN_TIME_AS_PERIOD:
383 if getattr(self, "time", None):
384 return "time"
386 for period in ["time", "day"]:
387 if getattr(self, period, None):
388 return "day"
390 for period in ["month", "year"]:
391 if getattr(self, period, None):
392 return period
394 if self._results():
395 return "day"
397 def _get_datetime_obj(self, **params):
398 try:
399 return datetime(**params)
400 except ValueError as e:
401 error_text = e.__str__()
402 error_msgs = ["day is out of range", "day must be in", "must be in range"]
403 if any(msg in error_text for msg in error_msgs):
404 if not (self._token_day or hasattr(self, "_token_weekday")):
405 # if day is not available put last day of the month
406 params["day"] = get_last_day_of_month(
407 params["year"], params["month"]
408 )
409 return datetime(**params)
410 elif (
411 not self._token_year
412 and params["day"] == 29
413 and params["month"] == 2
414 and not calendar.isleap(params["year"])
415 ):
416 # fix the year when year is not present and it is 29 of February
417 params["year"] = self._get_correct_leap_year(
418 self.settings.PREFER_DATES_FROM, params["year"]
419 )
420 return datetime(**params)
421 raise e
423 def _get_correct_leap_year(self, prefer_dates_from, current_year):
424 if prefer_dates_from == "future":
425 return get_next_leap_year(current_year)
426 if prefer_dates_from == "past":
427 return get_previous_leap_year(current_year)
429 # Default case ('current_period'): return closer leap year
430 next_leap_year = get_next_leap_year(current_year)
431 previous_leap_year = get_previous_leap_year(current_year)
432 next_leap_year_is_closer = (
433 next_leap_year - current_year < current_year - previous_leap_year
434 )
435 return next_leap_year if next_leap_year_is_closer else previous_leap_year
437 def _set_relative_base(self):
438 self.now = self.settings.RELATIVE_BASE
439 if not self.now:
440 self.now = datetime.now(tz=timezone.utc).replace(tzinfo=None)
442 def _get_datetime_obj_params(self):
443 if not self.now:
444 self._set_relative_base()
446 params = {
447 "day": self.day or self.now.day,
448 "month": self.month or self.now.month,
449 "year": self.year or self.now.year,
450 "hour": 0,
451 "minute": 0,
452 "second": 0,
453 "microsecond": 0,
454 }
455 return params
457 def _get_date_obj(self, token, directive):
458 return strptime(token, directive)
460 def _results(self):
461 missing = [
462 field for field in ("day", "month", "year") if not getattr(self, field)
463 ]
464 _check_strict_parsing(missing, self.settings)
465 self._set_relative_base()
467 time = self.time() if self.time is not None else None
468 params = self._get_datetime_obj_params()
470 if time:
471 params.update(
472 dict(
473 hour=time.hour,
474 minute=time.minute,
475 second=time.second,
476 microsecond=time.microsecond,
477 )
478 )
480 return self._get_datetime_obj(**params)
482 def _correct_for_time_frame(self, dateobj, tz):
483 days = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
485 token_weekday, _ = getattr(self, "_token_weekday", (None, None))
487 if token_weekday and not (
488 self._token_year or self._token_month or self._token_day
489 ):
490 day_index = calendar.weekday(dateobj.year, dateobj.month, dateobj.day)
491 day = token_weekday[:3].lower()
492 steps = 0
493 if "future" in self.settings.PREFER_DATES_FROM:
494 if days[day_index] == day:
495 steps = 7
496 else:
497 while days[day_index] != day:
498 day_index = (day_index + 1) % 7
499 steps += 1
500 delta = timedelta(days=steps)
501 else:
502 if days[day_index] == day:
503 if self.settings.PREFER_DATES_FROM == "past":
504 steps = 7
505 else:
506 steps = 0
507 else:
508 while days[day_index] != day:
509 day_index -= 1
510 steps += 1
511 delta = timedelta(days=-steps)
513 dateobj = dateobj + delta
515 # set the token_month here so that it is not subsequently
516 # altered by _correct_for_month
517 self._token_month = dateobj.month
519 # NOTE: If this assert fires, self.now needs to be made offset-aware in a similar
520 # way that dateobj is temporarily made offset-aware.
521 assert not (self.now.tzinfo is None and dateobj.tzinfo is not None), (
522 "`self.now` doesn't have `tzinfo`. Review comment in code for details."
523 )
525 # Store the original dateobj values so that upon subsequent parsing everything is not
526 # treated as offset-aware if offset awareness is changed.
527 original_dateobj = dateobj
529 # Since date comparisons must be either offset-naive or offset-aware, normalize dateobj
530 # to be offset-aware if one or the other is already offset-aware.
531 if self.now.tzinfo is not None and dateobj.tzinfo is None:
532 dateobj = pytz.utc.localize(dateobj)
534 if self.month and not self.year:
535 try:
536 if self.now < dateobj:
537 if self.settings.PREFER_DATES_FROM == "past":
538 dateobj = dateobj.replace(year=dateobj.year - 1)
539 else:
540 if self.settings.PREFER_DATES_FROM == "future":
541 dateobj = dateobj.replace(year=dateobj.year + 1)
542 except ValueError as e:
543 if dateobj.day == 29 and dateobj.month == 2:
544 valid_year = self._get_correct_leap_year(
545 self.settings.PREFER_DATES_FROM, dateobj.year
546 )
547 dateobj = dateobj.replace(year=valid_year)
548 else:
549 raise e
551 if self._token_year and len(self._token_year[0]) == 2:
552 if self.now < dateobj:
553 if "past" in self.settings.PREFER_DATES_FROM:
554 dateobj = dateobj.replace(year=dateobj.year - 100)
555 else:
556 if "future" in self.settings.PREFER_DATES_FROM:
557 dateobj = dateobj.replace(year=dateobj.year + 100)
559 if self._token_time and not any(
560 [
561 self._token_year,
562 self._token_month,
563 self._token_day,
564 hasattr(self, "_token_weekday"),
565 ]
566 ):
567 # Convert dateobj to utc time to compare with self.now
568 try:
569 tz = tz or get_timezone_from_tz_string(self.settings.TIMEZONE)
570 tz_offset = tz.utcoffset(dateobj)
571 except (pytz.UnknownTimeZoneError, pytz.NonExistentTimeError):
572 tz_offset = timedelta(hours=0)
574 if "past" in self.settings.PREFER_DATES_FROM:
575 if self.now < dateobj - tz_offset:
576 dateobj = dateobj + timedelta(days=-1)
577 if "future" in self.settings.PREFER_DATES_FROM:
578 if self.now > dateobj - tz_offset:
579 dateobj = dateobj + timedelta(days=1)
581 # Reset dateobj to the original value, thus removing any offset awareness that may
582 # have been set earlier.
583 dateobj = dateobj.replace(tzinfo=original_dateobj.tzinfo)
585 return dateobj
587 def _correct_for_day(self, dateobj):
588 if (
589 getattr(self, "_token_day", None)
590 or getattr(self, "_token_weekday", None)
591 or getattr(self, "_token_time", None)
592 ):
593 return dateobj
595 dateobj = set_correct_day_from_settings(
596 dateobj, self.settings, current_day=self.now.day
597 )
598 return dateobj
600 def _correct_for_month(self, dateobj):
601 relative_base = getattr(self.settings, "RELATIVE_BASE", None)
602 relative_base_month = (
603 relative_base.month if hasattr(relative_base, "month") else relative_base
604 )
606 if getattr(self, "_token_month", None):
607 return dateobj
609 dateobj = set_correct_month_from_settings(
610 dateobj, self.settings, relative_base_month
611 )
612 return dateobj
614 @classmethod
615 def parse(cls, datestring, settings, tz=None):
616 tokens = tokenizer(datestring)
617 po = cls(tokens.tokenize(), settings)
618 dateobj = po._results()
620 # correction for past, future if applicable
621 dateobj = po._correct_for_time_frame(dateobj, tz)
623 # correction for preference of month: beginning, current, end
624 # must happen before day so that day is derived from the correct month
625 dateobj = po._correct_for_month(dateobj)
627 # correction for preference of day: beginning, current, end
628 dateobj = po._correct_for_day(dateobj)
630 period = po._get_period()
632 return dateobj, period
634 def _parse(self, type, token, skip_component=None):
635 def set_and_return(token, type, component, dateobj, skip_date_order=False):
636 if not skip_date_order:
637 self.auto_order.append(component)
638 setattr(self, "_token_%s" % component, (token, type))
639 return [(component, getattr(dateobj, component))]
641 def parse_number(token, skip_component=None):
642 type = 0
644 for component, directives in self.ordered_num_directives.items():
645 if skip_component == component:
646 continue
647 for directive in directives:
648 try:
649 do = self._get_date_obj(token, directive)
650 prev_value = getattr(self, component, None)
651 if not prev_value:
652 return set_and_return(token, type, component, do)
653 else:
654 try:
655 prev_token, prev_type = getattr(
656 self, "_token_%s" % component
657 )
658 if prev_type == type:
659 do = self._get_date_obj(prev_token, directive)
660 except ValueError:
661 self.unset_tokens.append(
662 (prev_token, prev_type, component)
663 )
664 return set_and_return(token, type, component, do)
665 except ValueError:
666 pass
667 else:
668 raise ValueError("Unable to parse: %s" % token)
670 def parse_alpha(token, skip_component=None):
671 type = 1
673 for component, directives in self.alpha_directives.items():
674 if skip_component == component:
675 continue
676 for directive in directives:
677 try:
678 do = self._get_date_obj(token, directive)
679 prev_value = getattr(self, component, None)
680 if not prev_value:
681 return set_and_return(
682 token, type, component, do, skip_date_order=True
683 )
684 elif component == "month":
685 index = self.auto_order.index("month")
686 self.auto_order[index] = "day"
687 setattr(self, "_token_day", self._token_month)
688 setattr(self, "_token_month", (token, type))
689 return [
690 (component, getattr(do, component)),
691 ("day", prev_value),
692 ]
693 except Exception:
694 pass
695 else:
696 raise ValueError("Unable to parse: %s" % token)
698 handlers = {0: parse_number, 1: parse_alpha}
699 return handlers[type](token, skip_component)
702class tokenizer:
703 digits = "0123456789:"
704 letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
706 def _isletter(self, tkn):
707 return tkn in self.letters
709 def _isdigit(self, tkn):
710 return tkn in self.digits
712 def __init__(self, ds):
713 self.instream = StringIO(ds)
715 def _switch(self, chara, charb):
716 if self._isdigit(chara):
717 return 0, not self._isdigit(charb)
719 if self._isletter(chara):
720 return 1, not self._isletter(charb)
722 return 2, self._isdigit(charb) or self._isletter(charb)
724 def tokenize(self):
725 token = ""
726 EOF = False
728 while not EOF:
729 nextchar = self.instream.read(1)
731 if not nextchar:
732 EOF = True
733 type, _ = self._switch(token[-1], nextchar)
734 yield token, type
735 return
737 if token:
738 type, switch = self._switch(token[-1], nextchar)
740 if not switch:
741 token += nextchar
742 else:
743 yield token, type
744 token = nextchar
745 else:
746 token += nextchar