Skip to content

Commit

Permalink
Merge pull request #205 from aridevelopment-de/#198/dev
Browse files Browse the repository at this point in the history
Added parsing and evaluating of `first monday of august`

Co-authored-by: @Inf-inity
  • Loading branch information
Ari24-cb24 authored Apr 3, 2023
2 parents d95e85a + a1ea008 commit 6e49149
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 9 deletions.
54 changes: 50 additions & 4 deletions datetimeparser/evaluator/evaluatorutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -145,7 +148,37 @@ def get_base(sanitized_input: list, year: int, current_time: datetime, forced: b
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)

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

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 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:
Expand All @@ -169,15 +202,28 @@ 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)

if out > current_time or forced:
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:
Expand Down
28 changes: 23 additions & 5 deletions datetimeparser/parser/parsermethods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -755,26 +756,40 @@ 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', ...
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)

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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions tests/testcases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=14),
},
# Relative Datetimes
"relative_datetimes": {
Expand Down

0 comments on commit 6e49149

Please sign in to comment.