summaryrefslogtreecommitdiff
path: root/dateparse/dateparse_utils.py
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2021-03-24 18:08:54 -0700
committerScott Gasch <[email protected]>2021-03-24 18:08:54 -0700
commit497fb9e21f45ec08e1486abaee6dfa7b20b8a691 (patch)
tree47aa97a0fca36c4e7025cee5ad4e9ec6db49b881 /dateparse/dateparse_utils.py
Initial revision
Diffstat (limited to 'dateparse/dateparse_utils.py')
-rwxr-xr-xdateparse/dateparse_utils.py394
1 files changed, 394 insertions, 0 deletions
diff --git a/dateparse/dateparse_utils.py b/dateparse/dateparse_utils.py
new file mode 100755
index 0000000..fb55eef
--- /dev/null
+++ b/dateparse/dateparse_utils.py
@@ -0,0 +1,394 @@
+#!/usr/bin/env python3
+
+import antlr4 # type: ignore
+import datetime
+import dateutil.easter
+import holidays # type: ignore
+import re
+import sys
+from typing import Any, Dict, Optional
+
+from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
+from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore
+from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore
+
+
+class ParseException(Exception):
+ def __init__(self, message: str) -> None:
+ self.message = message
+
+
+class DateParser(dateparse_utilsListener):
+ PARSE_TYPE_SINGLE_DATE_EXPR = 1
+ PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
+ CONSTANT_DAYS = 7
+ CONSTANT_WEEKS = 8
+ CONSTANT_MONTHS = 9
+ CONSTANT_YEARS = 10
+
+ def __init__(self):
+ self.month_name_to_number = {
+ "jan": 1,
+ "feb": 2,
+ "mar": 3,
+ "apr": 4,
+ "may": 5,
+ "jun": 6,
+ "jul": 7,
+ "aug": 8,
+ "sep": 9,
+ "oct": 10,
+ "nov": 11,
+ "dec": 12,
+ }
+ self.day_name_to_number = {
+ "mon": 0,
+ "tue": 1,
+ "wed": 2,
+ "thu": 3,
+ "fri": 4,
+ "sat": 5,
+ "sun": 6,
+ }
+ self.delta_unit_to_constant = {
+ "day": DateParser.CONSTANT_DAYS,
+ "wee": DateParser.CONSTANT_WEEKS,
+ }
+ self.date: Optional[datetime.date] = None
+
+ def parse_date_string(self, date_string: str) -> Optional[datetime.date]:
+ input_stream = antlr4.InputStream(date_string)
+ lexer = dateparse_utilsLexer(input_stream)
+ stream = antlr4.CommonTokenStream(lexer)
+ parser = dateparse_utilsParser(stream)
+ tree = parser.parse()
+ walker = antlr4.ParseTreeWalker()
+ walker.walk(self, tree)
+ return self.get_date()
+
+ def get_date(self) -> Optional[datetime.date]:
+ return self.date
+
+ def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
+ self.date = None
+ self.context: Dict[str, Any] = {}
+ if ctx.singleDateExpr() is not None:
+ self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
+ elif ctx.baseAndOffsetDateExpr() is not None:
+ self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
+
+ @staticmethod
+ def normalize_special_day_name(name: str) -> str:
+ name = name.lower()
+ name = name.replace("'", "")
+ name = name.replace("xmas", "christmas")
+ name = name.replace("mlk", "martin luther king")
+ name = name.replace(" ", "")
+ eve = "eve" if name[-3:] == "eve" else ""
+ name = name[:5] + eve
+ name = name.replace("washi", "presi")
+ return name
+
+ def parse_special(self, name: str) -> Optional[datetime.date]:
+ today = datetime.date.today()
+ year = self.context.get("year", today.year)
+ name = DateParser.normalize_special_day_name(self.context["special"])
+ if name == "today":
+ return today
+ if name == "easte":
+ return dateutil.easter.easter(year=year)
+ for holiday_date, holiday_name in sorted(
+ holidays.US(years=year).items()
+ ):
+ if "Observed" not in holiday_name:
+ holiday_name = DateParser.normalize_special_day_name(
+ holiday_name
+ )
+ if name == holiday_name:
+ return holiday_date
+ if name == "chriseve":
+ return datetime.date(year=year, month=12, day=24)
+ elif name == "newyeeve":
+ return datetime.date(year=year, month=12, day=31)
+ return None
+
+ def parse_normal(self) -> datetime.date:
+ if "month" not in self.context:
+ raise ParseException("Missing month")
+ if "day" not in self.context:
+ raise ParseException("Missing day")
+ if "year" not in self.context:
+ today = datetime.date.today()
+ self.context["year"] = today.year
+ return datetime.date(
+ year=int(self.context["year"]),
+ month=int(self.context["month"]),
+ day=int(self.context["day"]),
+ )
+
+ def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
+ """When we leave the date expression, populate self.date."""
+ if "special" in self.context:
+ self.date = self.parse_special(self.context["special"])
+ else:
+ self.date = self.parse_normal()
+ assert self.date is not None
+
+ # For a single date, just return the date we pulled out.
+ if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
+ return
+
+ # Otherwise treat self.date as a base date that we're modifying
+ # with an offset.
+ if not "delta_int" in self.context:
+ raise ParseException("Missing delta_int?!")
+ count = self.context["delta_int"]
+ if count == 0:
+ return
+
+ # Adjust count's sign based on the presence of 'before' or 'after'.
+ if "delta_before_after" in self.context:
+ before_after = self.context["delta_before_after"].lower()
+ if before_after == "before":
+ count = -count
+
+ # What are we counting units of?
+ if "delta_unit" not in self.context:
+ raise ParseException("Missing delta_unit?!")
+ unit = self.context["delta_unit"]
+ if unit == DateParser.CONSTANT_DAYS:
+ timedelta = datetime.timedelta(days=count)
+ self.date = self.date + timedelta
+ elif unit == DateParser.CONSTANT_WEEKS:
+ timedelta = datetime.timedelta(weeks=count)
+ self.date = self.date + timedelta
+ else:
+ direction = 1 if count > 0 else -1
+ count = abs(count)
+ timedelta = datetime.timedelta(days=direction)
+
+ while True:
+ dow = self.date.weekday()
+ if dow == unit:
+ count -= 1
+ if count == 0:
+ return
+ self.date = self.date + timedelta
+
+ def enterDeltaInt(self, ctx: dateparse_utilsParser.DeltaIntContext) -> None:
+ try:
+ i = int(ctx.getText())
+ except:
+ raise ParseException(f"Bad delta int: {ctx.getText()}")
+ else:
+ self.context["delta_int"] = i
+
+ def enterDeltaUnit(
+ self, ctx: dateparse_utilsParser.DeltaUnitContext
+ ) -> None:
+ try:
+ txt = ctx.getText().lower()[:3]
+ if txt in self.day_name_to_number:
+ txt = self.day_name_to_number[txt]
+ elif txt in self.delta_unit_to_constant:
+ txt = self.delta_unit_to_constant[txt]
+ else:
+ raise ParseException(f"Bad delta unit: {ctx.getText()}")
+ except:
+ raise ParseException(f"Bad delta unit: {ctx.getText()}")
+ else:
+ self.context["delta_unit"] = txt
+
+ def enterDeltaBeforeAfter(
+ self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
+ ) -> None:
+ try:
+ txt = ctx.getText().lower()
+ except:
+ raise ParseException(f"Bad delta before|after: {ctx.getText()}")
+ else:
+ self.context["delta_before_after"] = txt
+
+ def exitNthWeekdayInMonthMaybeYearExpr(
+ self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
+ ) -> None:
+ """Do a bunch of work to convert expressions like...
+
+ 'the 2nd Friday of June' -and-
+ 'the last Wednesday in October'
+
+ ...into base + offset expressions instead.
+ """
+ try:
+ if "nth" not in self.context:
+ raise ParseException(f"Missing nth number: {ctx.getText()}")
+ n = self.context["nth"]
+ if n < 1 or n > 5: # months never have more than 5 Foodays
+ if n != -1:
+ raise ParseException(f"Invalid nth number: {ctx.getText()}")
+ del self.context["nth"]
+ self.context["delta_int"] = n
+
+ year = self.context.get("year", datetime.date.today().year)
+ if "month" not in self.context:
+ raise ParseException(
+ f"Missing month expression: {ctx.getText()}"
+ )
+ month = self.context["month"]
+
+ dow = self.context["dow"]
+ del self.context["dow"]
+ self.context["delta_unit"] = dow
+
+ # For the nth Fooday in Month, start at the 1st of the
+ # month and count ahead N Foodays. For the last Fooday in
+ # Month, start at the last of the month and count back one
+ # Fooday.
+ if n == -1:
+ month += 1
+ if month == 13:
+ month = 1
+ year += 1
+ tmp_date = datetime.date(year=year, month=month, day=1)
+ tmp_date = tmp_date - datetime.timedelta(days=1)
+
+ self.context["year"] = tmp_date.year
+ self.context["month"] = tmp_date.month
+ self.context["day"] = tmp_date.day
+
+ # The delta adjustment code can handle the case where
+ # the last day of the month is the day we're looking
+ # for already.
+ else:
+ self.context["year"] = year
+ self.context["month"] = month
+ self.context["day"] = 1
+ self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
+ except:
+ raise ParseException(
+ f"Invalid nthWeekday expression: {ctx.getText()}"
+ )
+
+ def exitFirstLastWeekdayInMonthMaybeYearExpr(
+ self,
+ ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
+ ) -> None:
+ self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
+
+ def enterNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
+ try:
+ i = ctx.getText()
+ m = re.match("\d+[a-z][a-z]", i)
+ if m is not None:
+ i = i[:-2]
+ i = int(i)
+ except:
+ raise ParseException(f"Bad nth expression: {ctx.getText()}")
+ else:
+ self.context["nth"] = i
+
+ def enterFirstOrLast(
+ self, ctx: dateparse_utilsParser.FirstOrLastContext
+ ) -> None:
+ try:
+ txt = ctx.getText()
+ if txt == "first":
+ txt = 1
+ elif txt == "last":
+ txt = -1
+ else:
+ raise ParseException(
+ f"Bad first|last expression: {ctx.getText()}"
+ )
+ except:
+ raise ParseException(f"Bad first|last expression: {ctx.getText()}")
+ else:
+ self.context["nth"] = txt
+
+ def enterDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
+ try:
+ dow = ctx.getText().lower()[:3]
+ dow = self.day_name_to_number.get(dow, None)
+ except:
+ raise ParseException("Bad day of week")
+ else:
+ self.context["dow"] = dow
+
+ def enterDayOfMonth(
+ self, ctx: dateparse_utilsParser.DayOfMonthContext
+ ) -> None:
+ try:
+ day = int(ctx.getText())
+ if day < 1 or day > 31:
+ raise ParseException(
+ f"Bad dayOfMonth expression: {ctx.getText()}"
+ )
+ except:
+ raise ParseException(f"Bad dayOfMonth expression: {ctx.getText()}")
+ self.context["day"] = day
+
+ def enterMonthName(
+ self, ctx: dateparse_utilsParser.MonthNameContext
+ ) -> None:
+ try:
+ month = ctx.getText()
+ month = month.lower()[:3]
+ month = self.month_name_to_number.get(month, None)
+ if month is None:
+ raise ParseException(
+ f"Bad monthName expression: {ctx.getText()}"
+ )
+ except:
+ raise ParseException(f"Bad monthName expression: {ctx.getText()}")
+ else:
+ self.context["month"] = month
+
+ def enterMonthNumber(
+ self, ctx: dateparse_utilsParser.MonthNumberContext
+ ) -> None:
+ try:
+ month = int(ctx.getText())
+ if month < 1 or month > 12:
+ raise ParseException(
+ f"Bad monthNumber expression: {ctx.getText()}"
+ )
+ except:
+ raise ParseException(f"Bad monthNumber expression: {ctx.getText()}")
+ else:
+ self.context["month"] = month
+
+ def enterYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
+ try:
+ year = int(ctx.getText())
+ if year < 1:
+ raise ParseException(f"Bad year expression: {ctx.getText()}")
+ except:
+ raise ParseException(f"Bad year expression: {ctx.getText()}")
+ else:
+ self.context["year"] = year
+
+ def enterSpecialDate(
+ self, ctx: dateparse_utilsParser.SpecialDateContext
+ ) -> None:
+ try:
+ txt = ctx.getText().lower()
+ except:
+ raise ParseException(f"Bad specialDate expression: {ctx.getText()}")
+ else:
+ self.context["special"] = txt
+
+
+def main() -> None:
+ parser = DateParser()
+ for line in sys.stdin:
+ line = line.strip()
+ line = line.lower()
+ line = re.sub(r"#.*$", "", line)
+ if re.match(r"^ *$", line) is not None:
+ continue
+ print(parser.parse_date_string(line))
+ sys.exit(0)
+
+
+if __name__ == "__main__":
+ main()