From a1aba2dd768f21d9780534ce7ca8e6e624235c08 Mon Sep 17 00:00:00 2001
From: Tulir Asokan <tulir@maunium.net>
Date: Wed, 25 Mar 2020 23:43:30 +0200
Subject: [PATCH] Return unconsumed part instead of end index from matcher

Also removed unused regexes
---
 reminder/locale_util.py | 62 ++++++++++++++++++++---------------------
 reminder/locales.py     | 44 ++++++++++++++++++-----------
 reminder/util.py        | 27 +-----------------
 3 files changed, 58 insertions(+), 75 deletions(-)

diff --git a/reminder/locale_util.py b/reminder/locale_util.py
index c1e6551..604878b 100644
--- a/reminder/locale_util.py
+++ b/reminder/locale_util.py
@@ -53,12 +53,12 @@ if TYPE_CHECKING:
 
 class MatcherReturn(NamedTuple):
     params: 'RelativeDeltaParams'
-    end: int
+    unconsumed: str
 
 
 class Matcher(ABC):
     @abstractmethod
-    def match(self, val: str, start: int = 0) -> Optional[MatcherReturn]:
+    def match(self, val: str) -> Optional[MatcherReturn]:
         pass
 
 
@@ -70,45 +70,44 @@ class RegexMatcher(Matcher):
         self.regex = re.compile(pattern, re.IGNORECASE)
         self.value_type = value_type
 
-    def match(self, val: str, start: int = 0) -> Optional[MatcherReturn]:
-        match = self.regex.match(val, pos=start)
-        if match and match.end() > 0:
-            return self._convert_match(match)
+    def match(self, val: str) -> Optional[MatcherReturn]:
+        match = self.regex.match(val)
+        if match and match.end() > 0 and len(match.groups()) > 0:
+            return MatcherReturn(params=self._convert_match(match), unconsumed=val[match.end():])
         return None
 
-    def _convert_match(self, match: Match) -> MatcherReturn:
-        return MatcherReturn(params=self._convert_groups(match.groupdict()),
-                             end=match.end())
+    def _convert_match(self, match: Match) -> 'RelativeDeltaParams':
+        return self._convert_groups(match.groupdict())
 
     def _convert_groups(self, groups: Dict[str, str]) -> 'RelativeDeltaParams':
         return {key: self.value_type(value) for key, value in groups.items() if value}
 
 
 class TimeMatcher(RegexMatcher):
-    def _convert_match(self, match: Match) -> MatcherReturn:
+    def _convert_match(self, match: Match) -> 'RelativeDeltaParams':
         groups = match.groupdict()
         try:
             meridiem = groups.pop("meridiem").lower()
-        except KeyError:
+        except (KeyError, AttributeError):
             meridiem = None
         params = self._convert_groups(groups)
         if meridiem == "pm":
             params["hour"] += 12
         elif meridiem == "am" and params["hour"] == 12:
             params["hour"] = 0
-        return MatcherReturn(params=params, end=match.end())
+        return params
 
 
 class ShortYearMatcher(RegexMatcher):
-    def _convert_match(self, match: Match) -> MatcherReturn:
-        rtrn = super()._convert_match(match)
-        if rtrn.params["year"] < 100:
+    def _convert_match(self, match: Match) -> 'RelativeDeltaParams':
+        params = super()._convert_match(match)
+        if params["year"] < 100:
             year = datetime.now().year
             current_century = year // 100
-            if rtrn.params["year"] < year % 100:
+            if params["year"] < year % 100:
                 current_century += 1
-            rtrn.params["year"] = (current_century * 100) + rtrn.params["year"]
-        return rtrn
+            params["year"] = (current_century * 100) + params["year"]
+        return params
 
 
 class WeekdayMatcher(Matcher):
@@ -121,13 +120,13 @@ class WeekdayMatcher(Matcher):
         self.map = map
         self.substr = substr
 
-    def match(self, val: str, start: int = 0) -> Optional[MatcherReturn]:
-        match = self.regex.match(val, pos=start)
+    def match(self, val: str) -> Optional[MatcherReturn]:
+        match = self.regex.match(val)
         if match and match.end() > 0:
             weekday = self.map[match.string[:self.substr].lower()]
             if isinstance(weekday, int):
                 weekday = (datetime.now().weekday() + weekday) % 7
-            return MatcherReturn(params={"weekday": weekday}, end=match.end())
+            return MatcherReturn(params={"weekday": weekday}, unconsumed=val[match.end():])
         return None
 
 
@@ -151,26 +150,25 @@ class Locale(Matcher):
         return Locale(name=name, timedelta=timedelta or self.timedelta, date=date or self.date,
                       weekday=weekday or self.weekday, time=time or self.time)
 
-    def match(self, val: str, start: int = 0) -> Optional[MatcherReturn]:
-        end = start
-        found_delta = self.timedelta.match(val, start=end)
+    def match(self, val: str) -> Optional[MatcherReturn]:
+        found_delta = self.timedelta.match(val)
         if found_delta:
-            params, end = found_delta
+            params, val = found_delta
         else:
             params = {}
-            found_day = self.weekday.match(val, start=end)
+            found_day = self.weekday.match(val)
             if found_day:
-                params, end = found_day
+                params, val = found_day
             else:
-                found_date = self.date.match(val, start=end)
+                found_date = self.date.match(val)
                 if found_date:
-                    params, end = found_date
+                    params, val = found_date
 
-            found_time = self.time.match(val, start=end)
+            found_time = self.time.match(val)
             if found_time:
                 params = {**params, **found_time.params}
-                end = found_time.end
-        return MatcherReturn(params, end) if len(params) > 0 else None
+                val = found_time.unconsumed
+        return MatcherReturn(params=params, unconsumed=val) if len(params) > 0 else None
 
 
 Locales = Dict[str, Locale]
diff --git a/reminder/locales.py b/reminder/locales.py
index 4b5471f..b29a12d 100644
--- a/reminder/locales.py
+++ b/reminder/locales.py
@@ -30,9 +30,10 @@ locales["en_iso"] = Locale(
                            rf"(?:(?P<days>[-+]?\d+)\s?d(?:ays?)?{td_sep_en})?"
                            rf"(?:(?P<hours>[-+]?\d+)\s?h(?:(?:r|our)?s?){td_sep_en})?"
                            rf"(?:(?P<minutes>[-+]?\d+)\s?m(?:in(?:ute)?s?)?{td_sep_en})?"
-                           r"(?:(?P<seconds>[-+]?\d+)\s?s(?:ec(?:ond)?s?)?)?"),
-    date=RegexMatcher(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"),
-    weekday=WeekdayMatcher(pattern=r"today"
+                           r"(?:(?P<seconds>[-+]?\d+)\s?s(?:ec(?:ond)?s?)?)?"
+                           r"(?:\s|$)"),
+    date=RegexMatcher(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})\s"),
+    weekday=WeekdayMatcher(pattern=r"(?:today"
                                    r"|tomorrow"
                                    r"|mon(?:day)?"
                                    r"|tues?(?:day)?"
@@ -40,7 +41,8 @@ locales["en_iso"] = Locale(
                                    r"|thu(?:rs(?:day)?)?"
                                    r"|fri(?:day)?"
                                    r"|sat(?:urday)?"
-                                   r"|sun(?:day)?",
+                                   r"|sun(?:day)?)"
+                                   r"(?:\s|$)",
                            map={
                                "tod": +0, "tom": +1, "mon": MO, "tue": TU, "wed": WE, "thu": TH,
                                "fri": FR, "sat": SA, "sun": SU,
@@ -48,22 +50,24 @@ locales["en_iso"] = Locale(
     time=RegexMatcher(r"\s?(?:at\s)?"
                       r"(?P<hour>\d{2})"
                       r"[:.](?P<minute>\d{2})"
-                      r"(?:[:.](?P<second>\d{2}))?"),
+                      r"(?:[:.](?P<second>\d{2}))?"
+                      r"(?:\s|$)"),
 )
 
 time_12_en = TimeMatcher(r"\s?(?:at\s)?"
                          r"(?P<hour>\d{2})"
                          r"(?:[:.](?P<minute>\d{2}))?"
                          r"(?:[:.](?P<second>\d{2}))?"
-                         r"(?:\s(?P<meridiem>a\.?m|p\.?m)\.?)?")
+                         r"(?:\s(?P<meridiem>a\.?m|p\.?m)\.?)?"
+                         r"(?:\s|$)")
 
 locales["en_us"] = locales["en_iso"].replace(
-    name="English (US)", time=time_12_en,
-    date=ShortYearMatcher(r"(?P<month>\d{1,2})/(?P<day>\d{1,2})(?:/(?P<year>\d{2}(?:\d{2})?))?"))
+    name="English (US)", time=time_12_en, date=ShortYearMatcher(
+        r"(?P<month>\d{1,2})/(?P<day>\d{1,2})(?:/(?P<year>\d{2}(?:\d{2})?))?(?:\s|$)"))
 
 locales["en_uk"] = locales["en_iso"].replace(
-    name="English (UK)", time=time_12_en,
-    date=ShortYearMatcher(r"(?P<day>\d{1,2})/(?P<month>\d{1,2})(?:/(?P<year>\d{2}(?:\d{2})?))?"))
+    name="English (UK)", time=time_12_en, date=ShortYearMatcher(
+        r"(?P<day>\d{1,2})/(?P<month>\d{1,2})(?:/(?P<year>\d{2}(?:\d{2})?))?(?:\s|$)"))
 
 td_sep_fi = r"(?:[\s,]{1,3}(?:ja\s)?)"
 locales["fi_fi"] = Locale(
@@ -75,8 +79,9 @@ locales["fi_fi"] = Locale(
                            rf"(?:(?P<hours>[-+]?\d+)\s?t(?:un(?:nin?|tia))?{td_sep_fi})?"
                            rf"(?:(?P<minutes>[-+]?\d+)\s?m(?:in(?:uut(?:in?|tia))?)?{td_sep_fi})?"
                            r"(?:(?P<seconds>[-+]?\d+)\s?s(?:ek(?:un(?:nin?|tia))?)?)?"
-                           r"(?:\s(?:kuluttua|päästä?))?"),
-    date=ShortYearMatcher(r"(?P<day>\d{1,2})\.(?P<month>\d{1,2})\.(?P<year>\d{2}(?:\d{2})?)"),
+                           r"(?:\s(?:kuluttua|päästä?))?"
+                           r"(?:\s|$)"),
+    date=ShortYearMatcher(r"(?P<day>\d{1,2})\.(?P<month>\d{1,2})\.(?P<year>\d{2}(?:\d{2})?)\s"),
     weekday=WeekdayMatcher(pattern=r"(?:tänään"
                                    r"|(?:yli)?huomen"
                                    r"|ma(?:aanantai)?"
@@ -86,7 +91,8 @@ locales["fi_fi"] = Locale(
                                    r"|pe(?:rjantai)?"
                                    r"|la(?:uantai)?"
                                    r"|su(?:nnuntai)?)"
-                                   r"(?:na)?",
+                                   r"(?:na)?"
+                                   r"(?:\s|$)",
                            map={
                                "tä": +0, "hu": +1, "yl": +2,
                                "ma": MO, "ti": TU, "ke": WE, "to": TH, "pe": FR, "la": SA, "su": SU,
@@ -94,7 +100,8 @@ locales["fi_fi"] = Locale(
     time=RegexMatcher(r"\s?(?:ke?ll?o\.?\s)?"
                       r"(?P<hour>\d{2})"
                       r"[:.](?P<minute>\d{2})"
-                      r"(?:[:.](?P<second>\d{2}))?"),
+                      r"(?:[:.](?P<second>\d{2}))?"
+                      r"(?:\s|$)"),
 )
 
 td_sep_de = r"(?:[\s,]{1,3}(?:und\s)?)"
@@ -108,7 +115,8 @@ locales["de_de"] = Locale(
                            rf"(?:(?P<hours>[-+]?\d+)\s?stunden?{td_sep_de})?"
                            rf"(?:(?P<minutes>[-+]?\d+)\s?minuten?{td_sep_de})?"
                            r"(?:(?P<seconds>[-+]?\d+)\s?sekunden?)?"),
-    date=ShortYearMatcher(r"(?P<day>\d{1,2})\.(?P<month>\d{1,2})\.(?P<year>\d{2}(?:\d{2})?)"),
+    date=ShortYearMatcher(
+        r"(?P<day>\d{1,2})\.(?P<month>\d{1,2})\.(?P<year>\d{2}(?:\d{2})?)(?:\s|$)"),
     weekday=WeekdayMatcher(pattern=r"(?:heute"
                                    r"|(?:über)?morgen"
                                    r"|mo(?:ntag)?"
@@ -117,7 +125,8 @@ locales["de_de"] = Locale(
                                    r"|do(?:nnerstag)?"
                                    r"|fr(?:eitag)?"
                                    r"|sa(?:mstag)?"
-                                   r"|so(?:nntag)?)",
+                                   r"|so(?:nntag)?)"
+                                   r"(?:\s|$)",
                            map={
                                "heu": +0, "mor": +1, "übe": +2, "mon": MO, "die": TU, "mit": WE,
                                "don": TH, "fre": FR, "sam": SA, "son": SU,
@@ -125,5 +134,6 @@ locales["de_de"] = Locale(
     time=RegexMatcher(r"\s?(?:um\s)?"
                       r"(?P<hour>\d{2})"
                       r"[:.](?P<minute>\d{2})"
-                      r"(?:[:.](?P<second>\d{2}))?"),
+                      r"(?:[:.](?P<second>\d{2}))?"
+                      r"(?:\s|$)"),
 )
diff --git a/reminder/util.py b/reminder/util.py
index 3482f82..a29992a 100644
--- a/reminder/util.py
+++ b/reminder/util.py
@@ -37,31 +37,6 @@ class Config(BaseProxyConfig):
         helper.copy("base_command")
 
 
-timedelta_regex = re.compile(r"(?:(?P<years>[-+]?\d+)\s?y(?:ears?)?\s?)?"
-                             r"(?:(?P<months>[-+]?\d+)\s?months?\s?)?"
-                             r"(?:(?P<weeks>[-+]?\d+)\s?w(?:eeks?)?\s?)?"
-                             r"(?:(?P<days>[-+]?\d+)\s?d(?:ays?)?\s?)?"
-                             r"(?:(?P<hours>[-+]?\d+)\s?h(?:ours?)?\s?)?"
-                             r"(?:(?P<minutes>[-+]?\d+)\s?m(?:inutes?)?\s?)?"
-                             r"(?:(?P<seconds>[-+]?\d+)\s?s(?:econds?)?\s?)?",
-                             flags=re.IGNORECASE)
-date_regex = re.compile(r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})")
-day_regex = re.compile(r"today"
-                       r"|tomorrow"
-                       r"|mon(?:day)?"
-                       r"|tues?(?:day)?"
-                       r"|wed(?:nesday)?"
-                       r"|thu(?:rs(?:day)?)?"
-                       r"|fri(?:day)?"
-                       r"|sat(?:urday)?"
-                       r"|sun(?:day)?",
-                       flags=re.IGNORECASE)
-time_regex = re.compile(r"(?:\sat\s)?(?P<hour>\d{2})"
-                        r"[:.](?P<minute>\d{2})"
-                        r"(?:[:.](?P<second>\d{2}))?",
-                        flags=re.IGNORECASE)
-
-
 class DateArgument(Argument):
     def __init__(self, name: str, label: str = None, *, required: bool = False):
         super().__init__(name, label=label, required=required, pass_raw=True)
@@ -79,7 +54,7 @@ class DateArgument(Argument):
             match = locale.match(val)
             if match:
                 date = (datetime.now(tz=tz) + relativedelta(**match.params)).astimezone(pytz.UTC)
-                return val[match.end:], date
+                return match.unconsumed, date
         return val, None
 
 
-- 
GitLab