|
|
|
#!/usr/bin/env python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
|
|
Date
|
|
|
|
"""
|
|
|
|
from dateutil import parser
|
|
|
|
|
|
|
|
from rebulk.remodule import re
|
|
|
|
|
|
|
|
_dsep = r'[-/ \.]'
|
|
|
|
_dsep_bis = r'[-/ \.x]'
|
|
|
|
|
|
|
|
date_regexps = [
|
|
|
|
# pylint:disable=consider-using-f-string
|
|
|
|
re.compile(r'%s((\d{8}))%s' % (_dsep, _dsep), re.IGNORECASE),
|
|
|
|
# pylint:disable=consider-using-f-string
|
|
|
|
re.compile(r'%s((\d{6}))%s' % (_dsep, _dsep), re.IGNORECASE),
|
|
|
|
# pylint:disable=consider-using-f-string
|
|
|
|
re.compile(r'(?:^|[^\d])((\d{2})%s(\d{1,2})%s(\d{1,2}))(?:$|[^\d])' % (_dsep, _dsep), re.IGNORECASE),
|
|
|
|
# pylint:disable=consider-using-f-string
|
|
|
|
re.compile(r'(?:^|[^\d])((\d{1,2})%s(\d{1,2})%s(\d{2}))(?:$|[^\d])' % (_dsep, _dsep), re.IGNORECASE),
|
|
|
|
# pylint:disable=consider-using-f-string
|
|
|
|
re.compile(r'(?:^|[^\d])((\d{4})%s(\d{1,2})%s(\d{1,2}))(?:$|[^\d])' % (_dsep_bis, _dsep), re.IGNORECASE),
|
|
|
|
# pylint:disable=consider-using-f-string
|
|
|
|
re.compile(r'(?:^|[^\d])((\d{1,2})%s(\d{1,2})%s(\d{4}))(?:$|[^\d])' % (_dsep, _dsep_bis), re.IGNORECASE),
|
|
|
|
# pylint:disable=consider-using-f-string
|
|
|
|
re.compile(r'(?:^|[^\d])((\d{1,2}(?:st|nd|rd|th)?%s(?:[a-z]{3,10})%s\d{4}))(?:$|[^\d])' % (_dsep, _dsep),
|
|
|
|
# pylint:disable=consider-using-f-string
|
|
|
|
re.IGNORECASE)]
|
|
|
|
|
|
|
|
|
|
|
|
def valid_year(year):
|
|
|
|
"""Check if number is a valid year"""
|
|
|
|
return 1920 <= year < 2030
|
|
|
|
|
|
|
|
|
|
|
|
def _is_int(string):
|
|
|
|
"""
|
|
|
|
Check if the input string is an integer
|
|
|
|
|
|
|
|
:param string:
|
|
|
|
:type string:
|
|
|
|
:return:
|
|
|
|
:rtype:
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
int(string)
|
|
|
|
return True
|
|
|
|
except ValueError:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def _guess_day_first_parameter(groups): # pylint:disable=inconsistent-return-statements
|
|
|
|
"""
|
|
|
|
If day_first is not defined, use some heuristic to fix it.
|
|
|
|
It helps to solve issues with python dateutils 2.5.3 parser changes.
|
|
|
|
|
|
|
|
:param groups: match groups found for the date
|
|
|
|
:type groups: list of match objects
|
|
|
|
:return: day_first option guessed value
|
|
|
|
:rtype: bool
|
|
|
|
"""
|
|
|
|
|
|
|
|
# If match starts with a long year, then day_first is force to false.
|
|
|
|
if _is_int(groups[0]) and valid_year(int(groups[0][:4])):
|
|
|
|
return False
|
|
|
|
# If match ends with a long year, the day_first is forced to true.
|
|
|
|
if _is_int(groups[-1]) and valid_year(int(groups[-1][-4:])):
|
|
|
|
return True
|
|
|
|
# If match starts with a short year, then day_first is force to false.
|
|
|
|
if _is_int(groups[0]) and int(groups[0][:2]) > 31:
|
|
|
|
return False
|
|
|
|
# If match ends with a short year, then day_first is force to true.
|
|
|
|
if _is_int(groups[-1]) and int(groups[-1][-2:]) > 31:
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def search_date(string, year_first=None, day_first=None): # pylint:disable=inconsistent-return-statements
|
|
|
|
"""Looks for date patterns, and if found return the date and group span.
|
|
|
|
|
|
|
|
Assumes there are sentinels at the beginning and end of the string that
|
|
|
|
always allow matching a non-digit delimiting the date.
|
|
|
|
|
|
|
|
Year can be defined on two digit only. It will return the nearest possible
|
|
|
|
date from today.
|
|
|
|
|
|
|
|
>>> search_date(' This happened on 2002-04-22. ')
|
|
|
|
(18, 28, datetime.date(2002, 4, 22))
|
|
|
|
|
|
|
|
>>> search_date(' And this on 17-06-1998. ')
|
|
|
|
(13, 23, datetime.date(1998, 6, 17))
|
|
|
|
|
|
|
|
>>> search_date(' no date in here ')
|
|
|
|
"""
|
|
|
|
for date_re in date_regexps:
|
|
|
|
search_match = date_re.search(string)
|
|
|
|
if not search_match:
|
|
|
|
continue
|
|
|
|
|
|
|
|
start, end = search_match.start(1), search_match.end(1)
|
|
|
|
groups = search_match.groups()[1:]
|
|
|
|
match = '-'.join(groups)
|
|
|
|
|
|
|
|
if match is None:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if year_first and day_first is None:
|
|
|
|
day_first = False
|
|
|
|
|
|
|
|
if day_first is None:
|
|
|
|
day_first = _guess_day_first_parameter(groups)
|
|
|
|
|
|
|
|
# If day_first/year_first is undefined, parse is made using both possible values.
|
|
|
|
yearfirst_opts = [False, True]
|
|
|
|
if year_first is not None:
|
|
|
|
yearfirst_opts = [year_first]
|
|
|
|
|
|
|
|
dayfirst_opts = [True, False]
|
|
|
|
if day_first is not None:
|
|
|
|
dayfirst_opts = [day_first]
|
|
|
|
|
|
|
|
kwargs_list = ({'dayfirst': d, 'yearfirst': y}
|
|
|
|
for d in dayfirst_opts for y in yearfirst_opts)
|
|
|
|
for kwargs in kwargs_list:
|
|
|
|
try:
|
|
|
|
date = parser.parse(match, **kwargs)
|
|
|
|
except (ValueError, TypeError): # pragma: no cover
|
|
|
|
# see https://bugs.launchpad.net/dateutil/+bug/1247643
|
|
|
|
date = None
|
|
|
|
|
|
|
|
# check date plausibility
|
|
|
|
if date and valid_year(date.year): # pylint:disable=no-member
|
|
|
|
return start, end, date.date() # pylint:disable=no-member
|