1
|
|
|
from datetime import datetime |
2
|
|
|
|
3
|
|
|
FAKE_YEAR = 9999 |
4
|
|
|
DEFAULT_FUTURE = datetime(FAKE_YEAR, 12, 31, 23, 59, 59) |
5
|
|
|
DEFAULT_PAST = datetime(FAKE_YEAR, 1, 1, 0, 0) |
6
|
|
|
|
7
|
|
|
|
8
|
|
|
def __get_pdt_calendar(): |
9
|
|
|
try: |
10
|
|
|
import parsedatetime.parsedatetime_consts as pdt |
11
|
|
|
except ImportError: |
12
|
|
|
import parsedatetime as pdt |
13
|
|
|
|
14
|
|
|
consts = pdt.Constants(usePyICU=False) |
15
|
|
|
consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday |
16
|
|
|
calendar = pdt.Calendar(consts) |
17
|
|
|
|
18
|
|
|
return calendar |
19
|
|
|
|
20
|
|
|
|
21
|
|
|
def parse( |
22
|
|
|
date_str, inclusive=False, default_hour=None, default_minute=None, bracketed=False |
23
|
|
|
): |
24
|
|
|
"""Parses a string containing a fuzzy date and returns a datetime.datetime object""" |
25
|
|
|
if not date_str: |
26
|
|
|
return None |
27
|
|
|
elif isinstance(date_str, datetime): |
28
|
|
|
return date_str |
29
|
|
|
|
30
|
|
|
# Don't try to parse anything with 6 or less characters and was parsed from the existing journal. |
31
|
|
|
# It's probably a markdown footnote |
32
|
|
|
if len(date_str) <= 6 and bracketed: |
33
|
|
|
return None |
34
|
|
|
|
35
|
|
|
default_date = DEFAULT_FUTURE if inclusive else DEFAULT_PAST |
36
|
|
|
date = None |
37
|
|
|
year_present = False |
38
|
|
|
while not date: |
39
|
|
|
try: |
40
|
|
|
from dateutil.parser import parse as dateparse |
41
|
|
|
|
42
|
|
|
date = dateparse(date_str, default=default_date) |
43
|
|
|
if date.year == FAKE_YEAR: |
44
|
|
|
date = datetime(datetime.now().year, date.timetuple()[1:6]) |
45
|
|
|
else: |
46
|
|
|
year_present = True |
47
|
|
|
flag = 1 if date.hour == date.minute == 0 else 2 |
48
|
|
|
date = date.timetuple() |
49
|
|
|
except Exception as e: |
50
|
|
|
if e.args[0] == "day is out of range for month": |
51
|
|
|
y, m, d, H, M, S = default_date.timetuple()[:6] |
52
|
|
|
default_date = datetime(y, m, d - 1, H, M, S) |
53
|
|
|
else: |
54
|
|
|
calendar = __get_pdt_calendar() |
55
|
|
|
date, flag = calendar.parse(date_str) |
56
|
|
|
|
57
|
|
|
if not flag: # Oops, unparsable. |
|
|
|
|
58
|
|
|
try: # Try and parse this as a single year |
59
|
|
|
year = int(date_str) |
60
|
|
|
return datetime(year, 1, 1) |
61
|
|
|
except ValueError: |
62
|
|
|
return None |
63
|
|
|
except TypeError: |
64
|
|
|
return None |
65
|
|
|
|
66
|
|
|
if flag == 1: # Date found, but no time. Use the default time. |
67
|
|
|
date = datetime( |
68
|
|
|
*date[:3], |
69
|
|
|
hour=23 if inclusive else default_hour or 0, |
70
|
|
|
minute=59 if inclusive else default_minute or 0, |
71
|
|
|
second=59 if inclusive else 0 |
72
|
|
|
) |
73
|
|
|
else: |
74
|
|
|
date = datetime(*date[:6]) |
75
|
|
|
|
76
|
|
|
# Ugly heuristic: if the date is more than 4 weeks in the future, we got the year wrong. |
77
|
|
|
# Rather then this, we would like to see parsedatetime patched so we can tell it to prefer |
78
|
|
|
# past dates |
79
|
|
|
dt = datetime.now() - date |
80
|
|
|
if dt.days < -28 and not year_present: |
81
|
|
|
date = date.replace(date.year - 1) |
82
|
|
|
return date |
83
|
|
|
|