3 import antlr4 # type: ignore
6 import holidays # type: ignore
9 from typing import Any, Dict, Optional
11 from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
12 from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore
13 from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore
16 class ParseException(Exception):
17 def __init__(self, message: str) -> None:
18 self.message = message
21 class DateParser(dateparse_utilsListener):
22 PARSE_TYPE_SINGLE_DATE_EXPR = 1
23 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
30 self.month_name_to_number = {
44 self.day_name_to_number = {
53 self.delta_unit_to_constant = {
54 "day": DateParser.CONSTANT_DAYS,
55 "wee": DateParser.CONSTANT_WEEKS,
57 self.date: Optional[datetime.date] = None
59 def parse_date_string(self, date_string: str) -> Optional[datetime.date]:
60 input_stream = antlr4.InputStream(date_string)
61 lexer = dateparse_utilsLexer(input_stream)
62 stream = antlr4.CommonTokenStream(lexer)
63 parser = dateparse_utilsParser(stream)
65 walker = antlr4.ParseTreeWalker()
66 walker.walk(self, tree)
67 return self.get_date()
69 def get_date(self) -> Optional[datetime.date]:
72 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
74 self.context: Dict[str, Any] = {}
75 if ctx.singleDateExpr() is not None:
76 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
77 elif ctx.baseAndOffsetDateExpr() is not None:
78 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
81 def normalize_special_day_name(name: str) -> str:
83 name = name.replace("'", "")
84 name = name.replace("xmas", "christmas")
85 name = name.replace("mlk", "martin luther king")
86 name = name.replace(" ", "")
87 eve = "eve" if name[-3:] == "eve" else ""
89 name = name.replace("washi", "presi")
92 def parse_special(self, name: str) -> Optional[datetime.date]:
93 today = datetime.date.today()
94 year = self.context.get("year", today.year)
95 name = DateParser.normalize_special_day_name(self.context["special"])
99 return dateutil.easter.easter(year=year)
100 for holiday_date, holiday_name in sorted(
101 holidays.US(years=year).items()
103 if "Observed" not in holiday_name:
104 holiday_name = DateParser.normalize_special_day_name(
107 if name == holiday_name:
109 if name == "chriseve":
110 return datetime.date(year=year, month=12, day=24)
111 elif name == "newyeeve":
112 return datetime.date(year=year, month=12, day=31)
115 def parse_normal(self) -> datetime.date:
116 if "month" not in self.context:
117 raise ParseException("Missing month")
118 if "day" not in self.context:
119 raise ParseException("Missing day")
120 if "year" not in self.context:
121 today = datetime.date.today()
122 self.context["year"] = today.year
123 return datetime.date(
124 year=int(self.context["year"]),
125 month=int(self.context["month"]),
126 day=int(self.context["day"]),
129 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
130 """When we leave the date expression, populate self.date."""
131 if "special" in self.context:
132 self.date = self.parse_special(self.context["special"])
134 self.date = self.parse_normal()
135 assert self.date is not None
137 # For a single date, just return the date we pulled out.
138 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
141 # Otherwise treat self.date as a base date that we're modifying
143 if not "delta_int" in self.context:
144 raise ParseException("Missing delta_int?!")
145 count = self.context["delta_int"]
149 # Adjust count's sign based on the presence of 'before' or 'after'.
150 if "delta_before_after" in self.context:
151 before_after = self.context["delta_before_after"].lower()
152 if before_after == "before":
155 # What are we counting units of?
156 if "delta_unit" not in self.context:
157 raise ParseException("Missing delta_unit?!")
158 unit = self.context["delta_unit"]
159 if unit == DateParser.CONSTANT_DAYS:
160 timedelta = datetime.timedelta(days=count)
161 self.date = self.date + timedelta
162 elif unit == DateParser.CONSTANT_WEEKS:
163 timedelta = datetime.timedelta(weeks=count)
164 self.date = self.date + timedelta
166 direction = 1 if count > 0 else -1
168 timedelta = datetime.timedelta(days=direction)
171 dow = self.date.weekday()
176 self.date = self.date + timedelta
178 def enterDeltaInt(self, ctx: dateparse_utilsParser.DeltaIntContext) -> None:
180 i = int(ctx.getText())
182 raise ParseException(f"Bad delta int: {ctx.getText()}")
184 self.context["delta_int"] = i
187 self, ctx: dateparse_utilsParser.DeltaUnitContext
190 txt = ctx.getText().lower()[:3]
191 if txt in self.day_name_to_number:
192 txt = self.day_name_to_number[txt]
193 elif txt in self.delta_unit_to_constant:
194 txt = self.delta_unit_to_constant[txt]
196 raise ParseException(f"Bad delta unit: {ctx.getText()}")
198 raise ParseException(f"Bad delta unit: {ctx.getText()}")
200 self.context["delta_unit"] = txt
202 def enterDeltaBeforeAfter(
203 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
206 txt = ctx.getText().lower()
208 raise ParseException(f"Bad delta before|after: {ctx.getText()}")
210 self.context["delta_before_after"] = txt
212 def exitNthWeekdayInMonthMaybeYearExpr(
213 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
215 """Do a bunch of work to convert expressions like...
217 'the 2nd Friday of June' -and-
218 'the last Wednesday in October'
220 ...into base + offset expressions instead.
223 if "nth" not in self.context:
224 raise ParseException(f"Missing nth number: {ctx.getText()}")
225 n = self.context["nth"]
226 if n < 1 or n > 5: # months never have more than 5 Foodays
228 raise ParseException(f"Invalid nth number: {ctx.getText()}")
229 del self.context["nth"]
230 self.context["delta_int"] = n
232 year = self.context.get("year", datetime.date.today().year)
233 if "month" not in self.context:
234 raise ParseException(
235 f"Missing month expression: {ctx.getText()}"
237 month = self.context["month"]
239 dow = self.context["dow"]
240 del self.context["dow"]
241 self.context["delta_unit"] = dow
243 # For the nth Fooday in Month, start at the 1st of the
244 # month and count ahead N Foodays. For the last Fooday in
245 # Month, start at the last of the month and count back one
252 tmp_date = datetime.date(year=year, month=month, day=1)
253 tmp_date = tmp_date - datetime.timedelta(days=1)
255 self.context["year"] = tmp_date.year
256 self.context["month"] = tmp_date.month
257 self.context["day"] = tmp_date.day
259 # The delta adjustment code can handle the case where
260 # the last day of the month is the day we're looking
263 self.context["year"] = year
264 self.context["month"] = month
265 self.context["day"] = 1
266 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
268 raise ParseException(
269 f"Invalid nthWeekday expression: {ctx.getText()}"
272 def exitFirstLastWeekdayInMonthMaybeYearExpr(
274 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
276 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
278 def enterNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
281 m = re.match("\d+[a-z][a-z]", i)
286 raise ParseException(f"Bad nth expression: {ctx.getText()}")
288 self.context["nth"] = i
290 def enterFirstOrLast(
291 self, ctx: dateparse_utilsParser.FirstOrLastContext
300 raise ParseException(
301 f"Bad first|last expression: {ctx.getText()}"
304 raise ParseException(f"Bad first|last expression: {ctx.getText()}")
306 self.context["nth"] = txt
308 def enterDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
310 dow = ctx.getText().lower()[:3]
311 dow = self.day_name_to_number.get(dow, None)
313 raise ParseException("Bad day of week")
315 self.context["dow"] = dow
318 self, ctx: dateparse_utilsParser.DayOfMonthContext
321 day = int(ctx.getText())
322 if day < 1 or day > 31:
323 raise ParseException(
324 f"Bad dayOfMonth expression: {ctx.getText()}"
327 raise ParseException(f"Bad dayOfMonth expression: {ctx.getText()}")
328 self.context["day"] = day
331 self, ctx: dateparse_utilsParser.MonthNameContext
334 month = ctx.getText()
335 month = month.lower()[:3]
336 month = self.month_name_to_number.get(month, None)
338 raise ParseException(
339 f"Bad monthName expression: {ctx.getText()}"
342 raise ParseException(f"Bad monthName expression: {ctx.getText()}")
344 self.context["month"] = month
346 def enterMonthNumber(
347 self, ctx: dateparse_utilsParser.MonthNumberContext
350 month = int(ctx.getText())
351 if month < 1 or month > 12:
352 raise ParseException(
353 f"Bad monthNumber expression: {ctx.getText()}"
356 raise ParseException(f"Bad monthNumber expression: {ctx.getText()}")
358 self.context["month"] = month
360 def enterYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
362 year = int(ctx.getText())
364 raise ParseException(f"Bad year expression: {ctx.getText()}")
366 raise ParseException(f"Bad year expression: {ctx.getText()}")
368 self.context["year"] = year
370 def enterSpecialDate(
371 self, ctx: dateparse_utilsParser.SpecialDateContext
374 txt = ctx.getText().lower()
376 raise ParseException(f"Bad specialDate expression: {ctx.getText()}")
378 self.context["special"] = txt
382 parser = DateParser()
383 for line in sys.stdin:
386 line = re.sub(r"#.*$", "", line)
387 if re.match(r"^ *$", line) is not None:
389 print(parser.parse_date_string(line))
393 if __name__ == "__main__":