From e69c87b7286506488cdfc931fa9d91043791080d Mon Sep 17 00:00:00 2001 From: Ari van Houten Date: Tue, 28 Mar 2023 16:02:10 +0200 Subject: [PATCH 1/7] Fixed #198 --- datetimeparser/parser/parsermethods.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/datetimeparser/parser/parsermethods.py b/datetimeparser/parser/parsermethods.py index 359c309..f57f880 100644 --- a/datetimeparser/parser/parsermethods.py +++ b/datetimeparser/parser/parsermethods.py @@ -676,6 +676,7 @@ class AbsolutePrepositionParser: ) RELATIVE_DATETIME_CONSTANTS = (*NumberCountConstants.ALL, *NumberConstants.ALL, *DatetimeConstants.ALL) + ABSOLUTE_RELATIVE_DATETIME_CONSTANTS = (*WeekdayConstants.ALL, *MonthConstants.ALL) RELATIVE_TIME_CONSTANTS = DatetimeConstants.ALL RELATIVE_DATA_SKIPPABLE_WORDS = ( @@ -755,6 +756,8 @@ def _parse_relative_statement(self, relative_statement: str, preposition: str) - returned_data.append(int(argument)) continue + found_keyword = False + # '1st', '1.', 'first', ... # 'one', 'two', 'three', ... # 'seconds', 'minutes', 'hours', ... @@ -766,15 +769,27 @@ def _parse_relative_statement(self, relative_statement: str, preposition: str) - returned_data.append(1) returned_data.append(keyword) + elif keyword in NumberCountConstants.ALL: + if not returned_data: + # The keyword comes before the actual datetime constant 'second (monday of August)' + returned_data.append(keyword) else: returned_data.append(keyword.value) + found_keyword = True break - else: - continue - break - else: + # 'monday', 'tuesday', 'wednesday', ... + # 'january', 'february', 'march', ... + # GitHub issue #198 + for keyword in self.ABSOLUTE_RELATIVE_DATETIME_CONSTANTS: + if argument in keyword.get_all(): + returned_data.append(keyword) + + found_keyword = True + break + + if not found_keyword: return None return returned_data @@ -824,6 +839,9 @@ def _concatenate_relative_data( else: # Otherwise, we cannot concatenate both parts of the data, so we just append the current one returned_data.append(current_data) + elif value in NumberCountConstants.ALL and unit in (*DatetimeConstants.ALL, *WeekdayConstants.ALL, *MonthConstants.ALL): + returned_data.append(value) + returned_data.append(unit) return returned_data From bbb2ef495bcab1387c9fd86e9d5bff09a6154d04 Mon Sep 17 00:00:00 2001 From: Ari van Houten Date: Tue, 28 Mar 2023 16:11:31 +0200 Subject: [PATCH 2/7] Fixed one being prepended if first keyword was a NumberCountConstant in absolute prepositions --- datetimeparser/parser/parsermethods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetimeparser/parser/parsermethods.py b/datetimeparser/parser/parsermethods.py index f57f880..5bc79c5 100644 --- a/datetimeparser/parser/parsermethods.py +++ b/datetimeparser/parser/parsermethods.py @@ -764,7 +764,7 @@ def _parse_relative_statement(self, relative_statement: str, preposition: str) - for keyword in self.RELATIVE_DATETIME_CONSTANTS: if argument in keyword.get_all(): if keyword in DatetimeConstants.ALL: - if not returned_data or not isinstance(returned_data[-1], int): + if not returned_data or (not isinstance(returned_data[-1], int) and not returned_data[-1] in NumberCountConstants.ALL): # For cases like 'day and month (before christmas)' returned_data.append(1) From bd798204d5fa64b666c2c1024dca0ffe1d3adc7f Mon Sep 17 00:00:00 2001 From: Ari van Houten Date: Tue, 28 Mar 2023 16:14:15 +0200 Subject: [PATCH 3/7] Added testcases --- tests/testcases.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/testcases.py b/tests/testcases.py index b99a87c..be64787 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -106,6 +106,8 @@ def is_leap_year(year: int) -> bool: # GitHub issue #176 "10 days after pi-day": Expected(time_sensitive=True, month=3, day=14, delta=relativedelta(days=10)), "10 days before tau day": Expected(time_sensitive=True, month=6, day=28, delta=relativedelta(days=-10)), + # GitHub issue #198 + "second monday of august 2023": Expected(year=2023, month=8, day=7), }, # Relative Datetimes "relative_datetimes": { From c8299426816634e32020a337e4fdb08d7c18aa4b Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 30 Mar 2023 18:28:07 +0200 Subject: [PATCH 4/7] Fixed wrong testcase --- tests/testcases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testcases.py b/tests/testcases.py index be64787..6f150ae 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -107,7 +107,7 @@ def is_leap_year(year: int) -> bool: "10 days after pi-day": Expected(time_sensitive=True, month=3, day=14, delta=relativedelta(days=10)), "10 days before tau day": Expected(time_sensitive=True, month=6, day=28, delta=relativedelta(days=-10)), # GitHub issue #198 - "second monday of august 2023": Expected(year=2023, month=8, day=7), + "second monday of august 2023": Expected(year=2023, month=8, day=14), }, # Relative Datetimes "relative_datetimes": { From 9be7bfa448b315ca8004526d9fe9a1b4da190fc6 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 30 Mar 2023 18:28:37 +0200 Subject: [PATCH 5/7] Adjusted evaluator --- datetimeparser/evaluator/evaluatorutils.py | 57 ++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/datetimeparser/evaluator/evaluatorutils.py b/datetimeparser/evaluator/evaluatorutils.py index b4f66f9..1b9da71 100644 --- a/datetimeparser/evaluator/evaluatorutils.py +++ b/datetimeparser/evaluator/evaluatorutils.py @@ -112,6 +112,10 @@ def sanitize_input( if current_time > test_out and not given_year: parsed_list = EvaluatorUtils.x_week_of_month(relative_dt, idx, pars2, year + 1) + else: + if isinstance(parsed_list[idx + 1], AbsoluteDateTime): + given_year = parsed_list[idx + 1].year + return list(filter(lambda e: e not in Keywords.ALL and not isinstance(e, str), parsed_list)), given_year @staticmethod @@ -124,8 +128,7 @@ def cut_time(time: datetime) -> datetime: return datetime(time.year, time.month, time.day, 0, 0, 0) - @staticmethod - def get_base(sanitized_input: list, year: int, current_time: datetime, forced: bool = False) -> datetime: + def get_base(self, sanitized_input: list, year: int, current_time: datetime, forced: bool = False) -> datetime: """ Takes the last elements from the list and tries to generate a basis for further processing from them The base consists of at least one constant, to which values are then assigned @@ -140,12 +143,45 @@ def get_base(sanitized_input: list, year: int, current_time: datetime, forced: b if isinstance(sanitized_input[-1], AbsoluteDateTime): # The Constant that the AbsoluteDateTime object should be based on (the year) if isinstance(sanitized_input[-2], Constant): + print(sanitized_input[-2].name, sanitized_input) # An Integer giving the information about the 'x'th of something (f.e. "first of august") if isinstance(sanitized_input[-3], int): dt: datetime = sanitized_input[-2].time_value(sanitized_input[-1].year) day: int = sanitized_input[-3] return datetime(dt.year, dt.month, day, dt.hour, dt.minute, dt.second) - return sanitized_input[-2].time_value(sanitized_input[-1].year) + + if sanitized_input[-2].time_value: + dt = sanitized_input[-2].time_value(sanitized_input[-1].year) + print(dt) + + if isinstance(sanitized_input[-3], Constant) and sanitized_input[-3].value: + dt += relativedelta(days=sanitized_input[-3].value - 1) + elif isinstance(sanitized_input[-3], Constant) and not sanitized_input[-3].value: + val = sanitized_input[-4].value + + if sanitized_input[-3].name == "days": + return datetime(dt.year, dt.month, val, dt.hour, dt.minute, dt.second) + if sanitized_input[-3].name == "weeks": + dt = self.get_week_of(dt) + return dt + relativedelta(weeks=val-1) + if sanitized_input[-3].name == "months": + return datetime(dt.year, val, dt.day, dt.hour, dt.minute, dt.second) + + days_dict = {x.name: x.time_value(dt) for x in WeekdayConstants.ALL} + if sanitized_input[-3].name in days_dict: + dt = datetime.strptime(days_dict.get(sanitized_input[-3].name), "%Y-%m-%d %H:%M:%S") + dt += relativedelta(weeks=val - 1) + + return dt + + else: + if sanitized_input[-3].value: + val: int = sanitized_input[-3].value + + if sanitized_input[-2].name == "days": + return datetime(sanitized_input[-1].year, 1, val, 0, 0, 0) + if sanitized_input[-2].name == "months": + return datetime(sanitized_input[-1].year, val, 1, 0, 0, 0) # If a year is given but no months/days, they will be set to '1' because datetime can't handle month/day-values with '0' if sanitized_input[-1].year != 0: @@ -169,8 +205,8 @@ def get_base(sanitized_input: list, year: int, current_time: datetime, forced: b # If no AbsoluteDatetime is given, the default year will be used instead elif isinstance(sanitized_input[-1], Constant): + dt: datetime = sanitized_input[-1].time_value(year) if isinstance(sanitized_input[-2], int): - dt: datetime = sanitized_input[-1].time_value(year) day: int = sanitized_input[-2] out = datetime(dt.year, dt.month, day, dt.hour, dt.minute, dt.second) @@ -178,6 +214,19 @@ def get_base(sanitized_input: list, year: int, current_time: datetime, forced: b return out out += relativedelta(years=1) return out + else: + if len(sanitized_input) == 3: + val: int = sanitized_input[-3].value + + if sanitized_input[-2].name == "days": + return datetime(dt.year, dt.month, dt.day + val, dt.hour, dt.minute, dt.second) + if sanitized_input[-2].name == "weeks": + dt = self.get_week_of(dt) + return dt + relativedelta(weeks=val) + if sanitized_input[-2].name == "months": + return datetime(dt.year, dt.month + val, dt.day, dt.hour, dt.minute, dt.second) + if not isinstance(sanitized_input[-2], RelativeDateTime): + return datetime(dt.year, dt.month, sanitized_input[-2].value, dt.hour, dt.minute, dt.second) # Checks if an event already happened this year (f.e. eastern). If so, the next year will be used if sanitized_input[-1].time_value(year) > current_time or forced: From 92786bfe2a0ed64c690f4609c83ea8f6b2585e2d Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 30 Mar 2023 18:30:21 +0200 Subject: [PATCH 6/7] Removed debug print-statements --- datetimeparser/evaluator/evaluatorutils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/datetimeparser/evaluator/evaluatorutils.py b/datetimeparser/evaluator/evaluatorutils.py index 1b9da71..88ff7a6 100644 --- a/datetimeparser/evaluator/evaluatorutils.py +++ b/datetimeparser/evaluator/evaluatorutils.py @@ -143,7 +143,6 @@ def get_base(self, sanitized_input: list, year: int, current_time: datetime, for if isinstance(sanitized_input[-1], AbsoluteDateTime): # The Constant that the AbsoluteDateTime object should be based on (the year) if isinstance(sanitized_input[-2], Constant): - print(sanitized_input[-2].name, sanitized_input) # An Integer giving the information about the 'x'th of something (f.e. "first of august") if isinstance(sanitized_input[-3], int): dt: datetime = sanitized_input[-2].time_value(sanitized_input[-1].year) @@ -152,7 +151,6 @@ def get_base(self, sanitized_input: list, year: int, current_time: datetime, for if sanitized_input[-2].time_value: dt = sanitized_input[-2].time_value(sanitized_input[-1].year) - print(dt) if isinstance(sanitized_input[-3], Constant) and sanitized_input[-3].value: dt += relativedelta(days=sanitized_input[-3].value - 1) From a1ea0087ec6ab1622917a557c3935a7fcb967cfb Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 30 Mar 2023 18:43:38 +0200 Subject: [PATCH 7/7] Adjusted code --- datetimeparser/evaluator/evaluatorutils.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/datetimeparser/evaluator/evaluatorutils.py b/datetimeparser/evaluator/evaluatorutils.py index 88ff7a6..b658c46 100644 --- a/datetimeparser/evaluator/evaluatorutils.py +++ b/datetimeparser/evaluator/evaluatorutils.py @@ -172,14 +172,13 @@ def get_base(self, sanitized_input: list, year: int, current_time: datetime, for return dt - else: - if sanitized_input[-3].value: - val: int = sanitized_input[-3].value + elif sanitized_input[-3].value: + val: int = sanitized_input[-3].value - if sanitized_input[-2].name == "days": - return datetime(sanitized_input[-1].year, 1, val, 0, 0, 0) - if sanitized_input[-2].name == "months": - return datetime(sanitized_input[-1].year, val, 1, 0, 0, 0) + if sanitized_input[-2].name == "days": + return datetime(sanitized_input[-1].year, 1, val, 0, 0, 0) + if sanitized_input[-2].name == "months": + return datetime(sanitized_input[-1].year, val, 1, 0, 0, 0) # If a year is given but no months/days, they will be set to '1' because datetime can't handle month/day-values with '0' if sanitized_input[-1].year != 0: